@enterprisestandard/react 0.0.5-beta.20260115.1 → 0.0.5-beta.20260115.3
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/dist/group-store.js +127 -0
- package/dist/iam.js +680 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +144 -3672
- package/dist/session-store.js +105 -0
- package/dist/sso-server.d.ts +1 -1
- package/dist/sso-server.d.ts.map +1 -1
- package/dist/sso-server.js +46 -0
- package/dist/sso.js +820 -0
- package/dist/tenant-server.js +6 -0
- package/dist/tenant.js +324 -0
- package/dist/types/base-user.js +1 -0
- package/dist/types/enterprise-user.js +1 -0
- package/dist/types/oidc-schema.js +328 -0
- package/dist/types/scim-schema.js +519 -0
- package/dist/types/standard-schema.js +1 -0
- package/dist/types/user.js +1 -0
- package/dist/types/workload-schema.js +208 -0
- package/dist/ui/sign-in-loading.js +8 -0
- package/dist/ui/signed-in.js +8 -0
- package/dist/ui/signed-out.js +8 -0
- package/dist/ui/sso-provider.js +275 -0
- package/dist/user-store.js +114 -0
- package/dist/utils.js +23 -0
- package/dist/vault.js +22 -0
- package/dist/workload-server.d.ts +1 -1
- package/dist/workload-server.d.ts.map +1 -1
- package/dist/workload-server.js +167 -0
- package/dist/workload-token-store.js +95 -0
- package/dist/workload.js +691 -0
- package/package.json +1 -1
- package/dist/index.js.map +0 -29
package/dist/iam.js
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { groupResourceSchema, userSchema } from './types/scim-schema.js';
|
|
2
|
+
const SCIM_CONTENT_TYPE = 'application/scim+json';
|
|
3
|
+
/**
|
|
4
|
+
* Send SCIM error response
|
|
5
|
+
*/
|
|
6
|
+
function scimErrorResponse(status, detail, scimType) {
|
|
7
|
+
return new Response(JSON.stringify({
|
|
8
|
+
schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
|
|
9
|
+
status: String(status),
|
|
10
|
+
scimType,
|
|
11
|
+
detail,
|
|
12
|
+
}), {
|
|
13
|
+
status,
|
|
14
|
+
headers: { 'Content-Type': SCIM_CONTENT_TYPE },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Send SCIM list response
|
|
19
|
+
*/
|
|
20
|
+
function scimListResponse(resources) {
|
|
21
|
+
return new Response(JSON.stringify({
|
|
22
|
+
schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
|
|
23
|
+
totalResults: resources.length,
|
|
24
|
+
startIndex: 1,
|
|
25
|
+
itemsPerPage: resources.length,
|
|
26
|
+
Resources: resources,
|
|
27
|
+
}), {
|
|
28
|
+
status: 200,
|
|
29
|
+
headers: { 'Content-Type': SCIM_CONTENT_TYPE },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Send SCIM resource response
|
|
34
|
+
*/
|
|
35
|
+
function scimResourceResponse(resource, status = 200) {
|
|
36
|
+
return new Response(JSON.stringify(resource), {
|
|
37
|
+
status,
|
|
38
|
+
headers: { 'Content-Type': SCIM_CONTENT_TYPE },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Convert StoredGroup to GroupResource for SCIM response
|
|
43
|
+
*/
|
|
44
|
+
function storedGroupToResource(group) {
|
|
45
|
+
return {
|
|
46
|
+
schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'],
|
|
47
|
+
id: group.id,
|
|
48
|
+
externalId: group.externalId,
|
|
49
|
+
displayName: group.displayName,
|
|
50
|
+
members: group.members,
|
|
51
|
+
meta: {
|
|
52
|
+
resourceType: 'Group',
|
|
53
|
+
created: group.createdAt.toISOString(),
|
|
54
|
+
lastModified: group.updatedAt.toISOString(),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Generate a UUID for new resources
|
|
60
|
+
*/
|
|
61
|
+
function generateId() {
|
|
62
|
+
return crypto.randomUUID();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Creates an IAM service instance.
|
|
66
|
+
*
|
|
67
|
+
* - If `url` is configured, enables outbound SCIM operations to external IAM
|
|
68
|
+
* - If `group_store` is configured, enables inbound SCIM operations from external IAM
|
|
69
|
+
*
|
|
70
|
+
* @param config - IAM configuration
|
|
71
|
+
* @param workload - Workload instance for authentication
|
|
72
|
+
* @returns IAM service instance
|
|
73
|
+
*/
|
|
74
|
+
export function iam(config, workload) {
|
|
75
|
+
const { url, group_store } = config;
|
|
76
|
+
/**
|
|
77
|
+
* Build headers for outgoing SCIM request using workload auth
|
|
78
|
+
*/
|
|
79
|
+
async function buildHeaders() {
|
|
80
|
+
const token = await workload.getToken();
|
|
81
|
+
return new Headers({
|
|
82
|
+
'Content-Type': SCIM_CONTENT_TYPE,
|
|
83
|
+
Accept: SCIM_CONTENT_TYPE,
|
|
84
|
+
Authorization: `Bearer ${token}`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Make an outgoing SCIM request to external IAM
|
|
89
|
+
*/
|
|
90
|
+
async function scimRequest(method, endpoint, body, validator) {
|
|
91
|
+
if (!url) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: {
|
|
95
|
+
schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
|
|
96
|
+
status: '500',
|
|
97
|
+
detail: 'IAM URL not configured for outgoing requests',
|
|
98
|
+
},
|
|
99
|
+
status: 500,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const requestUrl = `${url}${endpoint}`;
|
|
103
|
+
try {
|
|
104
|
+
const headers = await buildHeaders();
|
|
105
|
+
const response = await fetch(requestUrl, {
|
|
106
|
+
method,
|
|
107
|
+
headers,
|
|
108
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
109
|
+
});
|
|
110
|
+
const responseData = await response.json();
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: responseData,
|
|
115
|
+
status: response.status,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// Validate response if validator provided
|
|
119
|
+
if (validator) {
|
|
120
|
+
const validationResult = await validator['~standard'].validate(responseData);
|
|
121
|
+
if ('issues' in validationResult) {
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
error: {
|
|
125
|
+
schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
|
|
126
|
+
status: '400',
|
|
127
|
+
scimType: 'invalidValue',
|
|
128
|
+
detail: `Response validation failed: ${validationResult.issues?.map((i) => i.message).join('; ')}`,
|
|
129
|
+
},
|
|
130
|
+
status: 400,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
data: validationResult.value,
|
|
136
|
+
status: response.status,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
data: responseData,
|
|
142
|
+
status: response.status,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: {
|
|
149
|
+
schemas: ['urn:ietf:params:scim:api:messages:2.0:Error'],
|
|
150
|
+
status: '500',
|
|
151
|
+
detail: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
152
|
+
},
|
|
153
|
+
status: 500,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get the configured SCIM base URL
|
|
159
|
+
*/
|
|
160
|
+
function getBaseUrl() {
|
|
161
|
+
return url;
|
|
162
|
+
}
|
|
163
|
+
// Build groups_outbound if url is configured
|
|
164
|
+
let groups_outbound;
|
|
165
|
+
let createUser;
|
|
166
|
+
if (url) {
|
|
167
|
+
/**
|
|
168
|
+
* Create a new user/account in the external IAM provider
|
|
169
|
+
*/
|
|
170
|
+
createUser = async (user, options) => {
|
|
171
|
+
const userPayload = {
|
|
172
|
+
...user,
|
|
173
|
+
schemas: user.schemas ?? [
|
|
174
|
+
'urn:ietf:params:scim:schemas:core:2.0:User',
|
|
175
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User',
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
const validator = options?.validation ?? userSchema('es-iam');
|
|
179
|
+
return scimRequest('POST', '/Users', userPayload, validator);
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Create a new group in the external IAM provider
|
|
183
|
+
*/
|
|
184
|
+
async function createGroup(displayName, options) {
|
|
185
|
+
const groupPayload = {
|
|
186
|
+
schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'],
|
|
187
|
+
displayName,
|
|
188
|
+
externalId: options?.externalId,
|
|
189
|
+
members: options?.members,
|
|
190
|
+
};
|
|
191
|
+
const validator = options?.validation ?? groupResourceSchema('es-iam');
|
|
192
|
+
return scimRequest('POST', '/Groups', groupPayload, validator);
|
|
193
|
+
}
|
|
194
|
+
groups_outbound = {
|
|
195
|
+
createGroup,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// Build groups_inbound if group_store is configured
|
|
199
|
+
let groups_inbound;
|
|
200
|
+
if (group_store) {
|
|
201
|
+
// Capture reference to avoid undefined checks in nested functions
|
|
202
|
+
const store = group_store;
|
|
203
|
+
/**
|
|
204
|
+
* Validate authorization header for inbound requests
|
|
205
|
+
*/
|
|
206
|
+
const validateAuth = async (request) => {
|
|
207
|
+
const auth = request.headers.get('Authorization');
|
|
208
|
+
if (!auth || !auth.startsWith('Bearer ')) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
// Validate the token using workload
|
|
212
|
+
try {
|
|
213
|
+
const token = auth.substring(7);
|
|
214
|
+
const result = await workload.validateToken(token);
|
|
215
|
+
return result.valid;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Handle inbound SCIM requests for group management
|
|
223
|
+
*/
|
|
224
|
+
const handler = async (request, handlerConfig) => {
|
|
225
|
+
// Validate auth
|
|
226
|
+
const isAuthorized = await validateAuth(request);
|
|
227
|
+
if (!isAuthorized) {
|
|
228
|
+
return scimErrorResponse(401, 'Authorization required');
|
|
229
|
+
}
|
|
230
|
+
const urlObj = new URL(request.url);
|
|
231
|
+
const basePath = handlerConfig?.basePath ?? '/Groups';
|
|
232
|
+
let path = urlObj.pathname;
|
|
233
|
+
// Remove base path to get the resource path
|
|
234
|
+
if (path.startsWith(basePath)) {
|
|
235
|
+
path = path.substring(basePath.length);
|
|
236
|
+
}
|
|
237
|
+
// Parse group ID from path
|
|
238
|
+
const groupIdMatch = path.match(/^\/([^/]+)$/);
|
|
239
|
+
const groupId = groupIdMatch?.[1];
|
|
240
|
+
const method = request.method;
|
|
241
|
+
try {
|
|
242
|
+
// Route to appropriate handler
|
|
243
|
+
if (groupId) {
|
|
244
|
+
// Operations on specific group
|
|
245
|
+
switch (method) {
|
|
246
|
+
case 'GET':
|
|
247
|
+
return await handleGetGroup(groupId);
|
|
248
|
+
case 'PUT':
|
|
249
|
+
return await handleReplaceGroup(request, groupId);
|
|
250
|
+
case 'PATCH':
|
|
251
|
+
return await handlePatchGroup(request, groupId);
|
|
252
|
+
case 'DELETE':
|
|
253
|
+
return await handleDeleteGroup(groupId);
|
|
254
|
+
default:
|
|
255
|
+
return scimErrorResponse(405, 'Method not allowed');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (path === '' || path === '/') {
|
|
259
|
+
// Operations on groups collection
|
|
260
|
+
switch (method) {
|
|
261
|
+
case 'GET':
|
|
262
|
+
return await handleListGroups();
|
|
263
|
+
case 'POST':
|
|
264
|
+
return await handleCreateGroup(request);
|
|
265
|
+
default:
|
|
266
|
+
return scimErrorResponse(405, 'Method not allowed');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return scimErrorResponse(404, 'Resource not found');
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
console.error('Groups inbound handler error:', error);
|
|
273
|
+
return scimErrorResponse(500, error instanceof Error ? error.message : 'Internal server error');
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* List all groups
|
|
278
|
+
*/
|
|
279
|
+
const handleListGroups = async () => {
|
|
280
|
+
const groups = await store.list();
|
|
281
|
+
const resources = groups.map(storedGroupToResource);
|
|
282
|
+
return scimListResponse(resources);
|
|
283
|
+
};
|
|
284
|
+
/**
|
|
285
|
+
* Get a group by ID
|
|
286
|
+
*/
|
|
287
|
+
const handleGetGroup = async (id) => {
|
|
288
|
+
const group = await store.get(id);
|
|
289
|
+
if (!group) {
|
|
290
|
+
return scimErrorResponse(404, `Group ${id} not found`, 'invalidValue');
|
|
291
|
+
}
|
|
292
|
+
return scimResourceResponse(storedGroupToResource(group));
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* Create a new group
|
|
296
|
+
*/
|
|
297
|
+
const handleCreateGroup = async (request) => {
|
|
298
|
+
const body = (await request.json());
|
|
299
|
+
if (!body.displayName) {
|
|
300
|
+
return scimErrorResponse(400, 'displayName is required', 'invalidValue');
|
|
301
|
+
}
|
|
302
|
+
const now = new Date();
|
|
303
|
+
const storedGroup = {
|
|
304
|
+
id: generateId(),
|
|
305
|
+
displayName: body.displayName,
|
|
306
|
+
externalId: body.externalId,
|
|
307
|
+
members: body.members,
|
|
308
|
+
createdAt: now,
|
|
309
|
+
updatedAt: now,
|
|
310
|
+
};
|
|
311
|
+
await store.upsert(storedGroup);
|
|
312
|
+
return scimResourceResponse(storedGroupToResource(storedGroup), 201);
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Replace a group (PUT)
|
|
316
|
+
*/
|
|
317
|
+
const handleReplaceGroup = async (request, id) => {
|
|
318
|
+
const existing = await store.get(id);
|
|
319
|
+
if (!existing) {
|
|
320
|
+
return scimErrorResponse(404, `Group ${id} not found`, 'invalidValue');
|
|
321
|
+
}
|
|
322
|
+
const body = (await request.json());
|
|
323
|
+
const updatedGroup = {
|
|
324
|
+
...existing,
|
|
325
|
+
displayName: body.displayName ?? existing.displayName,
|
|
326
|
+
externalId: body.externalId,
|
|
327
|
+
members: body.members,
|
|
328
|
+
updatedAt: new Date(),
|
|
329
|
+
};
|
|
330
|
+
await store.upsert(updatedGroup);
|
|
331
|
+
return scimResourceResponse(storedGroupToResource(updatedGroup));
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* Patch a group (PATCH)
|
|
335
|
+
*/
|
|
336
|
+
const handlePatchGroup = async (request, id) => {
|
|
337
|
+
const existing = await store.get(id);
|
|
338
|
+
if (!existing) {
|
|
339
|
+
return scimErrorResponse(404, `Group ${id} not found`, 'invalidValue');
|
|
340
|
+
}
|
|
341
|
+
const body = (await request.json());
|
|
342
|
+
const operations = body.Operations ?? [];
|
|
343
|
+
const updated = { ...existing };
|
|
344
|
+
for (const op of operations) {
|
|
345
|
+
if (op.op === 'replace' && op.path && op.value !== undefined) {
|
|
346
|
+
if (op.path === 'displayName') {
|
|
347
|
+
updated.displayName = op.value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else if (op.op === 'add' && op.path && op.value !== undefined) {
|
|
351
|
+
if (op.path === 'members') {
|
|
352
|
+
const newMembers = op.value;
|
|
353
|
+
updated.members = [...(updated.members ?? []), ...newMembers];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else if (op.op === 'remove' && op.path) {
|
|
357
|
+
if (op.path.startsWith('members[')) {
|
|
358
|
+
// Parse member filter like members[value eq "user-id"]
|
|
359
|
+
const match = op.path.match(/members\[value eq "([^"]+)"\]/);
|
|
360
|
+
if (match) {
|
|
361
|
+
updated.members = (updated.members ?? []).filter((m) => m.value !== match[1]);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
updated.updatedAt = new Date();
|
|
367
|
+
await store.upsert(updated);
|
|
368
|
+
return scimResourceResponse(storedGroupToResource(updated));
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Delete a group
|
|
372
|
+
*/
|
|
373
|
+
const handleDeleteGroup = async (id) => {
|
|
374
|
+
const existing = await store.get(id);
|
|
375
|
+
if (!existing) {
|
|
376
|
+
return scimErrorResponse(404, `Group ${id} not found`, 'invalidValue');
|
|
377
|
+
}
|
|
378
|
+
await store.delete(id);
|
|
379
|
+
return new Response(null, { status: 204 });
|
|
380
|
+
};
|
|
381
|
+
groups_inbound = {
|
|
382
|
+
handler,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// Build users_inbound if user_store is configured
|
|
386
|
+
let users_inbound;
|
|
387
|
+
if (config.user_store) {
|
|
388
|
+
// Capture reference to avoid undefined checks in nested functions
|
|
389
|
+
const store = config.user_store;
|
|
390
|
+
/**
|
|
391
|
+
* Validate authorization header for inbound requests
|
|
392
|
+
*/
|
|
393
|
+
async function validateAuth(request) {
|
|
394
|
+
const auth = request.headers.get('Authorization');
|
|
395
|
+
if (!auth || !auth.startsWith('Bearer ')) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
// Validate the token using workload
|
|
399
|
+
try {
|
|
400
|
+
const token = auth.substring(7);
|
|
401
|
+
const result = await workload.validateToken(token);
|
|
402
|
+
return result.valid;
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Convert StoredUser to SCIM User for response
|
|
410
|
+
*/
|
|
411
|
+
const storedUserToScimUser = (storedUser) => {
|
|
412
|
+
return {
|
|
413
|
+
schemas: [
|
|
414
|
+
'urn:ietf:params:scim:schemas:core:2.0:User',
|
|
415
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User',
|
|
416
|
+
],
|
|
417
|
+
id: storedUser.id,
|
|
418
|
+
userName: storedUser.userName || storedUser.email || storedUser.id,
|
|
419
|
+
displayName: storedUser.name || storedUser.userName || storedUser.email,
|
|
420
|
+
name: storedUser.name
|
|
421
|
+
? {
|
|
422
|
+
givenName: storedUser.name.split(' ')[0],
|
|
423
|
+
familyName: storedUser.name.split(' ').slice(1).join(' ') || undefined,
|
|
424
|
+
}
|
|
425
|
+
: undefined,
|
|
426
|
+
emails: storedUser.email ? [{ value: storedUser.email, primary: true }] : [],
|
|
427
|
+
active: true,
|
|
428
|
+
meta: {
|
|
429
|
+
resourceType: 'User',
|
|
430
|
+
created: storedUser.createdAt.toISOString(),
|
|
431
|
+
lastModified: storedUser.updatedAt.toISOString(),
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
/**
|
|
436
|
+
* Convert SCIM User to StoredUser for storage
|
|
437
|
+
*/
|
|
438
|
+
const scimUserToStoredUser = (scimUser) => {
|
|
439
|
+
const now = new Date();
|
|
440
|
+
const primaryEmail = scimUser.emails?.find((e) => e.primary)?.value || scimUser.emails?.[0]?.value;
|
|
441
|
+
const name = scimUser.name
|
|
442
|
+
? `${scimUser.name.givenName || ''} ${scimUser.name.familyName || ''}`.trim()
|
|
443
|
+
: scimUser.displayName;
|
|
444
|
+
// Create minimal SSO data for IAM-provisioned users
|
|
445
|
+
const userId = scimUser.id || generateId();
|
|
446
|
+
const userName = scimUser.userName || primaryEmail || userId;
|
|
447
|
+
return {
|
|
448
|
+
id: userId,
|
|
449
|
+
userName,
|
|
450
|
+
name: name || scimUser.displayName || userName,
|
|
451
|
+
email: primaryEmail || userName,
|
|
452
|
+
avatarUrl: scimUser.profileUrl,
|
|
453
|
+
sso: {
|
|
454
|
+
profile: {
|
|
455
|
+
sub: userId,
|
|
456
|
+
iss: 'iam-provisioned',
|
|
457
|
+
aud: 'iam-provisioned',
|
|
458
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
459
|
+
iat: Math.floor(Date.now() / 1000),
|
|
460
|
+
email: primaryEmail || userName,
|
|
461
|
+
email_verified: true,
|
|
462
|
+
name: name || scimUser.displayName || userName,
|
|
463
|
+
preferred_username: userName,
|
|
464
|
+
},
|
|
465
|
+
tenant: {
|
|
466
|
+
id: 'iam-provisioned',
|
|
467
|
+
name: 'IAM Provisioned',
|
|
468
|
+
},
|
|
469
|
+
scope: 'openid profile email',
|
|
470
|
+
tokenType: 'Bearer',
|
|
471
|
+
expires: new Date(Date.now() + 3600 * 1000),
|
|
472
|
+
},
|
|
473
|
+
createdAt: scimUser.meta?.created ? new Date(scimUser.meta.created) : now,
|
|
474
|
+
updatedAt: scimUser.meta?.lastModified ? new Date(scimUser.meta.lastModified) : now,
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
/**
|
|
478
|
+
* Handle inbound SCIM requests for user management
|
|
479
|
+
*/
|
|
480
|
+
const handler = async (request, handlerConfig) => {
|
|
481
|
+
// Validate auth
|
|
482
|
+
const isAuthorized = await validateAuth(request);
|
|
483
|
+
if (!isAuthorized) {
|
|
484
|
+
return scimErrorResponse(401, 'Authorization required');
|
|
485
|
+
}
|
|
486
|
+
const urlObj = new URL(request.url);
|
|
487
|
+
const basePath = handlerConfig?.basePath ?? '/Users';
|
|
488
|
+
let path = urlObj.pathname;
|
|
489
|
+
// Remove base path to get the resource path
|
|
490
|
+
if (path.startsWith(basePath)) {
|
|
491
|
+
path = path.substring(basePath.length);
|
|
492
|
+
}
|
|
493
|
+
// Parse user ID from path
|
|
494
|
+
const userIdMatch = path.match(/^\/([^/]+)$/);
|
|
495
|
+
const userId = userIdMatch?.[1];
|
|
496
|
+
const method = request.method;
|
|
497
|
+
try {
|
|
498
|
+
// Route to appropriate handler
|
|
499
|
+
if (userId) {
|
|
500
|
+
// Operations on specific user
|
|
501
|
+
switch (method) {
|
|
502
|
+
case 'GET':
|
|
503
|
+
return await handleGetUser(userId);
|
|
504
|
+
case 'PUT':
|
|
505
|
+
return await handleReplaceUser(request, userId);
|
|
506
|
+
case 'PATCH':
|
|
507
|
+
return await handlePatchUser(request, userId);
|
|
508
|
+
case 'DELETE':
|
|
509
|
+
return await handleDeleteUser(userId);
|
|
510
|
+
default:
|
|
511
|
+
return scimErrorResponse(405, 'Method not allowed');
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
else if (path === '' || path === '/') {
|
|
515
|
+
// Operations on users collection
|
|
516
|
+
switch (method) {
|
|
517
|
+
case 'GET':
|
|
518
|
+
return await handleListUsers();
|
|
519
|
+
case 'POST':
|
|
520
|
+
return await handleCreateUser(request);
|
|
521
|
+
default:
|
|
522
|
+
return scimErrorResponse(405, 'Method not allowed');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return scimErrorResponse(404, 'Resource not found');
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
console.error('Users inbound handler error:', error);
|
|
529
|
+
return scimErrorResponse(500, error instanceof Error ? error.message : 'Internal server error');
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
/**
|
|
533
|
+
* List all users
|
|
534
|
+
*/
|
|
535
|
+
const handleListUsers = async () => {
|
|
536
|
+
// Note: UserStore doesn't have a list method, so we'd need to implement it
|
|
537
|
+
// For now, return empty list - this would need to be enhanced based on UserStore interface
|
|
538
|
+
return scimListResponse([]);
|
|
539
|
+
};
|
|
540
|
+
/**
|
|
541
|
+
* Get a user by ID
|
|
542
|
+
*/
|
|
543
|
+
const handleGetUser = async (id) => {
|
|
544
|
+
const user = await store.get(id);
|
|
545
|
+
if (!user) {
|
|
546
|
+
return scimErrorResponse(404, `User ${id} not found`, 'invalidValue');
|
|
547
|
+
}
|
|
548
|
+
return scimResourceResponse(storedUserToScimUser(user));
|
|
549
|
+
};
|
|
550
|
+
/**
|
|
551
|
+
* Create a new user
|
|
552
|
+
*/
|
|
553
|
+
const handleCreateUser = async (request) => {
|
|
554
|
+
const body = (await request.json());
|
|
555
|
+
if (!body.userName && !body.emails?.[0]?.value) {
|
|
556
|
+
return scimErrorResponse(400, 'userName or email is required', 'invalidValue');
|
|
557
|
+
}
|
|
558
|
+
const storedUser = scimUserToStoredUser(body);
|
|
559
|
+
await store.upsert(storedUser);
|
|
560
|
+
return scimResourceResponse(storedUserToScimUser(storedUser), 201);
|
|
561
|
+
};
|
|
562
|
+
/**
|
|
563
|
+
* Replace a user (PUT)
|
|
564
|
+
*/
|
|
565
|
+
const handleReplaceUser = async (request, id) => {
|
|
566
|
+
const existing = await store.get(id);
|
|
567
|
+
if (!existing) {
|
|
568
|
+
return scimErrorResponse(404, `User ${id} not found`, 'invalidValue');
|
|
569
|
+
}
|
|
570
|
+
const body = (await request.json());
|
|
571
|
+
const updatedUser = scimUserToStoredUser({ ...body, id });
|
|
572
|
+
updatedUser.createdAt = existing.createdAt;
|
|
573
|
+
updatedUser.updatedAt = new Date();
|
|
574
|
+
await store.upsert(updatedUser);
|
|
575
|
+
return scimResourceResponse(storedUserToScimUser(updatedUser));
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* Patch a user (PATCH)
|
|
579
|
+
*/
|
|
580
|
+
const handlePatchUser = async (request, id) => {
|
|
581
|
+
const existing = await store.get(id);
|
|
582
|
+
if (!existing) {
|
|
583
|
+
return scimErrorResponse(404, `User ${id} not found`, 'invalidValue');
|
|
584
|
+
}
|
|
585
|
+
const body = (await request.json());
|
|
586
|
+
const operations = body.Operations ?? [];
|
|
587
|
+
const updated = { ...existing };
|
|
588
|
+
for (const op of operations) {
|
|
589
|
+
if (op.op === 'replace' && op.path && op.value !== undefined) {
|
|
590
|
+
if (op.path === 'displayName') {
|
|
591
|
+
updated.name = op.value;
|
|
592
|
+
}
|
|
593
|
+
else if (op.path === 'userName') {
|
|
594
|
+
updated.userName = op.value;
|
|
595
|
+
}
|
|
596
|
+
else if (op.path.startsWith('name.')) {
|
|
597
|
+
const namePart = op.path.split('.')[1];
|
|
598
|
+
if (!updated.name)
|
|
599
|
+
updated.name = '';
|
|
600
|
+
// Simple name handling - in production you'd want more sophisticated parsing
|
|
601
|
+
if (namePart === 'givenName') {
|
|
602
|
+
updated.name = `${op.value} ${updated.name.split(' ').slice(1).join(' ')}`.trim();
|
|
603
|
+
}
|
|
604
|
+
else if (namePart === 'familyName') {
|
|
605
|
+
updated.name = `${updated.name.split(' ')[0]} ${op.value}`.trim();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else if (op.path === 'emails') {
|
|
609
|
+
// Note: StoredUser doesn't have emails array, only email string
|
|
610
|
+
// We'll extract the primary email and update the email field
|
|
611
|
+
const emails = op.value;
|
|
612
|
+
const primaryEmail = emails?.find((e) => e.primary)?.value || emails?.[0]?.value;
|
|
613
|
+
if (primaryEmail)
|
|
614
|
+
updated.email = primaryEmail;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else if (op.op === 'add' && op.path && op.value !== undefined) {
|
|
618
|
+
if (op.path === 'emails') {
|
|
619
|
+
// Note: StoredUser doesn't have emails array, only email string
|
|
620
|
+
// We'll extract the primary email and update the email field
|
|
621
|
+
const newEmails = op.value;
|
|
622
|
+
const primaryEmail = newEmails?.find((e) => e.primary)?.value || newEmails?.[0]?.value;
|
|
623
|
+
if (primaryEmail)
|
|
624
|
+
updated.email = primaryEmail;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
else if (op.op === 'remove' && op.path) {
|
|
628
|
+
if (op.path === 'displayName') {
|
|
629
|
+
updated.name = '';
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
updated.updatedAt = new Date();
|
|
634
|
+
await store.upsert(updated);
|
|
635
|
+
return scimResourceResponse(storedUserToScimUser(updated));
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* Delete a user
|
|
639
|
+
*/
|
|
640
|
+
const handleDeleteUser = async (id) => {
|
|
641
|
+
const existing = await store.get(id);
|
|
642
|
+
if (!existing) {
|
|
643
|
+
return scimErrorResponse(404, `User ${id} not found`, 'invalidValue');
|
|
644
|
+
}
|
|
645
|
+
await store.delete(id);
|
|
646
|
+
return new Response(null, { status: 204 });
|
|
647
|
+
};
|
|
648
|
+
users_inbound = {
|
|
649
|
+
handler,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Top-level handler that routes to users_inbound or groups_inbound based on path
|
|
654
|
+
*/
|
|
655
|
+
async function topLevelHandler(request, handlerConfig) {
|
|
656
|
+
const urlObj = new URL(request.url);
|
|
657
|
+
const path = urlObj.pathname;
|
|
658
|
+
const usersUrl = handlerConfig?.usersUrl ?? config.usersUrl ?? '/api/iam/Users';
|
|
659
|
+
const groupsUrl = handlerConfig?.groupsUrl ?? config.groupsUrl ?? '/api/iam/Groups';
|
|
660
|
+
// Route to users handler if path matches usersUrl
|
|
661
|
+
if (path.startsWith(usersUrl) && users_inbound) {
|
|
662
|
+
return users_inbound.handler(request, { basePath: usersUrl });
|
|
663
|
+
}
|
|
664
|
+
// Route to groups handler if path matches groupsUrl
|
|
665
|
+
if (path.startsWith(groupsUrl) && groups_inbound) {
|
|
666
|
+
return groups_inbound.handler(request, { basePath: groupsUrl });
|
|
667
|
+
}
|
|
668
|
+
// If neither matches, return 404
|
|
669
|
+
return scimErrorResponse(404, 'Resource not found');
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
...config,
|
|
673
|
+
createUser,
|
|
674
|
+
getBaseUrl,
|
|
675
|
+
groups_outbound,
|
|
676
|
+
groups_inbound,
|
|
677
|
+
users_inbound,
|
|
678
|
+
handler: topLevelHandler,
|
|
679
|
+
};
|
|
680
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,11 +24,11 @@ export type { GroupStore, StoredGroup } from './group-store';
|
|
|
24
24
|
export { InMemoryGroupStore } from './group-store';
|
|
25
25
|
export type { CreateGroupOptions, CreateUserOptions, GroupsInboundHandlerConfig, IAM, IAMConfig, IAMGroupsInbound, IAMGroupsOutbound, IAMHandlerConfig, IAMUsersInbound, ScimError, ScimListResponse, ScimResult, UsersInboundHandlerConfig, } from './iam';
|
|
26
26
|
export { iam } from './iam';
|
|
27
|
-
export * from './sso-server';
|
|
28
27
|
export type { SessionStore } from './session-store';
|
|
29
28
|
export { InMemorySessionStore } from './session-store';
|
|
30
29
|
export type { SSO, SSOConfig, SSOHandlerConfig } from './sso';
|
|
31
30
|
export { sso } from './sso';
|
|
31
|
+
export * from './sso-server';
|
|
32
32
|
export type { CreateTenantRequest, CreateTenantResponse, EnvironmentType, StoredTenant, TenantStatus, TenantStore, TenantWebhookPayload, } from './tenant';
|
|
33
33
|
export { InMemoryTenantStore, parseTenantRequest, sendTenantWebhook, serializeESConfig, TenantRequestError, } from './tenant';
|
|
34
34
|
export type { BaseUser } from './types/base-user';
|