@sasjs/api 0.2.0 → 0.3.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 +28 -8
- package/build/browser.js +810 -4
- package/build/cjs/corpus/drive.js +20 -0
- package/build/cjs/corpus/identity.js +268 -0
- package/build/cjs/corpus/runner.js +11 -2
- package/build/cjs/drive/service.js +8 -0
- package/build/cjs/host/dispatch.js +57 -2
- package/build/cjs/identity/fixture.js +118 -0
- package/build/cjs/identity/memory-store.js +179 -0
- package/build/cjs/identity/service.js +142 -0
- package/build/cjs/identity/types.js +11 -0
- package/build/cjs/index.js +11 -1
- package/build/cjs/routes/generated.js +72 -0
- package/build/cjs/routes/index.js +6 -0
- package/build/cjs/routes/overlay.js +38 -0
- package/build/esm/corpus/drive.d.ts +14 -0
- package/build/esm/corpus/drive.d.ts.map +1 -1
- package/build/esm/corpus/drive.js +20 -0
- package/build/esm/corpus/identity.d.ts +17 -0
- package/build/esm/corpus/identity.d.ts.map +1 -0
- package/build/esm/corpus/identity.js +265 -0
- package/build/esm/corpus/runner.d.ts +6 -1
- package/build/esm/corpus/runner.d.ts.map +1 -1
- package/build/esm/corpus/runner.js +11 -2
- package/build/esm/drive/service.d.ts.map +1 -1
- package/build/esm/drive/service.js +9 -1
- package/build/esm/host/dispatch.d.ts +16 -0
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +57 -2
- package/build/esm/identity/fixture.d.ts +84 -0
- package/build/esm/identity/fixture.d.ts.map +1 -0
- package/build/esm/identity/fixture.js +114 -0
- package/build/esm/identity/memory-store.d.ts +51 -0
- package/build/esm/identity/memory-store.d.ts.map +1 -0
- package/build/esm/identity/memory-store.js +175 -0
- package/build/esm/identity/service.d.ts +48 -0
- package/build/esm/identity/service.d.ts.map +1 -0
- package/build/esm/identity/service.js +137 -0
- package/build/esm/identity/types.d.ts +79 -0
- package/build/esm/identity/types.d.ts.map +1 -0
- package/build/esm/identity/types.js +10 -0
- package/build/esm/index.d.ts +7 -0
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +4 -0
- package/build/esm/routes/generated.d.ts.map +1 -1
- package/build/esm/routes/generated.js +72 -0
- package/build/esm/routes/index.d.ts.map +1 -1
- package/build/esm/routes/index.js +6 -0
- package/build/esm/routes/overlay.d.ts +9 -0
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +38 -0
- package/build/esm/types.d.ts +7 -0
- package/build/esm/types.d.ts.map +1 -1
- package/package.json +9 -3
package/build/browser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* @sasjs/api 0.
|
|
1
|
+
/* @sasjs/api 0.3.0 - the SASjs Server API, for a service worker. https://github.com/sasjs/api */
|
|
2
2
|
"use strict";
|
|
3
3
|
var SASjsApi = (() => {
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -23,14 +23,19 @@ var SASjsApi = (() => {
|
|
|
23
23
|
var index_exports = {};
|
|
24
24
|
__export(index_exports, {
|
|
25
25
|
ApiError: () => ApiError,
|
|
26
|
+
DEFAULT_IDENTITY: () => DEFAULT_IDENTITY,
|
|
26
27
|
DRIVE_CORPUS: () => DRIVE_CORPUS,
|
|
27
28
|
HANDLED_ROUTES: () => HANDLED_ROUTES,
|
|
29
|
+
IDENTITY_CORPUS: () => IDENTITY_CORPUS,
|
|
30
|
+
IDENTITY_FORMAT: () => IDENTITY_FORMAT,
|
|
28
31
|
MemoryDriveStore: () => MemoryDriveStore,
|
|
32
|
+
MemoryIdentityStore: () => MemoryIdentityStore,
|
|
29
33
|
OVERLAY: () => OVERLAY,
|
|
30
34
|
ROUTES: () => ROUTES,
|
|
31
35
|
TAG_CAPABILITIES: () => TAG_CAPABILITIES,
|
|
32
36
|
basename: () => basename,
|
|
33
37
|
createDriveService: () => createDriveService,
|
|
38
|
+
createIdentityService: () => createIdentityService,
|
|
34
39
|
dirname: () => dirname,
|
|
35
40
|
dispatch: () => dispatch,
|
|
36
41
|
findRoute: () => findRoute,
|
|
@@ -38,6 +43,7 @@ var SASjsApi = (() => {
|
|
|
38
43
|
isApiError: () => isApiError,
|
|
39
44
|
isRoot: () => isRoot,
|
|
40
45
|
normaliseDrivePath: () => normaliseDrivePath,
|
|
46
|
+
parseIdentity: () => parseIdentity,
|
|
41
47
|
routeById: () => routeById,
|
|
42
48
|
runCase: () => runCase,
|
|
43
49
|
servicesFor: () => servicesFor
|
|
@@ -203,6 +209,8 @@ var SASjsApi = (() => {
|
|
|
203
209
|
409,
|
|
204
210
|
stat.directory ? "Folder with new name already exists." : "File with new name already exists."
|
|
205
211
|
);
|
|
212
|
+
if (!stat.directory && hasBlockedExtension(to))
|
|
213
|
+
throw new ApiError(400, "Invalid file extension");
|
|
206
214
|
await store.move(from, to);
|
|
207
215
|
return { status: "success" };
|
|
208
216
|
}
|
|
@@ -423,6 +431,10 @@ var SASjsApi = (() => {
|
|
|
423
431
|
{
|
|
424
432
|
status: 204,
|
|
425
433
|
description: "No content"
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
status: 401,
|
|
437
|
+
description: "No valid access token was supplied"
|
|
426
438
|
}
|
|
427
439
|
]
|
|
428
440
|
},
|
|
@@ -438,6 +450,10 @@ var SASjsApi = (() => {
|
|
|
438
450
|
{
|
|
439
451
|
status: 200,
|
|
440
452
|
description: "Ok"
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
status: 401,
|
|
456
|
+
description: "The refresh token is missing, invalid or expired, or the client id is unknown"
|
|
441
457
|
}
|
|
442
458
|
]
|
|
443
459
|
},
|
|
@@ -466,6 +482,14 @@ var SASjsApi = (() => {
|
|
|
466
482
|
{
|
|
467
483
|
status: 200,
|
|
468
484
|
description: "Ok"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
status: 400,
|
|
488
|
+
description: "The request body is missing `clientId` or `code`"
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
status: 401,
|
|
492
|
+
description: "The authorization code is invalid, expired, already used, or was issued for another client, or the client id is unknown"
|
|
469
493
|
}
|
|
470
494
|
]
|
|
471
495
|
},
|
|
@@ -494,6 +518,26 @@ var SASjsApi = (() => {
|
|
|
494
518
|
{
|
|
495
519
|
status: 204,
|
|
496
520
|
description: "No content"
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
status: 400,
|
|
524
|
+
description: "The request body is missing `currentPassword` or `newPassword`"
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
status: 401,
|
|
528
|
+
description: "No valid access token was supplied"
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
status: 403,
|
|
532
|
+
description: "The current password is incorrect"
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
status: 404,
|
|
536
|
+
description: "The authenticated user no longer exists"
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
status: 405,
|
|
540
|
+
description: "The account authenticates through an external provider, which holds its password"
|
|
497
541
|
}
|
|
498
542
|
]
|
|
499
543
|
},
|
|
@@ -1407,6 +1451,10 @@ var SASjsApi = (() => {
|
|
|
1407
1451
|
{
|
|
1408
1452
|
status: 200,
|
|
1409
1453
|
description: "Ok"
|
|
1454
|
+
},
|
|
1455
|
+
{
|
|
1456
|
+
status: 401,
|
|
1457
|
+
description: "No valid access token or session was supplied"
|
|
1410
1458
|
}
|
|
1411
1459
|
]
|
|
1412
1460
|
},
|
|
@@ -1890,6 +1938,18 @@ var SASjsApi = (() => {
|
|
|
1890
1938
|
{
|
|
1891
1939
|
status: 200,
|
|
1892
1940
|
description: "Ok"
|
|
1941
|
+
},
|
|
1942
|
+
{
|
|
1943
|
+
status: 400,
|
|
1944
|
+
description: "The request body is missing `clientId`, or the CSRF token is missing or invalid"
|
|
1945
|
+
},
|
|
1946
|
+
{
|
|
1947
|
+
status: 401,
|
|
1948
|
+
description: "No signed-in session"
|
|
1949
|
+
},
|
|
1950
|
+
{
|
|
1951
|
+
status: 403,
|
|
1952
|
+
description: "The client id is unknown"
|
|
1893
1953
|
}
|
|
1894
1954
|
]
|
|
1895
1955
|
},
|
|
@@ -1918,6 +1978,22 @@ var SASjsApi = (() => {
|
|
|
1918
1978
|
{
|
|
1919
1979
|
status: 200,
|
|
1920
1980
|
description: "Ok"
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
status: 400,
|
|
1984
|
+
description: "The request body is missing `username` or `password`"
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
status: 401,
|
|
1988
|
+
description: "The credentials are not valid, or password sign-in is disabled for local accounts"
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
status: 429,
|
|
1992
|
+
description: "Too many failed attempts for this account"
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
status: 500,
|
|
1996
|
+
description: "An unexpected error occurred while signing in"
|
|
1921
1997
|
}
|
|
1922
1998
|
]
|
|
1923
1999
|
},
|
|
@@ -1933,6 +2009,10 @@ var SASjsApi = (() => {
|
|
|
1933
2009
|
{
|
|
1934
2010
|
status: 200,
|
|
1935
2011
|
description: "Ok"
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
status: 500,
|
|
2015
|
+
description: "The session store failed to replace the session"
|
|
1936
2016
|
}
|
|
1937
2017
|
]
|
|
1938
2018
|
}
|
|
@@ -1955,6 +2035,13 @@ var SASjsApi = (() => {
|
|
|
1955
2035
|
Web: ["identity"]
|
|
1956
2036
|
};
|
|
1957
2037
|
var success = { status: "success" };
|
|
2038
|
+
var user = {
|
|
2039
|
+
uid: "",
|
|
2040
|
+
username: "",
|
|
2041
|
+
displayName: "",
|
|
2042
|
+
isAdmin: false,
|
|
2043
|
+
needsToUpdatePassword: false
|
|
2044
|
+
};
|
|
1958
2045
|
var OVERLAY = {
|
|
1959
2046
|
// the route inventory reports what the CALLER may use, so it needs an identity
|
|
1960
2047
|
// even though the rest of the Info tag needs nothing
|
|
@@ -2006,6 +2093,37 @@ var SASjsApi = (() => {
|
|
|
2006
2093
|
},
|
|
2007
2094
|
"POST /SASjsApi/drive/deploy/upload": {
|
|
2008
2095
|
implemented: false
|
|
2096
|
+
},
|
|
2097
|
+
// The lockout and the provider failure are answered before the package is
|
|
2098
|
+
// consulted (routes/web/web.ts in sasjs/server).
|
|
2099
|
+
"POST /SASLogon/login": {
|
|
2100
|
+
implemented: true,
|
|
2101
|
+
hostResponses: [429, 500],
|
|
2102
|
+
responses: { 200: { loggedIn: true, user } }
|
|
2103
|
+
},
|
|
2104
|
+
// The sign-in flow. The statuses are the document's; these entries state the
|
|
2105
|
+
// body a client keys on, and that this package answers the operation.
|
|
2106
|
+
"POST /SASjsApi/auth/token": {
|
|
2107
|
+
implemented: true,
|
|
2108
|
+
responses: { 200: { accessToken: "", refreshToken: "" } }
|
|
2109
|
+
},
|
|
2110
|
+
"POST /SASjsApi/auth/refresh": {
|
|
2111
|
+
implemented: true,
|
|
2112
|
+
responses: { 200: { accessToken: "", refreshToken: "" } }
|
|
2113
|
+
},
|
|
2114
|
+
"POST /SASjsApi/auth/logout": { implemented: true },
|
|
2115
|
+
"POST /SASLogon/authorize": {
|
|
2116
|
+
implemented: true,
|
|
2117
|
+
responses: { 200: { code: "" } }
|
|
2118
|
+
},
|
|
2119
|
+
"GET /SASLogon/logout": {
|
|
2120
|
+
implemented: true,
|
|
2121
|
+
hostResponses: [500],
|
|
2122
|
+
responses: { 200: "OK!" }
|
|
2123
|
+
},
|
|
2124
|
+
"GET /SASjsApi/session": {
|
|
2125
|
+
implemented: true,
|
|
2126
|
+
responses: { 200: user }
|
|
2009
2127
|
}
|
|
2010
2128
|
};
|
|
2011
2129
|
|
|
@@ -2030,9 +2148,15 @@ var SASjsApi = (() => {
|
|
|
2030
2148
|
...body === void 0 ? {} : { body }
|
|
2031
2149
|
};
|
|
2032
2150
|
});
|
|
2151
|
+
for (const status of overlay.hostResponses || [])
|
|
2152
|
+
if (!documented.has(status))
|
|
2153
|
+
throw new Error(
|
|
2154
|
+
`src/routes/overlay.ts marks status ${status} of ${generated.id} as host-only, and the OpenAPI document does not state it. Correct the document in sasjs/server and re-vendor spec/swagger.yaml, or drop the annotation.`
|
|
2155
|
+
);
|
|
2033
2156
|
return {
|
|
2034
2157
|
...generated,
|
|
2035
2158
|
responses,
|
|
2159
|
+
hostResponses: overlay.hostResponses || [],
|
|
2036
2160
|
capabilities: overlay.capabilities || TAG_CAPABILITIES[generated.tag] || [],
|
|
2037
2161
|
implemented: overlay.implemented === true
|
|
2038
2162
|
};
|
|
@@ -2108,8 +2232,57 @@ var SASjsApi = (() => {
|
|
|
2108
2232
|
const route = routeById(routeId);
|
|
2109
2233
|
if (!route) throw new ApiError(404, `No such operation: ${routeId}`);
|
|
2110
2234
|
validate(route, request);
|
|
2111
|
-
const { drive } = services;
|
|
2235
|
+
const { drive, identity } = services;
|
|
2112
2236
|
const query = request.query || {};
|
|
2237
|
+
const body = request.body || {};
|
|
2238
|
+
const caller = async () => {
|
|
2239
|
+
if (request.subject) return request.subject;
|
|
2240
|
+
return identity?.authenticate(request.token);
|
|
2241
|
+
};
|
|
2242
|
+
if (routeId.startsWith("POST /SASjsApi/auth/") || routeId.startsWith("POST /SASLogon/") || routeId === "GET /SASLogon/logout" || routeId === "GET /SASjsApi/session") {
|
|
2243
|
+
if (!identity)
|
|
2244
|
+
throw new ApiError(
|
|
2245
|
+
501,
|
|
2246
|
+
`This host has no identity service, so it cannot answer ${routeId}.`
|
|
2247
|
+
);
|
|
2248
|
+
switch (routeId) {
|
|
2249
|
+
case "POST /SASjsApi/auth/token":
|
|
2250
|
+
return {
|
|
2251
|
+
status: 200,
|
|
2252
|
+
body: await identity.token({
|
|
2253
|
+
clientId: String(body.clientId ?? ""),
|
|
2254
|
+
code: String(body.code ?? "")
|
|
2255
|
+
})
|
|
2256
|
+
};
|
|
2257
|
+
case "POST /SASjsApi/auth/refresh":
|
|
2258
|
+
return { status: 200, body: await identity.refresh(request.token) };
|
|
2259
|
+
case "POST /SASjsApi/auth/logout":
|
|
2260
|
+
await identity.logout(await caller());
|
|
2261
|
+
return { status: 204 };
|
|
2262
|
+
case "POST /SASLogon/login":
|
|
2263
|
+
return {
|
|
2264
|
+
status: 200,
|
|
2265
|
+
body: await identity.login({
|
|
2266
|
+
username: String(body.username ?? ""),
|
|
2267
|
+
password: String(body.password ?? "")
|
|
2268
|
+
})
|
|
2269
|
+
};
|
|
2270
|
+
case "POST /SASLogon/authorize":
|
|
2271
|
+
return {
|
|
2272
|
+
status: 200,
|
|
2273
|
+
body: {
|
|
2274
|
+
code: await identity.authorize(
|
|
2275
|
+
await caller(),
|
|
2276
|
+
String(body.clientId ?? "")
|
|
2277
|
+
)
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
case "GET /SASLogon/logout":
|
|
2281
|
+
return { status: 200, contents: await identity.logoutSession() };
|
|
2282
|
+
case "GET /SASjsApi/session":
|
|
2283
|
+
return { status: 200, body: await identity.session(await caller()) };
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2113
2286
|
switch (routeId) {
|
|
2114
2287
|
case "GET /SASjsApi/drive/file": {
|
|
2115
2288
|
const file = await drive.read(String(query._filePath || ""));
|
|
@@ -2162,9 +2335,354 @@ var SASjsApi = (() => {
|
|
|
2162
2335
|
"POST /SASjsApi/drive/folder",
|
|
2163
2336
|
"DELETE /SASjsApi/drive/folder",
|
|
2164
2337
|
"POST /SASjsApi/drive/rename",
|
|
2165
|
-
"GET /SASjsApi/drive/filetree"
|
|
2338
|
+
"GET /SASjsApi/drive/filetree",
|
|
2339
|
+
"POST /SASjsApi/auth/token",
|
|
2340
|
+
"POST /SASjsApi/auth/refresh",
|
|
2341
|
+
"POST /SASjsApi/auth/logout",
|
|
2342
|
+
"POST /SASLogon/login",
|
|
2343
|
+
"POST /SASLogon/authorize",
|
|
2344
|
+
"GET /SASLogon/logout",
|
|
2345
|
+
"GET /SASjsApi/session"
|
|
2166
2346
|
];
|
|
2167
2347
|
|
|
2348
|
+
// src/identity/service.ts
|
|
2349
|
+
var unauthorized = () => new ApiError(401, "Unauthorized");
|
|
2350
|
+
var liveSubject = async (store, subject) => {
|
|
2351
|
+
const user3 = await store.userById(subject.userId);
|
|
2352
|
+
if (!user3 || !user3.isActive) throw unauthorized();
|
|
2353
|
+
return subject;
|
|
2354
|
+
};
|
|
2355
|
+
var createIdentityService = (store) => ({
|
|
2356
|
+
async authenticate(token) {
|
|
2357
|
+
if (token === void 0) return void 0;
|
|
2358
|
+
const subject = await store.readAccess(token);
|
|
2359
|
+
if (!subject) return void 0;
|
|
2360
|
+
const user3 = await store.userById(subject.userId);
|
|
2361
|
+
return user3 && user3.isActive ? subject : void 0;
|
|
2362
|
+
},
|
|
2363
|
+
async login({ username, password }) {
|
|
2364
|
+
const user3 = await store.userByName(username);
|
|
2365
|
+
if (!user3) throw new ApiError(401, "Username is not found.");
|
|
2366
|
+
if (!await store.passwordMatches(user3, password))
|
|
2367
|
+
throw new ApiError(401, "Invalid Password.");
|
|
2368
|
+
if (!user3.isActive) throw new ApiError(401, "This account is not active.");
|
|
2369
|
+
return {
|
|
2370
|
+
loggedIn: true,
|
|
2371
|
+
user: {
|
|
2372
|
+
uid: user3.uid,
|
|
2373
|
+
username: user3.username,
|
|
2374
|
+
displayName: user3.displayName,
|
|
2375
|
+
isAdmin: user3.isAdmin,
|
|
2376
|
+
needsToUpdatePassword: user3.needsToUpdatePassword
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
},
|
|
2380
|
+
async authorize(subject, clientId) {
|
|
2381
|
+
if (!subject) throw unauthorized();
|
|
2382
|
+
if (!await store.client(clientId))
|
|
2383
|
+
throw new ApiError(403, "Invalid clientId.");
|
|
2384
|
+
return store.issueCode({ userId: subject.userId, clientId });
|
|
2385
|
+
},
|
|
2386
|
+
async token({ clientId, code }) {
|
|
2387
|
+
const subject = await store.readCode(code);
|
|
2388
|
+
if (!subject || subject.clientId !== clientId)
|
|
2389
|
+
throw new ApiError(401, "Invalid Auth Code");
|
|
2390
|
+
const client = await store.client(clientId);
|
|
2391
|
+
if (!client) throw new ApiError(401, "Invalid clientId.");
|
|
2392
|
+
const named = await liveSubject(store, subject);
|
|
2393
|
+
if (!await store.claimCode(code))
|
|
2394
|
+
throw new ApiError(401, "Invalid Auth Code");
|
|
2395
|
+
const existing = await store.pairFor(named);
|
|
2396
|
+
if (existing) return existing;
|
|
2397
|
+
const pair = await store.issue(named, client);
|
|
2398
|
+
await store.savePair(named, pair);
|
|
2399
|
+
return pair;
|
|
2400
|
+
},
|
|
2401
|
+
async refresh(token) {
|
|
2402
|
+
const subject = token === void 0 ? void 0 : await store.readRefresh(token);
|
|
2403
|
+
if (!subject) throw unauthorized();
|
|
2404
|
+
const client = await store.client(subject.clientId);
|
|
2405
|
+
if (!client) throw new ApiError(401, "Invalid clientId.");
|
|
2406
|
+
const named = await liveSubject(store, subject);
|
|
2407
|
+
const pair = await store.issue(named, client);
|
|
2408
|
+
await store.savePair(named, pair);
|
|
2409
|
+
return pair;
|
|
2410
|
+
},
|
|
2411
|
+
async logout(subject) {
|
|
2412
|
+
if (!subject) throw unauthorized();
|
|
2413
|
+
await store.deletePair(subject);
|
|
2414
|
+
},
|
|
2415
|
+
async logoutSession() {
|
|
2416
|
+
return "OK!";
|
|
2417
|
+
},
|
|
2418
|
+
async session(subject) {
|
|
2419
|
+
if (!subject) throw unauthorized();
|
|
2420
|
+
const user3 = await store.userById(subject.userId);
|
|
2421
|
+
if (!user3) throw unauthorized();
|
|
2422
|
+
return asSession(user3);
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2425
|
+
var asSession = (user3) => ({
|
|
2426
|
+
uid: user3.uid,
|
|
2427
|
+
username: user3.username,
|
|
2428
|
+
displayName: user3.displayName,
|
|
2429
|
+
isAdmin: user3.isAdmin,
|
|
2430
|
+
needsToUpdatePassword: user3.needsToUpdatePassword
|
|
2431
|
+
});
|
|
2432
|
+
|
|
2433
|
+
// src/identity/fixture.ts
|
|
2434
|
+
var IDENTITY_FORMAT = 1;
|
|
2435
|
+
var SECONDS_IN_A_DAY = 86400;
|
|
2436
|
+
var DEFAULT_IDENTITY = {
|
|
2437
|
+
format: IDENTITY_FORMAT,
|
|
2438
|
+
users: [
|
|
2439
|
+
{
|
|
2440
|
+
uid: "user-admin",
|
|
2441
|
+
username: "admin",
|
|
2442
|
+
displayName: "Demo Admin",
|
|
2443
|
+
// long enough to pass the API's own password rule (six characters), so a
|
|
2444
|
+
// demo can sign in against a real server as well as a mock
|
|
2445
|
+
password: "admin123",
|
|
2446
|
+
isAdmin: true,
|
|
2447
|
+
isActive: true,
|
|
2448
|
+
needsToUpdatePassword: false
|
|
2449
|
+
},
|
|
2450
|
+
{
|
|
2451
|
+
uid: "user-restricted",
|
|
2452
|
+
username: "user2",
|
|
2453
|
+
displayName: "Demo User",
|
|
2454
|
+
password: "user123",
|
|
2455
|
+
isAdmin: false,
|
|
2456
|
+
isActive: true,
|
|
2457
|
+
needsToUpdatePassword: false
|
|
2458
|
+
}
|
|
2459
|
+
],
|
|
2460
|
+
groups: [
|
|
2461
|
+
{
|
|
2462
|
+
uid: "group-data-controller",
|
|
2463
|
+
name: "DCGroup",
|
|
2464
|
+
description: "Data Controller users",
|
|
2465
|
+
isActive: true,
|
|
2466
|
+
users: ["user2"]
|
|
2467
|
+
},
|
|
2468
|
+
{
|
|
2469
|
+
uid: "group-admins",
|
|
2470
|
+
name: "Admins",
|
|
2471
|
+
description: "Everything the API offers",
|
|
2472
|
+
isActive: true,
|
|
2473
|
+
users: ["admin"]
|
|
2474
|
+
}
|
|
2475
|
+
],
|
|
2476
|
+
permissions: [
|
|
2477
|
+
{
|
|
2478
|
+
uid: "rule-routes",
|
|
2479
|
+
path: "/SASjsApi/info",
|
|
2480
|
+
type: "Route",
|
|
2481
|
+
setting: "Grant",
|
|
2482
|
+
group: "group-data-controller"
|
|
2483
|
+
},
|
|
2484
|
+
{
|
|
2485
|
+
uid: "rule-execute",
|
|
2486
|
+
path: "/SASjsApi/stp/execute",
|
|
2487
|
+
type: "Route",
|
|
2488
|
+
setting: "Grant",
|
|
2489
|
+
group: "group-data-controller"
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
uid: "rule-code",
|
|
2493
|
+
path: "/SASjsApi/code/execute",
|
|
2494
|
+
type: "Route",
|
|
2495
|
+
setting: "Deny",
|
|
2496
|
+
user: "user-restricted"
|
|
2497
|
+
}
|
|
2498
|
+
],
|
|
2499
|
+
clients: [
|
|
2500
|
+
{
|
|
2501
|
+
clientId: "web_app",
|
|
2502
|
+
clientSecret: "web_app",
|
|
2503
|
+
accessTokenExpiration: SECONDS_IN_A_DAY,
|
|
2504
|
+
refreshTokenExpiration: SECONDS_IN_A_DAY * 30
|
|
2505
|
+
},
|
|
2506
|
+
{
|
|
2507
|
+
clientId: "sasjs",
|
|
2508
|
+
clientSecret: "sasjs",
|
|
2509
|
+
accessTokenExpiration: 3600,
|
|
2510
|
+
refreshTokenExpiration: SECONDS_IN_A_DAY
|
|
2511
|
+
}
|
|
2512
|
+
]
|
|
2513
|
+
};
|
|
2514
|
+
var parseIdentity = (raw) => {
|
|
2515
|
+
const fixture = raw;
|
|
2516
|
+
if (!fixture || typeof fixture !== "object")
|
|
2517
|
+
throw new Error("The identity fixture is not an object.");
|
|
2518
|
+
if (fixture.format !== IDENTITY_FORMAT)
|
|
2519
|
+
throw new Error(
|
|
2520
|
+
`The identity fixture states format ${String(fixture.format)}, and this build reads format ${IDENTITY_FORMAT}. Rebuild the site, or replace mock-identity.json with one this version wrote.`
|
|
2521
|
+
);
|
|
2522
|
+
for (const key of ["users", "groups", "permissions", "clients"])
|
|
2523
|
+
if (!Array.isArray(fixture[key]))
|
|
2524
|
+
throw new Error(`The identity fixture has no \`${key}\` array.`);
|
|
2525
|
+
for (const token of fixture.tokens || [])
|
|
2526
|
+
if (!token.accessToken || !token.refreshToken)
|
|
2527
|
+
throw new Error(
|
|
2528
|
+
`The identity fixture declares a half pair for ${token.username}/${token.clientId}: a seeded token needs both an accessToken and a refreshToken, because a token only authenticates while the subject's pair holds it.`
|
|
2529
|
+
);
|
|
2530
|
+
if (!fixture.users.some((user3) => user3.isAdmin))
|
|
2531
|
+
throw new Error(
|
|
2532
|
+
"The identity fixture declares no administrator: a demo with nobody able to administer it cannot exercise the admin routes."
|
|
2533
|
+
);
|
|
2534
|
+
return fixture;
|
|
2535
|
+
};
|
|
2536
|
+
|
|
2537
|
+
// src/identity/memory-store.ts
|
|
2538
|
+
var keyOf = (subject) => `${subject.userId}|${subject.clientId}`;
|
|
2539
|
+
var randomHex = (bytes2) => {
|
|
2540
|
+
const source = globalThis.crypto;
|
|
2541
|
+
if (!source)
|
|
2542
|
+
throw new ApiError(
|
|
2543
|
+
500,
|
|
2544
|
+
"This host has no Web Crypto (`globalThis.crypto`), so it cannot mint an unguessable identity string."
|
|
2545
|
+
);
|
|
2546
|
+
const buffer = new Uint8Array(bytes2);
|
|
2547
|
+
source.getRandomValues(buffer);
|
|
2548
|
+
return Array.from(buffer, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
2549
|
+
""
|
|
2550
|
+
);
|
|
2551
|
+
};
|
|
2552
|
+
var CODE_LIFETIME_SECONDS = 30;
|
|
2553
|
+
var live = (issued, now) => issued && (issued.expiresAt === void 0 || issued.expiresAt > now) ? issued.subject : void 0;
|
|
2554
|
+
var MemoryIdentityStore = class {
|
|
2555
|
+
constructor(fixture = DEFAULT_IDENTITY) {
|
|
2556
|
+
this.users = [];
|
|
2557
|
+
this.passwords = /* @__PURE__ */ new Map();
|
|
2558
|
+
this.clients = /* @__PURE__ */ new Map();
|
|
2559
|
+
this.pairs = /* @__PURE__ */ new Map();
|
|
2560
|
+
this.access = /* @__PURE__ */ new Map();
|
|
2561
|
+
this.refresh = /* @__PURE__ */ new Map();
|
|
2562
|
+
this.codes = /* @__PURE__ */ new Map();
|
|
2563
|
+
this.claimed = /* @__PURE__ */ new Set();
|
|
2564
|
+
if (fixture.format !== IDENTITY_FORMAT)
|
|
2565
|
+
throw new ApiError(
|
|
2566
|
+
500,
|
|
2567
|
+
`The identity fixture states format ${String(fixture.format)}; this store reads ${IDENTITY_FORMAT}.`
|
|
2568
|
+
);
|
|
2569
|
+
for (const user3 of fixture.users) {
|
|
2570
|
+
this.users.push({
|
|
2571
|
+
uid: user3.uid,
|
|
2572
|
+
username: user3.username,
|
|
2573
|
+
displayName: user3.displayName,
|
|
2574
|
+
isAdmin: user3.isAdmin,
|
|
2575
|
+
isActive: user3.isActive,
|
|
2576
|
+
needsToUpdatePassword: user3.needsToUpdatePassword
|
|
2577
|
+
});
|
|
2578
|
+
this.passwords.set(user3.uid, user3.password);
|
|
2579
|
+
}
|
|
2580
|
+
for (const client of fixture.clients)
|
|
2581
|
+
this.clients.set(client.clientId, {
|
|
2582
|
+
clientId: client.clientId,
|
|
2583
|
+
accessTokenExpiration: client.accessTokenExpiration,
|
|
2584
|
+
refreshTokenExpiration: client.refreshTokenExpiration
|
|
2585
|
+
});
|
|
2586
|
+
for (const token of fixture.tokens || []) {
|
|
2587
|
+
const user3 = this.users.find(
|
|
2588
|
+
(candidate) => candidate.username === token.username
|
|
2589
|
+
);
|
|
2590
|
+
if (!user3) continue;
|
|
2591
|
+
const subject = { userId: user3.uid, clientId: token.clientId };
|
|
2592
|
+
if (token.accessToken) this.access.set(token.accessToken, { subject });
|
|
2593
|
+
if (token.refreshToken) this.refresh.set(token.refreshToken, { subject });
|
|
2594
|
+
if (token.accessToken && token.refreshToken)
|
|
2595
|
+
this.pairs.set(keyOf(subject), {
|
|
2596
|
+
accessToken: token.accessToken,
|
|
2597
|
+
refreshToken: token.refreshToken
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
for (const code of fixture.codes || []) {
|
|
2601
|
+
const user3 = this.users.find(
|
|
2602
|
+
(candidate) => candidate.username === code.username
|
|
2603
|
+
);
|
|
2604
|
+
if (!user3) continue;
|
|
2605
|
+
this.codes.set(code.code, {
|
|
2606
|
+
subject: { userId: user3.uid, clientId: code.clientId }
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
async userByName(username) {
|
|
2611
|
+
return this.users.find((user3) => user3.username === username);
|
|
2612
|
+
}
|
|
2613
|
+
async userById(uid) {
|
|
2614
|
+
return this.users.find((user3) => user3.uid === uid);
|
|
2615
|
+
}
|
|
2616
|
+
/** A fixture holds the password in clear, so the check is an equality. */
|
|
2617
|
+
async passwordMatches(user3, password) {
|
|
2618
|
+
return this.passwords.get(user3.uid) === password;
|
|
2619
|
+
}
|
|
2620
|
+
async client(clientId) {
|
|
2621
|
+
return this.clients.get(clientId);
|
|
2622
|
+
}
|
|
2623
|
+
async issue(subject, client) {
|
|
2624
|
+
const issuedAt = Date.now();
|
|
2625
|
+
const pair = {
|
|
2626
|
+
accessToken: `access-${randomHex(16)}`,
|
|
2627
|
+
refreshToken: `refresh-${randomHex(16)}`
|
|
2628
|
+
};
|
|
2629
|
+
this.access.set(pair.accessToken, {
|
|
2630
|
+
subject,
|
|
2631
|
+
expiresAt: issuedAt + client.accessTokenExpiration * 1e3
|
|
2632
|
+
});
|
|
2633
|
+
this.refresh.set(pair.refreshToken, {
|
|
2634
|
+
subject,
|
|
2635
|
+
expiresAt: issuedAt + client.refreshTokenExpiration * 1e3
|
|
2636
|
+
});
|
|
2637
|
+
return pair;
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* A token names its subject only while that subject's CURRENT pair holds it.
|
|
2641
|
+
* The server verifies a token against the pair saved for the user and client,
|
|
2642
|
+
* so a logged-out or rotated token stops working there too; without the same
|
|
2643
|
+
* check here, the browser host would honour a token the server had revoked.
|
|
2644
|
+
*/
|
|
2645
|
+
async readAccess(token) {
|
|
2646
|
+
return this.current(token, this.access, "accessToken");
|
|
2647
|
+
}
|
|
2648
|
+
async readRefresh(token) {
|
|
2649
|
+
return this.current(token, this.refresh, "refreshToken");
|
|
2650
|
+
}
|
|
2651
|
+
current(token, issued, kind) {
|
|
2652
|
+
const entry = issued.get(token);
|
|
2653
|
+
if (!entry) return void 0;
|
|
2654
|
+
const subject = live(entry, Date.now());
|
|
2655
|
+
if (!subject) return void 0;
|
|
2656
|
+
const pair = this.pairs.get(keyOf(subject));
|
|
2657
|
+
return pair && pair[kind] === token ? subject : void 0;
|
|
2658
|
+
}
|
|
2659
|
+
async pairFor(subject) {
|
|
2660
|
+
return this.pairs.get(keyOf(subject));
|
|
2661
|
+
}
|
|
2662
|
+
async savePair(subject, pair) {
|
|
2663
|
+
this.pairs.set(keyOf(subject), pair);
|
|
2664
|
+
}
|
|
2665
|
+
async deletePair(subject) {
|
|
2666
|
+
this.pairs.delete(keyOf(subject));
|
|
2667
|
+
}
|
|
2668
|
+
async issueCode(subject) {
|
|
2669
|
+
const code = `code-${randomHex(16)}`;
|
|
2670
|
+
this.codes.set(code, {
|
|
2671
|
+
subject,
|
|
2672
|
+
expiresAt: Date.now() + CODE_LIFETIME_SECONDS * 1e3
|
|
2673
|
+
});
|
|
2674
|
+
return code;
|
|
2675
|
+
}
|
|
2676
|
+
async readCode(code) {
|
|
2677
|
+
return live(this.codes.get(code), Date.now());
|
|
2678
|
+
}
|
|
2679
|
+
async claimCode(code) {
|
|
2680
|
+
if (this.claimed.has(code) || !this.codes.has(code)) return false;
|
|
2681
|
+
this.claimed.add(code);
|
|
2682
|
+
return true;
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
|
|
2168
2686
|
// src/corpus/drive.ts
|
|
2169
2687
|
var success2 = { status: "success" };
|
|
2170
2688
|
var bytes = (...values) => Uint8Array.from(values);
|
|
@@ -2398,6 +2916,26 @@ var SASjsApi = (() => {
|
|
|
2398
2916
|
request: { body: { oldPath: "/data/nope.csv", newPath: "/data/x.csv" } },
|
|
2399
2917
|
expect: { status: 404, error: "No file/folder found for provided path." }
|
|
2400
2918
|
},
|
|
2919
|
+
{
|
|
2920
|
+
id: "rename-file-to-blocked-extension",
|
|
2921
|
+
route: "POST /SASjsApi/drive/rename",
|
|
2922
|
+
description: "refuses a rename that would produce a name the file routes refuse",
|
|
2923
|
+
seed: { "/data/report.csv": "a,b" },
|
|
2924
|
+
request: {
|
|
2925
|
+
body: { oldPath: "/data/report.csv", newPath: "/data/report.sh" }
|
|
2926
|
+
},
|
|
2927
|
+
expect: { status: 400, error: "Invalid file extension" }
|
|
2928
|
+
},
|
|
2929
|
+
{
|
|
2930
|
+
id: "rename-folder-to-dotted-name",
|
|
2931
|
+
route: "POST /SASjsApi/drive/rename",
|
|
2932
|
+
description: "renames a folder whose new name carries a blocked suffix",
|
|
2933
|
+
seed: { "/data/reports/a.csv": "a" },
|
|
2934
|
+
request: {
|
|
2935
|
+
body: { oldPath: "/data/reports", newPath: "/data/reports.sh" }
|
|
2936
|
+
},
|
|
2937
|
+
expect: { status: 200, body: { status: "success" } }
|
|
2938
|
+
},
|
|
2401
2939
|
{
|
|
2402
2940
|
id: "rename-folder-into-itself",
|
|
2403
2941
|
route: "POST /SASjsApi/drive/rename",
|
|
@@ -2447,9 +2985,277 @@ var SASjsApi = (() => {
|
|
|
2447
2985
|
}
|
|
2448
2986
|
];
|
|
2449
2987
|
|
|
2988
|
+
// src/corpus/identity.ts
|
|
2989
|
+
var SIGNED_IN = {
|
|
2990
|
+
...DEFAULT_IDENTITY,
|
|
2991
|
+
tokens: [
|
|
2992
|
+
{
|
|
2993
|
+
username: "user2",
|
|
2994
|
+
clientId: "web_app",
|
|
2995
|
+
accessToken: "seed-access",
|
|
2996
|
+
refreshToken: "seed-refresh"
|
|
2997
|
+
},
|
|
2998
|
+
{
|
|
2999
|
+
username: "admin",
|
|
3000
|
+
clientId: "removed_client",
|
|
3001
|
+
accessToken: "seed-access-orphan",
|
|
3002
|
+
refreshToken: "seed-refresh-orphan"
|
|
3003
|
+
}
|
|
3004
|
+
],
|
|
3005
|
+
codes: [
|
|
3006
|
+
{ code: "seed-code", username: "user2", clientId: "web_app" },
|
|
3007
|
+
{ code: "seed-orphan-code", username: "user2", clientId: "removed_client" },
|
|
3008
|
+
// the same shape with a pair already issued: the exchange must still refuse,
|
|
3009
|
+
// rather than handing back tokens that belong to a client that is gone
|
|
3010
|
+
{
|
|
3011
|
+
code: "seed-orphan-pair-code",
|
|
3012
|
+
username: "admin",
|
|
3013
|
+
clientId: "removed_client"
|
|
3014
|
+
}
|
|
3015
|
+
]
|
|
3016
|
+
};
|
|
3017
|
+
var DEACTIVATED = {
|
|
3018
|
+
...SIGNED_IN,
|
|
3019
|
+
users: SIGNED_IN.users.map(
|
|
3020
|
+
(user3) => user3.username === "user2" ? { ...user3, isActive: false } : user3
|
|
3021
|
+
)
|
|
3022
|
+
};
|
|
3023
|
+
var user2 = {
|
|
3024
|
+
username: "user2",
|
|
3025
|
+
displayName: "Demo User",
|
|
3026
|
+
isAdmin: false,
|
|
3027
|
+
needsToUpdatePassword: false
|
|
3028
|
+
};
|
|
3029
|
+
var IDENTITY_CORPUS = [
|
|
3030
|
+
{
|
|
3031
|
+
id: "login",
|
|
3032
|
+
route: "POST /SASLogon/login",
|
|
3033
|
+
description: "signs a seeded user in",
|
|
3034
|
+
request: { body: { username: "admin", password: "admin123" } },
|
|
3035
|
+
expect: {
|
|
3036
|
+
status: 200,
|
|
3037
|
+
partial: {
|
|
3038
|
+
loggedIn: true,
|
|
3039
|
+
user: {
|
|
3040
|
+
username: "admin",
|
|
3041
|
+
displayName: "Demo Admin",
|
|
3042
|
+
isAdmin: true,
|
|
3043
|
+
needsToUpdatePassword: false
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
},
|
|
3048
|
+
{
|
|
3049
|
+
id: "login-missing-username",
|
|
3050
|
+
route: "POST /SASLogon/login",
|
|
3051
|
+
description: "refuses a sign-in with no username as 400, naming the field",
|
|
3052
|
+
request: { body: { password: "admin123" } },
|
|
3053
|
+
expect: { status: 400, error: '"username" is required' }
|
|
3054
|
+
},
|
|
3055
|
+
{
|
|
3056
|
+
id: "login-deactivated",
|
|
3057
|
+
route: "POST /SASLogon/login",
|
|
3058
|
+
description: "refuses a deactivated account, naming it",
|
|
3059
|
+
identity: DEACTIVATED,
|
|
3060
|
+
request: { body: { username: "user2", password: "user123" } },
|
|
3061
|
+
expect: { status: 401, error: "This account is not active." }
|
|
3062
|
+
},
|
|
3063
|
+
{
|
|
3064
|
+
id: "login-wrong-password",
|
|
3065
|
+
route: "POST /SASLogon/login",
|
|
3066
|
+
description: "refuses a wrong password as 401, naming it",
|
|
3067
|
+
request: { body: { username: "admin", password: "wrong123" } },
|
|
3068
|
+
expect: { status: 401, error: "Invalid Password." }
|
|
3069
|
+
},
|
|
3070
|
+
{
|
|
3071
|
+
id: "login-unknown-user",
|
|
3072
|
+
route: "POST /SASLogon/login",
|
|
3073
|
+
description: "refuses an unknown username as 401, naming it",
|
|
3074
|
+
request: { body: { username: "nobody", password: "admin123" } },
|
|
3075
|
+
expect: { status: 401, error: "Username is not found." }
|
|
3076
|
+
},
|
|
3077
|
+
{
|
|
3078
|
+
id: "authorize",
|
|
3079
|
+
route: "POST /SASLogon/authorize",
|
|
3080
|
+
description: "issues a code to a signed-in caller",
|
|
3081
|
+
identity: SIGNED_IN,
|
|
3082
|
+
request: {
|
|
3083
|
+
token: "seed-access",
|
|
3084
|
+
body: { clientId: "web_app" }
|
|
3085
|
+
},
|
|
3086
|
+
expect: { status: 200, keys: ["code"] }
|
|
3087
|
+
},
|
|
3088
|
+
{
|
|
3089
|
+
id: "authorize-missing-client",
|
|
3090
|
+
route: "POST /SASLogon/authorize",
|
|
3091
|
+
description: "refuses an authorize with no clientId as 400, naming the field",
|
|
3092
|
+
identity: SIGNED_IN,
|
|
3093
|
+
request: { token: "seed-access", body: {} },
|
|
3094
|
+
expect: { status: 400, error: '"clientId" is required' }
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
id: "authorize-without-session",
|
|
3098
|
+
route: "POST /SASLogon/authorize",
|
|
3099
|
+
description: "refuses with 401 when no caller is signed in",
|
|
3100
|
+
request: { body: { clientId: "web_app" } },
|
|
3101
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3102
|
+
},
|
|
3103
|
+
{
|
|
3104
|
+
id: "authorize-unknown-client",
|
|
3105
|
+
route: "POST /SASLogon/authorize",
|
|
3106
|
+
description: "refuses an unknown client with 403",
|
|
3107
|
+
identity: SIGNED_IN,
|
|
3108
|
+
request: { token: "seed-access", body: { clientId: "nope" } },
|
|
3109
|
+
expect: { status: 403, error: "Invalid clientId." }
|
|
3110
|
+
},
|
|
3111
|
+
{
|
|
3112
|
+
id: "exchange-code",
|
|
3113
|
+
route: "POST /SASjsApi/auth/token",
|
|
3114
|
+
description: "exchanges a code for a token pair",
|
|
3115
|
+
identity: SIGNED_IN,
|
|
3116
|
+
request: { body: { clientId: "web_app", code: "seed-code" } },
|
|
3117
|
+
expect: { status: 200, keys: ["accessToken", "refreshToken"] }
|
|
3118
|
+
},
|
|
3119
|
+
{
|
|
3120
|
+
id: "exchange-missing-client",
|
|
3121
|
+
route: "POST /SASjsApi/auth/token",
|
|
3122
|
+
description: "refuses an exchange with no clientId as 400, naming the field",
|
|
3123
|
+
identity: SIGNED_IN,
|
|
3124
|
+
request: { body: { code: "seed-code" } },
|
|
3125
|
+
expect: { status: 400, error: '"clientId" is required' }
|
|
3126
|
+
},
|
|
3127
|
+
{
|
|
3128
|
+
id: "exchange-code-unknown",
|
|
3129
|
+
route: "POST /SASjsApi/auth/token",
|
|
3130
|
+
description: "refuses a code the host never issued",
|
|
3131
|
+
request: { body: { clientId: "web_app", code: "not-a-code" } },
|
|
3132
|
+
expect: { status: 401, error: "Invalid Auth Code" }
|
|
3133
|
+
},
|
|
3134
|
+
{
|
|
3135
|
+
id: "exchange-code-other-client",
|
|
3136
|
+
route: "POST /SASjsApi/auth/token",
|
|
3137
|
+
description: "refuses a code presented for another client, as a bad code",
|
|
3138
|
+
identity: SIGNED_IN,
|
|
3139
|
+
request: { body: { clientId: "sasjs", code: "seed-code" } },
|
|
3140
|
+
expect: { status: 401, error: "Invalid Auth Code" }
|
|
3141
|
+
},
|
|
3142
|
+
{
|
|
3143
|
+
id: "exchange-code-client-gone",
|
|
3144
|
+
route: "POST /SASjsApi/auth/token",
|
|
3145
|
+
description: "refuses a valid code whose client no longer exists",
|
|
3146
|
+
identity: SIGNED_IN,
|
|
3147
|
+
request: { body: { clientId: "removed_client", code: "seed-orphan-code" } },
|
|
3148
|
+
expect: { status: 401, error: "Invalid clientId." }
|
|
3149
|
+
},
|
|
3150
|
+
{
|
|
3151
|
+
id: "exchange-code-client-gone-with-pair",
|
|
3152
|
+
route: "POST /SASjsApi/auth/token",
|
|
3153
|
+
description: "refuses an exchange whose client is gone even when a pair was issued before it was deleted",
|
|
3154
|
+
identity: SIGNED_IN,
|
|
3155
|
+
request: {
|
|
3156
|
+
body: { clientId: "removed_client", code: "seed-orphan-pair-code" }
|
|
3157
|
+
},
|
|
3158
|
+
expect: { status: 401, error: "Invalid clientId." }
|
|
3159
|
+
},
|
|
3160
|
+
{
|
|
3161
|
+
id: "exchange-code-deactivated",
|
|
3162
|
+
route: "POST /SASjsApi/auth/token",
|
|
3163
|
+
description: "refuses an exchange for an account that has been deactivated",
|
|
3164
|
+
identity: DEACTIVATED,
|
|
3165
|
+
request: { body: { clientId: "web_app", code: "seed-code" } },
|
|
3166
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3167
|
+
},
|
|
3168
|
+
{
|
|
3169
|
+
id: "refresh",
|
|
3170
|
+
route: "POST /SASjsApi/auth/refresh",
|
|
3171
|
+
description: "mints a new pair from a refresh token",
|
|
3172
|
+
identity: SIGNED_IN,
|
|
3173
|
+
request: { token: "seed-refresh" },
|
|
3174
|
+
expect: { status: 200, keys: ["accessToken", "refreshToken"] }
|
|
3175
|
+
},
|
|
3176
|
+
{
|
|
3177
|
+
id: "refresh-invalid",
|
|
3178
|
+
route: "POST /SASjsApi/auth/refresh",
|
|
3179
|
+
description: "refuses a refresh token it did not issue",
|
|
3180
|
+
request: { token: "not-a-token" },
|
|
3181
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3182
|
+
},
|
|
3183
|
+
{
|
|
3184
|
+
id: "refresh-without-token",
|
|
3185
|
+
route: "POST /SASjsApi/auth/refresh",
|
|
3186
|
+
description: "refuses a refresh with no token at all",
|
|
3187
|
+
request: {},
|
|
3188
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3189
|
+
},
|
|
3190
|
+
{
|
|
3191
|
+
id: "refresh-client-gone",
|
|
3192
|
+
route: "POST /SASjsApi/auth/refresh",
|
|
3193
|
+
description: "refuses a refresh whose client no longer exists",
|
|
3194
|
+
identity: SIGNED_IN,
|
|
3195
|
+
request: { token: "seed-refresh-orphan" },
|
|
3196
|
+
expect: { status: 401, error: "Invalid clientId." }
|
|
3197
|
+
},
|
|
3198
|
+
{
|
|
3199
|
+
id: "refresh-deactivated",
|
|
3200
|
+
route: "POST /SASjsApi/auth/refresh",
|
|
3201
|
+
description: "refuses a refresh for an account that has been deactivated, as the server does on every request",
|
|
3202
|
+
identity: DEACTIVATED,
|
|
3203
|
+
request: { token: "seed-refresh" },
|
|
3204
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3205
|
+
},
|
|
3206
|
+
{
|
|
3207
|
+
id: "logout",
|
|
3208
|
+
route: "POST /SASjsApi/auth/logout",
|
|
3209
|
+
description: "revokes the caller pair and answers 204",
|
|
3210
|
+
identity: SIGNED_IN,
|
|
3211
|
+
request: { token: "seed-access" },
|
|
3212
|
+
expect: { status: 204 }
|
|
3213
|
+
},
|
|
3214
|
+
{
|
|
3215
|
+
id: "logout-without-token",
|
|
3216
|
+
route: "POST /SASjsApi/auth/logout",
|
|
3217
|
+
description: "refuses a logout with no valid token",
|
|
3218
|
+
request: {},
|
|
3219
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3220
|
+
},
|
|
3221
|
+
{
|
|
3222
|
+
id: "session",
|
|
3223
|
+
route: "GET /SASjsApi/session",
|
|
3224
|
+
description: "reports the caller the token names",
|
|
3225
|
+
identity: SIGNED_IN,
|
|
3226
|
+
request: { token: "seed-access" },
|
|
3227
|
+
expect: { status: 200, partial: user2 }
|
|
3228
|
+
},
|
|
3229
|
+
{
|
|
3230
|
+
id: "session-deactivated",
|
|
3231
|
+
route: "GET /SASjsApi/session",
|
|
3232
|
+
description: "stops resolving a token whose account has been deactivated",
|
|
3233
|
+
identity: DEACTIVATED,
|
|
3234
|
+
request: { token: "seed-access" },
|
|
3235
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3236
|
+
},
|
|
3237
|
+
{
|
|
3238
|
+
id: "session-without-token",
|
|
3239
|
+
route: "GET /SASjsApi/session",
|
|
3240
|
+
description: "refuses the session read with no caller",
|
|
3241
|
+
request: {},
|
|
3242
|
+
expect: { status: 401, error: "Unauthorized" }
|
|
3243
|
+
},
|
|
3244
|
+
{
|
|
3245
|
+
id: "sign-out",
|
|
3246
|
+
route: "GET /SASLogon/logout",
|
|
3247
|
+
description: "answers the body the browser sign-out returns",
|
|
3248
|
+
request: {},
|
|
3249
|
+
expect: { status: 200, contents: "OK!" }
|
|
3250
|
+
}
|
|
3251
|
+
];
|
|
3252
|
+
|
|
2450
3253
|
// src/corpus/runner.ts
|
|
2451
3254
|
var servicesFor = (testCase) => ({
|
|
2452
|
-
drive: createDriveService(new MemoryDriveStore(testCase.seed || {}))
|
|
3255
|
+
drive: createDriveService(new MemoryDriveStore(testCase.seed || {})),
|
|
3256
|
+
identity: createIdentityService(
|
|
3257
|
+
new MemoryIdentityStore(testCase.identity || DEFAULT_IDENTITY)
|
|
3258
|
+
)
|
|
2453
3259
|
});
|
|
2454
3260
|
var runCase = async (testCase, services = servicesFor(testCase)) => {
|
|
2455
3261
|
try {
|