@sasjs/api 0.6.1 → 0.7.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/README.md +3 -2
- package/build/browser.js +2218 -118
- package/build/cjs/corpus/records.js +1222 -0
- package/build/cjs/corpus/runner.js +15 -6
- package/build/cjs/host/dispatch.js +190 -2
- package/build/cjs/identity/memory-store.js +196 -11
- package/build/cjs/identity/records.js +398 -0
- package/build/cjs/index.js +8 -1
- package/build/cjs/routes/generated.js +184 -4
- package/build/cjs/routes/overlay.js +53 -58
- package/build/esm/corpus/records.d.ts +25 -0
- package/build/esm/corpus/records.d.ts.map +1 -0
- package/build/esm/corpus/records.js +1219 -0
- package/build/esm/corpus/runner.d.ts.map +1 -1
- package/build/esm/corpus/runner.js +15 -6
- package/build/esm/host/dispatch.d.ts +9 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +190 -2
- package/build/esm/identity/fixture.d.ts +6 -0
- package/build/esm/identity/fixture.d.ts.map +1 -1
- package/build/esm/identity/memory-store.d.ts +29 -5
- package/build/esm/identity/memory-store.d.ts.map +1 -1
- package/build/esm/identity/memory-store.js +196 -11
- package/build/esm/identity/records.d.ts +102 -0
- package/build/esm/identity/records.d.ts.map +1 -0
- package/build/esm/identity/records.js +393 -0
- package/build/esm/identity/types.d.ts +102 -2
- package/build/esm/identity/types.d.ts.map +1 -1
- package/build/esm/index.d.ts +4 -1
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +2 -0
- package/build/esm/routes/generated.d.ts.map +1 -1
- package/build/esm/routes/generated.js +184 -4
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +53 -58
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/corpus/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/corpus/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AASnD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;IAC9B,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,UAAU,UAAU,KAAG,QAkBlD,CAAA;AAkBD;;;;GAIG;AACH,eAAO,MAAM,OAAO,GAClB,UAAU,UAAU,EACpB,WAAU,QAAgC,KACzC,OAAO,CAAC,UAAU,CAepB,CAAA"}
|
|
@@ -2,6 +2,7 @@ import { dispatch } from '../host/dispatch.js';
|
|
|
2
2
|
import { createDriveService } from '../drive/service.js';
|
|
3
3
|
import { MemoryDriveStore } from '../drive/memory-store.js';
|
|
4
4
|
import { createIdentityService } from '../identity/service.js';
|
|
5
|
+
import { createRecordService } from '../identity/records.js';
|
|
5
6
|
import { MemoryIdentityStore } from '../identity/memory-store.js';
|
|
6
7
|
import { DEFAULT_IDENTITY } from '../identity/fixture.js';
|
|
7
8
|
import { isApiError } from '../types.js';
|
|
@@ -11,12 +12,20 @@ import { isApiError } from '../types.js';
|
|
|
11
12
|
* The identity store is built for every case, drive or not, so a case that starts
|
|
12
13
|
* mid-flow can declare the tokens and codes it begins with.
|
|
13
14
|
*/
|
|
14
|
-
export const servicesFor = (testCase) =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
export const servicesFor = (testCase) => {
|
|
16
|
+
// one identity store, shared by both services: a token the sign-in flow issues
|
|
17
|
+
// has to be the token a record route reads, and a user a record route creates
|
|
18
|
+
// has to be one the flow can sign in. Two stores built from the same fixture
|
|
19
|
+
// would let a case assert across two independent states and still pass.
|
|
20
|
+
const identity = new MemoryIdentityStore(testCase.identity || DEFAULT_IDENTITY);
|
|
21
|
+
return {
|
|
22
|
+
drive: createDriveService(testCase.unwritable
|
|
23
|
+
? new RefusingDriveStore(testCase.seed || {})
|
|
24
|
+
: new MemoryDriveStore(testCase.seed || {})),
|
|
25
|
+
identity: createIdentityService(identity),
|
|
26
|
+
records: createRecordService(identity)
|
|
27
|
+
};
|
|
28
|
+
};
|
|
20
29
|
/**
|
|
21
30
|
* A drive that reads but refuses every write.
|
|
22
31
|
*
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { DriveService } from '../drive/service.js';
|
|
2
2
|
import type { IdentityService } from '../identity/service.js';
|
|
3
|
+
import type { RecordService } from '../identity/records.js';
|
|
3
4
|
import type { Subject } from '../identity/types.js';
|
|
4
5
|
export interface ApiRequest {
|
|
5
6
|
query?: Record<string, string | undefined>;
|
|
6
7
|
body?: Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* The values the route's path parameters carried, by name - `{ uid: 'abc' }`
|
|
10
|
+
* for `/user/{uid}`. A host extracts them from its own router; the corpus
|
|
11
|
+
* states them.
|
|
12
|
+
*/
|
|
13
|
+
params?: Record<string, string>;
|
|
7
14
|
/** The multipart payload: the API names it `file` on every route that takes one. */
|
|
8
15
|
file?: {
|
|
9
16
|
name: string;
|
|
@@ -33,6 +40,8 @@ export interface Services {
|
|
|
33
40
|
* implement - the difference is the message, and it names the capability.
|
|
34
41
|
*/
|
|
35
42
|
identity?: IdentityService;
|
|
43
|
+
/** Users, groups, permission rules and clients. Absent, the record routes answer 501. */
|
|
44
|
+
records?: RecordService;
|
|
36
45
|
}
|
|
37
46
|
export declare const dispatch: (services: Services, routeId: string, request?: ApiRequest) => Promise<ApiResponse>;
|
|
38
47
|
/** Whether a route is one `dispatch` can answer today. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAOnD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,yFAAyF;IACzF,OAAO,CAAC,EAAE,aAAa,CAAA;CACxB;AAsCD,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,EAClB,SAAS,MAAM,EACf,UAAS,UAAe,KACvB,OAAO,CAAC,WAAW,CA+VrB,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,cAAc,EAAE,MAAM,EAwClC,CAAA"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { routeById } from '../routes/index.js';
|
|
2
2
|
import { ApiError } from '../types.js';
|
|
3
3
|
import { DEPLOY_SUCCESS } from '../drive/deploy.js';
|
|
4
|
+
/** The defaults sasjs/server's Client schema applies to an omitted lifetime. */
|
|
5
|
+
const SECONDS_IN_A_DAY = 86400;
|
|
4
6
|
/**
|
|
5
7
|
* The parameters the table marks required, checked before the handler runs.
|
|
6
8
|
*
|
|
@@ -39,10 +41,36 @@ export const dispatch = async (services, routeId, request = {}) => {
|
|
|
39
41
|
const route = routeById(routeId);
|
|
40
42
|
if (!route)
|
|
41
43
|
throw new ApiError(404, `No such operation: ${routeId}`);
|
|
44
|
+
const { drive, identity, records } = services;
|
|
45
|
+
// The server authenticates before its payload schema runs, so an anonymous
|
|
46
|
+
// request with an incomplete body is a 401 there rather than a 400. The order
|
|
47
|
+
// is only observable on the route groups that gate on a caller: the sign-in
|
|
48
|
+
// flow has no caller by definition, and the drive has no identity at all.
|
|
49
|
+
const identityScoped = ['User', 'Group', 'Permission', 'Client'].includes(route.tag) ||
|
|
50
|
+
routeId === 'PATCH /SASjsApi/auth/updatePassword';
|
|
51
|
+
if (identityScoped && records) {
|
|
52
|
+
const subject = request.subject ??
|
|
53
|
+
(identity ? await identity.authenticate(request.token) : undefined);
|
|
54
|
+
if (!subject) {
|
|
55
|
+
// A host may serve records without a sign-in flow - it resolves the caller
|
|
56
|
+
// itself and passes `subject`. Without one it cannot authenticate at all,
|
|
57
|
+
// and a 401 would send every request round the login loop for ever, so it
|
|
58
|
+
// is told which capability it is missing instead.
|
|
59
|
+
if (!identity)
|
|
60
|
+
throw new ApiError(501, `This host has no identity service, so it cannot answer ${routeId}.`);
|
|
61
|
+
throw new ApiError(401, 'Unauthorized');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
42
64
|
validate(route, request);
|
|
43
|
-
const { drive, identity } = services;
|
|
44
65
|
const query = request.query || {};
|
|
45
66
|
const body = request.body || {};
|
|
67
|
+
const params = request.params || {};
|
|
68
|
+
/** The record service, or the 501 a host without one answers. */
|
|
69
|
+
const required = () => {
|
|
70
|
+
if (!records)
|
|
71
|
+
throw new ApiError(501, `This host has no record service, so it cannot answer ${routeId}.`);
|
|
72
|
+
return records;
|
|
73
|
+
};
|
|
46
74
|
/** The caller, from the request's own subject or the token it carried. */
|
|
47
75
|
const caller = async () => {
|
|
48
76
|
if (request.subject)
|
|
@@ -135,6 +163,144 @@ export const dispatch = async (services, routeId, request = {}) => {
|
|
|
135
163
|
}
|
|
136
164
|
case 'GET /SASjsApi/drive/filetree':
|
|
137
165
|
return { status: 200, body: await drive.tree(query._folderPath) };
|
|
166
|
+
// ------------------------------------------------------------- records
|
|
167
|
+
//
|
|
168
|
+
// The administration routes: users, groups, permission rules, clients and the
|
|
169
|
+
// caller's own password. Each handler resolves the caller and passes it on;
|
|
170
|
+
// the admin gate and the wording live in the service.
|
|
171
|
+
case 'GET /SASjsApi/user':
|
|
172
|
+
return { status: 200, body: await required().listUsers(await caller()) };
|
|
173
|
+
case 'GET /SASjsApi/user/{uid}':
|
|
174
|
+
return {
|
|
175
|
+
status: 200,
|
|
176
|
+
body: await required().getUser(await caller(), {
|
|
177
|
+
uid: String(params.uid ?? '')
|
|
178
|
+
})
|
|
179
|
+
};
|
|
180
|
+
case 'GET /SASjsApi/user/by/username/{username}':
|
|
181
|
+
return {
|
|
182
|
+
status: 200,
|
|
183
|
+
body: await required().getUser(await caller(), {
|
|
184
|
+
username: String(params.username ?? '')
|
|
185
|
+
})
|
|
186
|
+
};
|
|
187
|
+
case 'POST /SASjsApi/user':
|
|
188
|
+
return {
|
|
189
|
+
status: 200,
|
|
190
|
+
body: await required().createUser(await caller(), {
|
|
191
|
+
username: String(body.username ?? ''),
|
|
192
|
+
displayName: String(body.displayName ?? ''),
|
|
193
|
+
password: String(body.password ?? ''),
|
|
194
|
+
isAdmin: body.isAdmin,
|
|
195
|
+
isActive: body.isActive,
|
|
196
|
+
autoExec: body.autoExec
|
|
197
|
+
})
|
|
198
|
+
};
|
|
199
|
+
case 'PATCH /SASjsApi/user/{uid}':
|
|
200
|
+
return {
|
|
201
|
+
status: 200,
|
|
202
|
+
body: await required().updateUser(await caller(), { uid: String(params.uid ?? '') }, body)
|
|
203
|
+
};
|
|
204
|
+
case 'PATCH /SASjsApi/user/by/username/{username}':
|
|
205
|
+
return {
|
|
206
|
+
status: 200,
|
|
207
|
+
body: await required().updateUser(await caller(), { username: String(params.username ?? '') }, body)
|
|
208
|
+
};
|
|
209
|
+
case 'DELETE /SASjsApi/user/{uid}':
|
|
210
|
+
await required().deleteUser(await caller(), { uid: String(params.uid ?? '') }, { password: body.password });
|
|
211
|
+
return { status: 204 };
|
|
212
|
+
case 'DELETE /SASjsApi/user/by/username/{username}':
|
|
213
|
+
await required().deleteUser(await caller(), { username: String(params.username ?? '') }, { password: body.password });
|
|
214
|
+
return { status: 204 };
|
|
215
|
+
case 'GET /SASjsApi/group':
|
|
216
|
+
return { status: 200, body: await required().listGroups(await caller()) };
|
|
217
|
+
case 'GET /SASjsApi/group/{uid}':
|
|
218
|
+
return {
|
|
219
|
+
status: 200,
|
|
220
|
+
body: await required().getGroup(await caller(), {
|
|
221
|
+
uid: String(params.uid ?? '')
|
|
222
|
+
})
|
|
223
|
+
};
|
|
224
|
+
case 'GET /SASjsApi/group/by/groupname/{name}':
|
|
225
|
+
return {
|
|
226
|
+
status: 200,
|
|
227
|
+
body: await required().getGroup(await caller(), {
|
|
228
|
+
name: String(params.name ?? '')
|
|
229
|
+
})
|
|
230
|
+
};
|
|
231
|
+
case 'POST /SASjsApi/group':
|
|
232
|
+
return {
|
|
233
|
+
status: 200,
|
|
234
|
+
body: await required().createGroup(await caller(), {
|
|
235
|
+
name: String(body.name ?? ''),
|
|
236
|
+
description: String(body.description ?? ''),
|
|
237
|
+
isActive: body.isActive
|
|
238
|
+
})
|
|
239
|
+
};
|
|
240
|
+
case 'POST /SASjsApi/group/{groupUid}/{userUid}':
|
|
241
|
+
return {
|
|
242
|
+
status: 200,
|
|
243
|
+
body: await required().addUserToGroup(await caller(), String(params.groupUid ?? ''), String(params.userUid ?? ''))
|
|
244
|
+
};
|
|
245
|
+
case 'DELETE /SASjsApi/group/{groupUid}/{userUid}':
|
|
246
|
+
return {
|
|
247
|
+
status: 200,
|
|
248
|
+
body: await required().removeUserFromGroup(await caller(), String(params.groupUid ?? ''), String(params.userUid ?? ''))
|
|
249
|
+
};
|
|
250
|
+
case 'DELETE /SASjsApi/group/{uid}':
|
|
251
|
+
// the document states 200 for this one - the server answers with the
|
|
252
|
+
// driver's delete result rather than nothing
|
|
253
|
+
await required().deleteGroup(await caller(), String(params.uid ?? ''));
|
|
254
|
+
return { status: 200 };
|
|
255
|
+
case 'GET /SASjsApi/permission':
|
|
256
|
+
return {
|
|
257
|
+
status: 200,
|
|
258
|
+
body: await required().listPermissions(await caller())
|
|
259
|
+
};
|
|
260
|
+
case 'POST /SASjsApi/permission':
|
|
261
|
+
return {
|
|
262
|
+
status: 200,
|
|
263
|
+
body: await required().createPermission(await caller(), {
|
|
264
|
+
path: String(body.path ?? ''),
|
|
265
|
+
type: String(body.type ?? ''),
|
|
266
|
+
setting: String(body.setting ?? ''),
|
|
267
|
+
principalType: String(body.principalType ?? ''),
|
|
268
|
+
principalId: String(body.principalId ?? '')
|
|
269
|
+
})
|
|
270
|
+
};
|
|
271
|
+
case 'PATCH /SASjsApi/permission/{uid}':
|
|
272
|
+
return {
|
|
273
|
+
status: 200,
|
|
274
|
+
body: await required().updatePermission(await caller(), String(params.uid ?? ''), { setting: String(body.setting ?? '') })
|
|
275
|
+
};
|
|
276
|
+
case 'DELETE /SASjsApi/permission/{uid}':
|
|
277
|
+
await required().deletePermission(await caller(), String(params.uid ?? ''));
|
|
278
|
+
return { status: 204 };
|
|
279
|
+
case 'GET /SASjsApi/client':
|
|
280
|
+
return { status: 200, body: await required().listClients(await caller()) };
|
|
281
|
+
case 'POST /SASjsApi/client':
|
|
282
|
+
return {
|
|
283
|
+
status: 200,
|
|
284
|
+
// An omitted lifetime is the schema's default, not NaN: the server's
|
|
285
|
+
// Client model declares 86400 and 2592000, and a NaN lifetime would make
|
|
286
|
+
// every token the client buys expire on arrival.
|
|
287
|
+
body: await required().createClient(await caller(), {
|
|
288
|
+
clientId: String(body.clientId ?? ''),
|
|
289
|
+
clientSecret: String(body.clientSecret ?? ''),
|
|
290
|
+
accessTokenExpiration: body.accessTokenExpiration === undefined
|
|
291
|
+
? SECONDS_IN_A_DAY
|
|
292
|
+
: Number(body.accessTokenExpiration),
|
|
293
|
+
refreshTokenExpiration: body.refreshTokenExpiration === undefined
|
|
294
|
+
? SECONDS_IN_A_DAY * 30
|
|
295
|
+
: Number(body.refreshTokenExpiration)
|
|
296
|
+
})
|
|
297
|
+
};
|
|
298
|
+
case 'PATCH /SASjsApi/auth/updatePassword':
|
|
299
|
+
await required().updatePassword(await caller(), {
|
|
300
|
+
currentPassword: String(body.currentPassword ?? ''),
|
|
301
|
+
newPassword: String(body.newPassword ?? '')
|
|
302
|
+
});
|
|
303
|
+
return { status: 204 };
|
|
138
304
|
default: {
|
|
139
305
|
// The reason the table carries travels with the refusal: a host developer
|
|
140
306
|
// reading a 501 wants to know what the operation waits on, not only that
|
|
@@ -162,5 +328,27 @@ export const HANDLED_ROUTES = [
|
|
|
162
328
|
'POST /SASLogon/login',
|
|
163
329
|
'POST /SASLogon/authorize',
|
|
164
330
|
'GET /SASLogon/logout',
|
|
165
|
-
'GET /SASjsApi/session'
|
|
331
|
+
'GET /SASjsApi/session',
|
|
332
|
+
'GET /SASjsApi/user',
|
|
333
|
+
'GET /SASjsApi/user/{uid}',
|
|
334
|
+
'GET /SASjsApi/user/by/username/{username}',
|
|
335
|
+
'POST /SASjsApi/user',
|
|
336
|
+
'PATCH /SASjsApi/user/{uid}',
|
|
337
|
+
'PATCH /SASjsApi/user/by/username/{username}',
|
|
338
|
+
'DELETE /SASjsApi/user/{uid}',
|
|
339
|
+
'DELETE /SASjsApi/user/by/username/{username}',
|
|
340
|
+
'GET /SASjsApi/group',
|
|
341
|
+
'GET /SASjsApi/group/{uid}',
|
|
342
|
+
'GET /SASjsApi/group/by/groupname/{name}',
|
|
343
|
+
'POST /SASjsApi/group',
|
|
344
|
+
'POST /SASjsApi/group/{groupUid}/{userUid}',
|
|
345
|
+
'DELETE /SASjsApi/group/{groupUid}/{userUid}',
|
|
346
|
+
'DELETE /SASjsApi/group/{uid}',
|
|
347
|
+
'GET /SASjsApi/permission',
|
|
348
|
+
'POST /SASjsApi/permission',
|
|
349
|
+
'PATCH /SASjsApi/permission/{uid}',
|
|
350
|
+
'DELETE /SASjsApi/permission/{uid}',
|
|
351
|
+
'GET /SASjsApi/client',
|
|
352
|
+
'POST /SASjsApi/client',
|
|
353
|
+
'PATCH /SASjsApi/auth/updatePassword'
|
|
166
354
|
];
|
|
@@ -19,6 +19,10 @@ export interface IdentityFixtureUser {
|
|
|
19
19
|
isAdmin: boolean;
|
|
20
20
|
isActive: boolean;
|
|
21
21
|
needsToUpdatePassword: boolean;
|
|
22
|
+
/** Set when the account is managed by an external auth provider. */
|
|
23
|
+
authProvider?: string;
|
|
24
|
+
/** The account's own auto-exec code, where the fixture declares one. */
|
|
25
|
+
autoExec?: string;
|
|
22
26
|
}
|
|
23
27
|
export interface IdentityFixtureGroup {
|
|
24
28
|
uid: string;
|
|
@@ -27,6 +31,8 @@ export interface IdentityFixtureGroup {
|
|
|
27
31
|
isActive: boolean;
|
|
28
32
|
/** Usernames of the members. */
|
|
29
33
|
users: string[];
|
|
34
|
+
/** Set when the group is managed by an external auth provider. */
|
|
35
|
+
authProvider?: string;
|
|
30
36
|
}
|
|
31
37
|
export interface IdentityFixturePermission {
|
|
32
38
|
uid: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../../../src/identity/fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,eAAe,IAAI,CAAA;AAEhC,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,qBAAqB,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../../../src/identity/fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,eAAe,IAAI,CAAA;AAEhC,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,qBAAqB,EAAE,OAAO,CAAA;IAC9B,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,OAAO,CAAA;IACjB,gCAAgC;IAChC,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,qCAAqC;IACrC,qBAAqB,EAAE,MAAM,CAAA;IAC7B,qCAAqC;IACrC,sBAAsB,EAAE,MAAM,CAAA;CAC/B;AAED,wFAAwF;AACxF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,2EAA2E;AAC3E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,mBAAmB,EAAE,CAAA;IAC5B,MAAM,EAAE,oBAAoB,EAAE,CAAA;IAC9B,WAAW,EAAE,yBAAyB,EAAE,CAAA;IACxC,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,sDAAsD;IACtD,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAA;IAC/B,mEAAmE;IACnE,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAA;CAC9B;AAID,eAAO,MAAM,gBAAgB,EAAE,eA6E9B,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,OAAO,KAAG,eA2B5C,CAAA"}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* resolving. A host with real credentials - a server - supplies its own store
|
|
9
9
|
* with JWTs, bcrypt and a database behind it.
|
|
10
10
|
*/
|
|
11
|
-
import type { IdentityClient, IdentityStore, IdentityUser, Subject, TokenPair } from './types.js';
|
|
11
|
+
import type { IdentityClient, IdentityClientRecord, IdentityGroupRecord, IdentityPermissionRecord, IdentityStore, IdentityUser, IdentityUserRecord, NewGroup, NewPermission, NewUser, Subject, TokenPair, UserChange } from './types.js';
|
|
12
12
|
import { type IdentityFixture } from './fixture.js';
|
|
13
13
|
/**
|
|
14
14
|
* Whether a token minted for one subject names the same user and client.
|
|
@@ -17,17 +17,20 @@ import { type IdentityFixture } from './fixture.js';
|
|
|
17
17
|
* the read paths compare rather than trust the reference.
|
|
18
18
|
*/
|
|
19
19
|
export declare class MemoryIdentityStore implements IdentityStore {
|
|
20
|
-
private readonly
|
|
20
|
+
private readonly userList;
|
|
21
21
|
private readonly passwords;
|
|
22
|
-
private readonly
|
|
22
|
+
private readonly groupList;
|
|
23
|
+
private readonly permissionList;
|
|
24
|
+
private readonly clientRecords;
|
|
25
|
+
private readonly clientMap;
|
|
23
26
|
private readonly pairs;
|
|
24
27
|
private readonly access;
|
|
25
28
|
private readonly refresh;
|
|
26
29
|
private readonly codes;
|
|
27
30
|
private readonly claimed;
|
|
28
31
|
constructor(fixture?: IdentityFixture);
|
|
29
|
-
userByName(username: string): Promise<
|
|
30
|
-
userById(uid: string): Promise<
|
|
32
|
+
userByName(username: string): Promise<IdentityUserRecord | undefined>;
|
|
33
|
+
userById(uid: string): Promise<IdentityUserRecord | undefined>;
|
|
31
34
|
/** A fixture holds the password in clear, so the check is an equality. */
|
|
32
35
|
passwordMatches(user: IdentityUser, password: string): Promise<boolean>;
|
|
33
36
|
client(clientId: string): Promise<IdentityClient | undefined>;
|
|
@@ -57,5 +60,26 @@ export declare class MemoryIdentityStore implements IdentityStore {
|
|
|
57
60
|
issueCode(subject: Subject): Promise<string>;
|
|
58
61
|
readCode(code: string): Promise<Subject | undefined>;
|
|
59
62
|
claimCode(code: string): Promise<boolean>;
|
|
63
|
+
users(): Promise<IdentityUserRecord[]>;
|
|
64
|
+
/** A user as the administration routes report one: groups derived, not stored. */
|
|
65
|
+
private recordOf;
|
|
66
|
+
createUser(input: NewUser): Promise<IdentityUserRecord>;
|
|
67
|
+
updateUser(uid: string, change: UserChange): Promise<IdentityUserRecord | undefined>;
|
|
68
|
+
deleteUser(uid: string): Promise<void>;
|
|
69
|
+
setPassword(uid: string, password: string): Promise<void>;
|
|
70
|
+
groups(): Promise<IdentityGroupRecord[]>;
|
|
71
|
+
groupById(uid: string): Promise<IdentityGroupRecord | undefined>;
|
|
72
|
+
groupByName(name: string): Promise<IdentityGroupRecord | undefined>;
|
|
73
|
+
createGroup(input: NewGroup): Promise<IdentityGroupRecord>;
|
|
74
|
+
deleteGroup(uid: string): Promise<void>;
|
|
75
|
+
addToGroup(groupUid: string, userUid: string): Promise<IdentityGroupRecord | undefined>;
|
|
76
|
+
removeFromGroup(groupUid: string, userUid: string): Promise<IdentityGroupRecord | undefined>;
|
|
77
|
+
permissions(): Promise<IdentityPermissionRecord[]>;
|
|
78
|
+
permissionById(uid: string): Promise<IdentityPermissionRecord | undefined>;
|
|
79
|
+
createPermission(input: NewPermission): Promise<IdentityPermissionRecord>;
|
|
80
|
+
setPermissionSetting(uid: string, setting: string): Promise<IdentityPermissionRecord | undefined>;
|
|
81
|
+
deletePermission(uid: string): Promise<void>;
|
|
82
|
+
clients(): Promise<IdentityClientRecord[]>;
|
|
83
|
+
createClient(input: IdentityClientRecord): Promise<IdentityClientRecord>;
|
|
60
84
|
}
|
|
61
85
|
//# sourceMappingURL=memory-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../../src/identity/memory-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,YAAY,EACZ,OAAO,EACP,SAAS,
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../../src/identity/memory-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -39,9 +39,12 @@ const live = (issued, now) => issued && (issued.expiresAt === undefined || issue
|
|
|
39
39
|
*/
|
|
40
40
|
export class MemoryIdentityStore {
|
|
41
41
|
constructor(fixture = DEFAULT_IDENTITY) {
|
|
42
|
-
this.
|
|
42
|
+
this.userList = [];
|
|
43
43
|
this.passwords = new Map();
|
|
44
|
-
this.
|
|
44
|
+
this.groupList = new Map();
|
|
45
|
+
this.permissionList = new Map();
|
|
46
|
+
this.clientRecords = new Map();
|
|
47
|
+
this.clientMap = new Map();
|
|
45
48
|
this.pairs = new Map();
|
|
46
49
|
this.access = new Map();
|
|
47
50
|
this.refresh = new Map();
|
|
@@ -50,24 +53,60 @@ export class MemoryIdentityStore {
|
|
|
50
53
|
if (fixture.format !== IDENTITY_FORMAT)
|
|
51
54
|
throw new ApiError(500, `The identity fixture states format ${String(fixture.format)}; this store reads ${IDENTITY_FORMAT}.`);
|
|
52
55
|
for (const user of fixture.users) {
|
|
53
|
-
this.
|
|
56
|
+
this.userList.push({
|
|
54
57
|
uid: user.uid,
|
|
55
58
|
username: user.username,
|
|
56
59
|
displayName: user.displayName,
|
|
57
60
|
isAdmin: user.isAdmin,
|
|
58
61
|
isActive: user.isActive,
|
|
59
|
-
needsToUpdatePassword: user.needsToUpdatePassword
|
|
62
|
+
needsToUpdatePassword: user.needsToUpdatePassword,
|
|
63
|
+
authProvider: user.authProvider,
|
|
64
|
+
autoExec: user.autoExec
|
|
60
65
|
});
|
|
61
66
|
this.passwords.set(user.uid, user.password);
|
|
62
67
|
}
|
|
63
|
-
for (const client of fixture.clients)
|
|
64
|
-
this.
|
|
68
|
+
for (const client of fixture.clients) {
|
|
69
|
+
this.clientMap.set(client.clientId, {
|
|
65
70
|
clientId: client.clientId,
|
|
66
71
|
accessTokenExpiration: client.accessTokenExpiration,
|
|
67
72
|
refreshTokenExpiration: client.refreshTokenExpiration
|
|
68
73
|
});
|
|
74
|
+
this.clientRecords.set(client.clientId, {
|
|
75
|
+
clientId: client.clientId,
|
|
76
|
+
clientSecret: client.clientSecret,
|
|
77
|
+
accessTokenExpiration: client.accessTokenExpiration,
|
|
78
|
+
refreshTokenExpiration: client.refreshTokenExpiration
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
for (const group of fixture.groups) {
|
|
82
|
+
const uid = group.uid;
|
|
83
|
+
this.groupList.set(uid, {
|
|
84
|
+
uid,
|
|
85
|
+
name: group.name,
|
|
86
|
+
description: group.description,
|
|
87
|
+
isActive: group.isActive,
|
|
88
|
+
authProvider: group.authProvider,
|
|
89
|
+
users: group.users
|
|
90
|
+
.map((username) => this.userList.find((user) => user.username === username)?.uid)
|
|
91
|
+
.filter((member) => member !== undefined)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
for (const permission of fixture.permissions) {
|
|
95
|
+
this.permissionList.set(permission.uid, {
|
|
96
|
+
uid: permission.uid,
|
|
97
|
+
path: permission.path,
|
|
98
|
+
type: permission.type,
|
|
99
|
+
setting: permission.setting,
|
|
100
|
+
user: permission.user
|
|
101
|
+
? this.userList.find((user) => user.uid === permission.user)?.uid
|
|
102
|
+
: undefined,
|
|
103
|
+
group: permission.group
|
|
104
|
+
? this.groupList.get(permission.group)?.uid
|
|
105
|
+
: undefined
|
|
106
|
+
});
|
|
107
|
+
}
|
|
69
108
|
for (const token of fixture.tokens || []) {
|
|
70
|
-
const user = this.
|
|
109
|
+
const user = this.userList.find((candidate) => candidate.username === token.username);
|
|
71
110
|
if (!user)
|
|
72
111
|
continue;
|
|
73
112
|
const subject = { userId: user.uid, clientId: token.clientId };
|
|
@@ -84,7 +123,7 @@ export class MemoryIdentityStore {
|
|
|
84
123
|
});
|
|
85
124
|
}
|
|
86
125
|
for (const code of fixture.codes || []) {
|
|
87
|
-
const user = this.
|
|
126
|
+
const user = this.userList.find((candidate) => candidate.username === code.username);
|
|
88
127
|
if (!user)
|
|
89
128
|
continue;
|
|
90
129
|
this.codes.set(code.code, {
|
|
@@ -93,17 +132,19 @@ export class MemoryIdentityStore {
|
|
|
93
132
|
}
|
|
94
133
|
}
|
|
95
134
|
async userByName(username) {
|
|
96
|
-
|
|
135
|
+
const user = this.userList.find((candidate) => candidate.username === username);
|
|
136
|
+
return user ? this.recordOf(user) : undefined;
|
|
97
137
|
}
|
|
98
138
|
async userById(uid) {
|
|
99
|
-
|
|
139
|
+
const user = this.userList.find((candidate) => candidate.uid === uid);
|
|
140
|
+
return user ? this.recordOf(user) : undefined;
|
|
100
141
|
}
|
|
101
142
|
/** A fixture holds the password in clear, so the check is an equality. */
|
|
102
143
|
async passwordMatches(user, password) {
|
|
103
144
|
return this.passwords.get(user.uid) === password;
|
|
104
145
|
}
|
|
105
146
|
async client(clientId) {
|
|
106
|
-
return this.
|
|
147
|
+
return this.clientMap.get(clientId);
|
|
107
148
|
}
|
|
108
149
|
async issue(subject, client) {
|
|
109
150
|
// The lifetimes are the client's, recorded against the instant of issue: a
|
|
@@ -188,4 +229,148 @@ export class MemoryIdentityStore {
|
|
|
188
229
|
this.claimed.add(code);
|
|
189
230
|
return true;
|
|
190
231
|
}
|
|
232
|
+
// ---------------------------------------------------------------- records
|
|
233
|
+
async users() {
|
|
234
|
+
return this.userList.map((user) => this.recordOf(user));
|
|
235
|
+
}
|
|
236
|
+
/** A user as the administration routes report one: groups derived, not stored. */
|
|
237
|
+
recordOf(user) {
|
|
238
|
+
return {
|
|
239
|
+
...user,
|
|
240
|
+
groups: [...this.groupList.values()]
|
|
241
|
+
.filter((group) => group.users.includes(user.uid))
|
|
242
|
+
.map((group) => group.uid)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async createUser(input) {
|
|
246
|
+
const user = {
|
|
247
|
+
uid: `user-${randomHex(6)}`,
|
|
248
|
+
username: input.username,
|
|
249
|
+
displayName: input.displayName,
|
|
250
|
+
isAdmin: input.isAdmin,
|
|
251
|
+
isActive: input.isActive,
|
|
252
|
+
needsToUpdatePassword: false,
|
|
253
|
+
autoExec: input.autoExec,
|
|
254
|
+
authProvider: input.authProvider
|
|
255
|
+
};
|
|
256
|
+
this.userList.push(user);
|
|
257
|
+
return this.recordOf(user);
|
|
258
|
+
}
|
|
259
|
+
async updateUser(uid, change) {
|
|
260
|
+
const user = this.userList.find((candidate) => candidate.uid === uid);
|
|
261
|
+
if (!user)
|
|
262
|
+
return undefined;
|
|
263
|
+
if (change.username !== undefined)
|
|
264
|
+
user.username = change.username;
|
|
265
|
+
if (change.displayName !== undefined)
|
|
266
|
+
user.displayName = change.displayName;
|
|
267
|
+
if (change.isAdmin !== undefined)
|
|
268
|
+
user.isAdmin = change.isAdmin;
|
|
269
|
+
if (change.isActive !== undefined)
|
|
270
|
+
user.isActive = change.isActive;
|
|
271
|
+
if (change.needsToUpdatePassword !== undefined)
|
|
272
|
+
user.needsToUpdatePassword = change.needsToUpdatePassword;
|
|
273
|
+
if (change.autoExec !== undefined)
|
|
274
|
+
user.autoExec = change.autoExec;
|
|
275
|
+
return this.recordOf(user);
|
|
276
|
+
}
|
|
277
|
+
async deleteUser(uid) {
|
|
278
|
+
const index = this.userList.findIndex((user) => user.uid === uid);
|
|
279
|
+
if (index === -1)
|
|
280
|
+
return;
|
|
281
|
+
this.userList.splice(index, 1);
|
|
282
|
+
this.passwords.delete(uid);
|
|
283
|
+
for (const group of this.groupList.values())
|
|
284
|
+
group.users = group.users.filter((member) => member !== uid);
|
|
285
|
+
for (const [key, permission] of this.permissionList)
|
|
286
|
+
if (permission.user === uid)
|
|
287
|
+
this.permissionList.delete(key);
|
|
288
|
+
for (const key of this.pairs.keys())
|
|
289
|
+
if (key.startsWith(`${uid}|`))
|
|
290
|
+
this.pairs.delete(key);
|
|
291
|
+
}
|
|
292
|
+
async setPassword(uid, password) {
|
|
293
|
+
this.passwords.set(uid, password);
|
|
294
|
+
}
|
|
295
|
+
async groups() {
|
|
296
|
+
return [...this.groupList.values()];
|
|
297
|
+
}
|
|
298
|
+
async groupById(uid) {
|
|
299
|
+
return this.groupList.get(uid);
|
|
300
|
+
}
|
|
301
|
+
async groupByName(name) {
|
|
302
|
+
return [...this.groupList.values()].find((group) => group.name === name);
|
|
303
|
+
}
|
|
304
|
+
async createGroup(input) {
|
|
305
|
+
const group = {
|
|
306
|
+
uid: `group-${randomHex(6)}`,
|
|
307
|
+
name: input.name,
|
|
308
|
+
description: input.description,
|
|
309
|
+
isActive: input.isActive,
|
|
310
|
+
users: []
|
|
311
|
+
};
|
|
312
|
+
this.groupList.set(group.uid, group);
|
|
313
|
+
return group;
|
|
314
|
+
}
|
|
315
|
+
async deleteGroup(uid) {
|
|
316
|
+
this.groupList.delete(uid);
|
|
317
|
+
for (const [key, permission] of this.permissionList)
|
|
318
|
+
if (permission.group === uid)
|
|
319
|
+
this.permissionList.delete(key);
|
|
320
|
+
}
|
|
321
|
+
async addToGroup(groupUid, userUid) {
|
|
322
|
+
const group = this.groupList.get(groupUid);
|
|
323
|
+
if (!group)
|
|
324
|
+
return undefined;
|
|
325
|
+
if (!group.users.includes(userUid))
|
|
326
|
+
group.users.push(userUid);
|
|
327
|
+
return group;
|
|
328
|
+
}
|
|
329
|
+
async removeFromGroup(groupUid, userUid) {
|
|
330
|
+
const group = this.groupList.get(groupUid);
|
|
331
|
+
if (!group)
|
|
332
|
+
return undefined;
|
|
333
|
+
group.users = group.users.filter((member) => member !== userUid);
|
|
334
|
+
return group;
|
|
335
|
+
}
|
|
336
|
+
async permissions() {
|
|
337
|
+
return [...this.permissionList.values()];
|
|
338
|
+
}
|
|
339
|
+
async permissionById(uid) {
|
|
340
|
+
return this.permissionList.get(uid);
|
|
341
|
+
}
|
|
342
|
+
async createPermission(input) {
|
|
343
|
+
const permission = {
|
|
344
|
+
uid: `rule-${randomHex(6)}`,
|
|
345
|
+
path: input.path,
|
|
346
|
+
type: input.type,
|
|
347
|
+
setting: input.setting,
|
|
348
|
+
user: input.user,
|
|
349
|
+
group: input.group
|
|
350
|
+
};
|
|
351
|
+
this.permissionList.set(permission.uid, permission);
|
|
352
|
+
return permission;
|
|
353
|
+
}
|
|
354
|
+
async setPermissionSetting(uid, setting) {
|
|
355
|
+
const permission = this.permissionList.get(uid);
|
|
356
|
+
if (!permission)
|
|
357
|
+
return undefined;
|
|
358
|
+
permission.setting = setting;
|
|
359
|
+
return permission;
|
|
360
|
+
}
|
|
361
|
+
async deletePermission(uid) {
|
|
362
|
+
this.permissionList.delete(uid);
|
|
363
|
+
}
|
|
364
|
+
async clients() {
|
|
365
|
+
return [...this.clientRecords.values()];
|
|
366
|
+
}
|
|
367
|
+
async createClient(input) {
|
|
368
|
+
this.clientRecords.set(input.clientId, input);
|
|
369
|
+
this.clientMap.set(input.clientId, {
|
|
370
|
+
clientId: input.clientId,
|
|
371
|
+
accessTokenExpiration: input.accessTokenExpiration,
|
|
372
|
+
refreshTokenExpiration: input.refreshTokenExpiration
|
|
373
|
+
});
|
|
374
|
+
return input;
|
|
375
|
+
}
|
|
191
376
|
}
|