@sasjs/api 0.6.0 → 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.
Files changed (40) hide show
  1. package/README.md +3 -2
  2. package/build/browser.js +2231 -118
  3. package/build/cjs/corpus/records.js +1222 -0
  4. package/build/cjs/corpus/runner.js +15 -6
  5. package/build/cjs/drive/deploy.js +49 -10
  6. package/build/cjs/host/dispatch.js +190 -2
  7. package/build/cjs/identity/memory-store.js +196 -11
  8. package/build/cjs/identity/records.js +398 -0
  9. package/build/cjs/index.js +8 -1
  10. package/build/cjs/routes/generated.js +184 -4
  11. package/build/cjs/routes/overlay.js +53 -58
  12. package/build/esm/corpus/records.d.ts +25 -0
  13. package/build/esm/corpus/records.d.ts.map +1 -0
  14. package/build/esm/corpus/records.js +1219 -0
  15. package/build/esm/corpus/runner.d.ts.map +1 -1
  16. package/build/esm/corpus/runner.js +15 -6
  17. package/build/esm/drive/deploy.d.ts +10 -0
  18. package/build/esm/drive/deploy.d.ts.map +1 -1
  19. package/build/esm/drive/deploy.js +48 -9
  20. package/build/esm/host/dispatch.d.ts +9 -0
  21. package/build/esm/host/dispatch.d.ts.map +1 -1
  22. package/build/esm/host/dispatch.js +190 -2
  23. package/build/esm/identity/fixture.d.ts +6 -0
  24. package/build/esm/identity/fixture.d.ts.map +1 -1
  25. package/build/esm/identity/memory-store.d.ts +29 -5
  26. package/build/esm/identity/memory-store.d.ts.map +1 -1
  27. package/build/esm/identity/memory-store.js +196 -11
  28. package/build/esm/identity/records.d.ts +102 -0
  29. package/build/esm/identity/records.d.ts.map +1 -0
  30. package/build/esm/identity/records.js +393 -0
  31. package/build/esm/identity/types.d.ts +102 -2
  32. package/build/esm/identity/types.d.ts.map +1 -1
  33. package/build/esm/index.d.ts +4 -1
  34. package/build/esm/index.d.ts.map +1 -1
  35. package/build/esm/index.js +2 -0
  36. package/build/esm/routes/generated.d.ts.map +1 -1
  37. package/build/esm/routes/generated.js +184 -4
  38. package/build/esm/routes/overlay.d.ts.map +1 -1
  39. package/build/esm/routes/overlay.js +53 -58
  40. package/package.json +1 -1
package/build/browser.js CHANGED
@@ -1,4 +1,4 @@
1
- /* @sasjs/api 0.6.0 - the SASjs Server API, for a service worker. https://github.com/sasjs/api */
1
+ /* @sasjs/api 0.7.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;
@@ -22,6 +22,7 @@ var SASjsApi = (() => {
22
22
  // src/index.ts
23
23
  var index_exports = {};
24
24
  __export(index_exports, {
25
+ ADMIN_SIGNED_IN: () => ADMIN_SIGNED_IN,
25
26
  ApiError: () => ApiError,
26
27
  DEFAULT_IDENTITY: () => DEFAULT_IDENTITY,
27
28
  DEPLOY_LIMITS: () => DEPLOY_LIMITS,
@@ -34,12 +35,15 @@ var SASjsApi = (() => {
34
35
  MemoryDriveStore: () => MemoryDriveStore,
35
36
  MemoryIdentityStore: () => MemoryIdentityStore,
36
37
  OVERLAY: () => OVERLAY,
38
+ PROVIDER_MANAGED: () => PROVIDER_MANAGED,
39
+ RECORD_CORPUS: () => RECORD_CORPUS,
37
40
  ROUTES: () => ROUTES,
38
41
  TAG_CAPABILITIES: () => TAG_CAPABILITIES,
39
42
  basename: () => basename,
40
43
  checkDeployTree: () => checkDeployTree,
41
44
  createDriveService: () => createDriveService,
42
45
  createIdentityService: () => createIdentityService,
46
+ createRecordService: () => createRecordService,
43
47
  decodeBase64: () => decodeBase64,
44
48
  deployTree: () => deployTree,
45
49
  dirname: () => dirname,
@@ -54,6 +58,7 @@ var SASjsApi = (() => {
54
58
  parseIdentity: () => parseIdentity,
55
59
  routeById: () => routeById,
56
60
  runCase: () => runCase,
61
+ sanitiseDisplayName: () => sanitiseDisplayName,
57
62
  servicesFor: () => servicesFor
58
63
  });
59
64
 
@@ -111,6 +116,7 @@ var SASjsApi = (() => {
111
116
  status: "success",
112
117
  message: "Files deployed successfully to @sasjs/server."
113
118
  };
119
+ var MAX_DEPTH = 32;
114
120
  var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
115
121
  var base64Body = (value) => value.replace(/[^A-Za-z0-9+/]/g, "");
116
122
  var base64Length = (value) => Math.floor(base64Body(value).length * 3 / 4);
@@ -145,7 +151,9 @@ var SASjsApi = (() => {
145
151
  };
146
152
  var memberName = (member) => member.type === "service" ? `${member.name}.sas` : member.name;
147
153
  var memberBytes = (member) => member.type === "file" ? base64Length(member.code) : utf8Length(member.code);
148
- var describeSize = (bytes2) => bytes2 < 1024 * 1024 ? `${bytes2} bytes` : `${Math.round(bytes2 / (1024 * 1024) * 10) / 10} MB`;
154
+ var MEGABYTE = 1024 * 1024;
155
+ var describeSize = (bytes2) => bytes2 < MEGABYTE ? `${bytes2} bytes` : `${Math.ceil(bytes2 / MEGABYTE * 10) / 10} MB`;
156
+ var describeLimit = (bytes2) => bytes2 < MEGABYTE ? `${bytes2} bytes` : `${Math.round(bytes2 / MEGABYTE * 10) / 10} MB`;
149
157
  var emptyWalk = () => ({
150
158
  entries: 0,
151
159
  totalBytes: 0,
@@ -153,7 +161,7 @@ var SASjsApi = (() => {
153
161
  });
154
162
  var isSafeSegment = (name) => name !== "" && name !== "." && name !== ".." && !/[/\\]/.test(name) && // a Windows drive or a NUL byte is never a segment either
155
163
  !/[\0]/.test(name);
156
- var walk = (members, parentPath, acc) => {
164
+ var walk = (members, parentPath, acc, depth = 1) => {
157
165
  for (const member of members) {
158
166
  const name = memberName(member);
159
167
  const memberPath = parentPath ? `${parentPath}/${name}` : name;
@@ -165,7 +173,11 @@ var SASjsApi = (() => {
165
173
  };
166
174
  acc.entries++;
167
175
  if (member.type === "folder") {
168
- walk(member.members, memberPath, acc);
176
+ if (depth + 1 > MAX_DEPTH) {
177
+ if (!acc.tooDeep) acc.tooDeep = depth + 1;
178
+ continue;
179
+ }
180
+ walk(member.members, memberPath, acc, depth + 1);
169
181
  continue;
170
182
  }
171
183
  const bytes2 = memberBytes(member);
@@ -186,6 +198,12 @@ var SASjsApi = (() => {
186
198
  const measured = emptyWalk();
187
199
  walk(members, "", measured);
188
200
  if (measured.nameRefusal) return measured.nameRefusal;
201
+ if (measured.tooDeep)
202
+ return {
203
+ code: 413,
204
+ status: "failure",
205
+ message: `The file tree is nested too deeply: ${measured.tooDeep} levels (maximum ${MAX_DEPTH}).`
206
+ };
189
207
  if (measured.entries > limits.entries)
190
208
  return {
191
209
  code: 413,
@@ -196,13 +214,13 @@ var SASjsApi = (() => {
196
214
  return {
197
215
  code: 413,
198
216
  status: "failure",
199
- message: `The file tree exceeds the deploy limit: ${describeSize(measured.totalBytes)} (maximum ${describeSize(limits.totalBytes)}).`
217
+ message: `The file tree exceeds the deploy limit: ${describeSize(measured.totalBytes)} (maximum ${describeLimit(limits.totalBytes)}).`
200
218
  };
201
219
  if (measured.largest.bytes > limits.fileBytes)
202
220
  return {
203
221
  code: 413,
204
222
  status: "failure",
205
- message: `A file in the tree exceeds the deploy limit: ${measured.largest.path} is ${describeSize(measured.largest.bytes)} (maximum ${describeSize(limits.fileBytes)} per file).`
223
+ message: `A file in the tree exceeds the deploy limit: ${measured.largest.path} is ${describeSize(measured.largest.bytes)} (maximum ${describeLimit(limits.fileBytes)} per file).`
206
224
  };
207
225
  return null;
208
226
  };
@@ -665,7 +683,7 @@ var SASjsApi = (() => {
665
683
  },
666
684
  {
667
685
  status: 401,
668
- description: "The authorization code is invalid, expired, already used, or was issued for another client, or the client id is unknown"
686
+ description: "The authorization code is invalid, expired, already used, or was issued for another client, or the client id is unknown. This route is unauthenticated, so an unknown client id is a `401` here where `POST /SASLogon/authorize` (which requires a session) answers `403`"
669
687
  }
670
688
  ]
671
689
  },
@@ -759,6 +777,10 @@ var SASjsApi = (() => {
759
777
  {
760
778
  status: 200,
761
779
  description: "Ok"
780
+ },
781
+ {
782
+ status: 401,
783
+ description: "Admin account required"
762
784
  }
763
785
  ]
764
786
  },
@@ -799,6 +821,14 @@ var SASjsApi = (() => {
799
821
  {
800
822
  status: 200,
801
823
  description: "Ok"
824
+ },
825
+ {
826
+ status: 401,
827
+ description: "Admin account required"
828
+ },
829
+ {
830
+ status: 403,
831
+ description: "Client ID already exists."
802
832
  }
803
833
  ]
804
834
  },
@@ -1288,7 +1318,7 @@ var SASjsApi = (() => {
1288
1318
  },
1289
1319
  {
1290
1320
  status: 409,
1291
- description: "Folder already exists"
1321
+ description: "Folder already exists. The body is that message as plain text."
1292
1322
  }
1293
1323
  ]
1294
1324
  },
@@ -1328,7 +1358,7 @@ var SASjsApi = (() => {
1328
1358
  },
1329
1359
  {
1330
1360
  status: 409,
1331
- description: "Something already exists at the new path"
1361
+ description: "Something already exists at the new path. The body is that message as plain text."
1332
1362
  }
1333
1363
  ]
1334
1364
  },
@@ -1378,6 +1408,14 @@ var SASjsApi = (() => {
1378
1408
  {
1379
1409
  status: 200,
1380
1410
  description: "Ok"
1411
+ },
1412
+ {
1413
+ status: 401,
1414
+ description: "Admin account required"
1415
+ },
1416
+ {
1417
+ status: 409,
1418
+ description: "Group name already exists."
1381
1419
  }
1382
1420
  ]
1383
1421
  },
@@ -1406,6 +1444,22 @@ var SASjsApi = (() => {
1406
1444
  {
1407
1445
  status: 200,
1408
1446
  description: "Ok"
1447
+ },
1448
+ {
1449
+ status: 400,
1450
+ description: "Can't add/remove user to 'public' group."
1451
+ },
1452
+ {
1453
+ status: 401,
1454
+ description: "Admin account required"
1455
+ },
1456
+ {
1457
+ status: 404,
1458
+ description: "Group not found. / User not found."
1459
+ },
1460
+ {
1461
+ status: 405,
1462
+ description: "Can't add/remove user to group created by external auth provider."
1409
1463
  }
1410
1464
  ]
1411
1465
  },
@@ -1434,6 +1488,22 @@ var SASjsApi = (() => {
1434
1488
  {
1435
1489
  status: 200,
1436
1490
  description: "Ok"
1491
+ },
1492
+ {
1493
+ status: 400,
1494
+ description: "Can't add/remove user to 'public' group."
1495
+ },
1496
+ {
1497
+ status: 401,
1498
+ description: "Admin account required"
1499
+ },
1500
+ {
1501
+ status: 404,
1502
+ description: "Group not found. / User not found."
1503
+ },
1504
+ {
1505
+ status: 405,
1506
+ description: "Can't add/remove user to group created by external auth provider."
1437
1507
  }
1438
1508
  ]
1439
1509
  },
@@ -1456,6 +1526,14 @@ var SASjsApi = (() => {
1456
1526
  {
1457
1527
  status: 200,
1458
1528
  description: "Ok"
1529
+ },
1530
+ {
1531
+ status: 401,
1532
+ description: "Admin account required"
1533
+ },
1534
+ {
1535
+ status: 404,
1536
+ description: "Group not found."
1459
1537
  }
1460
1538
  ]
1461
1539
  },
@@ -1478,6 +1556,10 @@ var SASjsApi = (() => {
1478
1556
  {
1479
1557
  status: 200,
1480
1558
  description: "Ok"
1559
+ },
1560
+ {
1561
+ status: 404,
1562
+ description: "Group not found."
1481
1563
  }
1482
1564
  ]
1483
1565
  },
@@ -1500,6 +1582,10 @@ var SASjsApi = (() => {
1500
1582
  {
1501
1583
  status: 200,
1502
1584
  description: "Ok"
1585
+ },
1586
+ {
1587
+ status: 404,
1588
+ description: "Group not found."
1503
1589
  }
1504
1590
  ]
1505
1591
  },
@@ -1560,6 +1646,14 @@ var SASjsApi = (() => {
1560
1646
  {
1561
1647
  status: 200,
1562
1648
  description: "Ok"
1649
+ },
1650
+ {
1651
+ status: 401,
1652
+ description: "Admin account required, or no grant on the route"
1653
+ },
1654
+ {
1655
+ status: 404,
1656
+ description: "User not found."
1563
1657
  }
1564
1658
  ]
1565
1659
  },
@@ -1606,6 +1700,22 @@ var SASjsApi = (() => {
1606
1700
  {
1607
1701
  status: 200,
1608
1702
  description: "Ok"
1703
+ },
1704
+ {
1705
+ status: 400,
1706
+ description: "Can not add permission for admin user. / Invalid principal type. Valid types are user or group."
1707
+ },
1708
+ {
1709
+ status: 401,
1710
+ description: "Admin account required"
1711
+ },
1712
+ {
1713
+ status: 404,
1714
+ description: "User not found. / Group not found."
1715
+ },
1716
+ {
1717
+ status: 409,
1718
+ description: "Permission already exists with provided Path, Type and User. / Permission already exists with provided Path, Type and Group."
1609
1719
  }
1610
1720
  ]
1611
1721
  },
@@ -1628,6 +1738,14 @@ var SASjsApi = (() => {
1628
1738
  {
1629
1739
  status: 204,
1630
1740
  description: "No content"
1741
+ },
1742
+ {
1743
+ status: 401,
1744
+ description: "Admin account required"
1745
+ },
1746
+ {
1747
+ status: 404,
1748
+ description: "Permission not found."
1631
1749
  }
1632
1750
  ]
1633
1751
  },
@@ -1656,6 +1774,14 @@ var SASjsApi = (() => {
1656
1774
  {
1657
1775
  status: 200,
1658
1776
  description: "Ok"
1777
+ },
1778
+ {
1779
+ status: 401,
1780
+ description: "Admin account required"
1781
+ },
1782
+ {
1783
+ status: 404,
1784
+ description: "Permission not found."
1659
1785
  }
1660
1786
  ]
1661
1787
  },
@@ -1920,6 +2046,14 @@ var SASjsApi = (() => {
1920
2046
  {
1921
2047
  status: 200,
1922
2048
  description: "Ok"
2049
+ },
2050
+ {
2051
+ status: 401,
2052
+ description: "Admin account required"
2053
+ },
2054
+ {
2055
+ status: 409,
2056
+ description: "Username already exists."
1923
2057
  }
1924
2058
  ]
1925
2059
  },
@@ -1948,6 +2082,14 @@ var SASjsApi = (() => {
1948
2082
  {
1949
2083
  status: 204,
1950
2084
  description: "No content"
2085
+ },
2086
+ {
2087
+ status: 401,
2088
+ description: "Admin account required, or invalid password"
2089
+ },
2090
+ {
2091
+ status: 404,
2092
+ description: "User is not found."
1951
2093
  }
1952
2094
  ]
1953
2095
  },
@@ -1970,6 +2112,14 @@ var SASjsApi = (() => {
1970
2112
  {
1971
2113
  status: 200,
1972
2114
  description: "Ok"
2115
+ },
2116
+ {
2117
+ status: 401,
2118
+ description: "Unauthorized"
2119
+ },
2120
+ {
2121
+ status: 404,
2122
+ description: "User is not found."
1973
2123
  }
1974
2124
  ]
1975
2125
  },
@@ -2028,6 +2178,22 @@ var SASjsApi = (() => {
2028
2178
  {
2029
2179
  status: 200,
2030
2180
  description: "Ok"
2181
+ },
2182
+ {
2183
+ status: 401,
2184
+ description: "Admin account required"
2185
+ },
2186
+ {
2187
+ status: 404,
2188
+ description: "Unable to find user with <id>"
2189
+ },
2190
+ {
2191
+ status: 405,
2192
+ description: "Can not update username of user that is created by an external auth provider. / Can not update display name of user that is created by an external auth provider."
2193
+ },
2194
+ {
2195
+ status: 409,
2196
+ description: "Username already exists."
2031
2197
  }
2032
2198
  ]
2033
2199
  },
@@ -2056,6 +2222,14 @@ var SASjsApi = (() => {
2056
2222
  {
2057
2223
  status: 204,
2058
2224
  description: "No content"
2225
+ },
2226
+ {
2227
+ status: 401,
2228
+ description: "Admin account required, or invalid password"
2229
+ },
2230
+ {
2231
+ status: 404,
2232
+ description: "User is not found."
2059
2233
  }
2060
2234
  ]
2061
2235
  },
@@ -2078,6 +2252,14 @@ var SASjsApi = (() => {
2078
2252
  {
2079
2253
  status: 200,
2080
2254
  description: "Ok"
2255
+ },
2256
+ {
2257
+ status: 401,
2258
+ description: "Unauthorized"
2259
+ },
2260
+ {
2261
+ status: 404,
2262
+ description: "User is not found."
2081
2263
  }
2082
2264
  ]
2083
2265
  },
@@ -2136,6 +2318,22 @@ var SASjsApi = (() => {
2136
2318
  {
2137
2319
  status: 200,
2138
2320
  description: "Ok"
2321
+ },
2322
+ {
2323
+ status: 401,
2324
+ description: "Admin account required"
2325
+ },
2326
+ {
2327
+ status: 404,
2328
+ description: "Unable to find user with <username>"
2329
+ },
2330
+ {
2331
+ status: 405,
2332
+ description: "Can not update username of user that is created by an external auth provider. / Can not update display name of user that is created by an external auth provider."
2333
+ },
2334
+ {
2335
+ status: 409,
2336
+ description: "Username already exists."
2139
2337
  }
2140
2338
  ]
2141
2339
  },
@@ -2169,7 +2367,7 @@ var SASjsApi = (() => {
2169
2367
  },
2170
2368
  {
2171
2369
  status: 403,
2172
- description: "The client id is unknown"
2370
+ description: "The client id is unknown. This route runs under a signed-in session, so the caller is authenticated and an unknown client is refused as forbidden; the unauthenticated `POST /SASjsApi/auth/token` answers `401` for the same condition"
2173
2371
  }
2174
2372
  ]
2175
2373
  },
@@ -2254,7 +2452,7 @@ var SASjsApi = (() => {
2254
2452
  Client: ["identity"],
2255
2453
  Web: ["identity"]
2256
2454
  };
2257
- var IDENTITY_RECORDS = "The identity service answers the sign-in flow. User, group, permission rule, client and provider records need read and write methods on the same store, which this package does not have yet.";
2455
+ var DIRECTORY = "Synchronising with an external auth provider needs a directory port, which this package does not have: a host decides which directory it holds accounts in, and answers this itself.";
2258
2456
  var EDITOR = "Linting and formatting come from @sasjs/lint, which this package does not depend on. They land with the code routes so an editor has one wiring.";
2259
2457
  var RUNTIME = "Starting a program needs a runtime port, which this package does not have: a host decides which languages it holds, and answers this itself until then.";
2260
2458
  var SESSION_RECORD = "A triggered session's record, log, output and state belong to the runtime that ran the program, and this package has no runtime port.";
@@ -2309,18 +2507,20 @@ var SASjsApi = (() => {
2309
2507
  },
2310
2508
  "POST /SASjsApi/auth/logout": { implemented: true },
2311
2509
  "PATCH /SASjsApi/auth/updatePassword": {
2312
- implemented: false,
2313
- reason: IDENTITY_RECORDS
2510
+ implemented: true,
2511
+ // As with the permission list: the document's 404 is a caller whose record
2512
+ // has gone, which a host's store can see and this package cannot.
2513
+ hostResponses: [404]
2314
2514
  },
2315
2515
  // Auth Config - the providers a host was started with, and an LDAP import
2316
2516
  "GET /SASjsApi/authConfig": { implemented: false, reason: HOST_CONFIG },
2317
2517
  "POST /SASjsApi/authConfig/synchroniseWithLDAP": {
2318
2518
  implemented: false,
2319
- reason: IDENTITY_RECORDS
2519
+ reason: DIRECTORY
2320
2520
  },
2321
2521
  // Client - the OAuth clients the identity store already holds for sign-in
2322
- "GET /SASjsApi/client": { implemented: false, reason: IDENTITY_RECORDS },
2323
- "POST /SASjsApi/client": { implemented: false, reason: IDENTITY_RECORDS },
2522
+ "GET /SASjsApi/client": { implemented: true },
2523
+ "POST /SASjsApi/client": { implemented: true },
2324
2524
  // Code - the editor's three calls, and the runtime one
2325
2525
  "POST /SASjsApi/code/execute": { implemented: false, reason: RUNTIME },
2326
2526
  "POST /SASjsApi/code/trigger": { implemented: false, reason: RUNTIME },
@@ -2394,25 +2594,13 @@ var SASjsApi = (() => {
2394
2594
  reason: UNPUBLISH
2395
2595
  },
2396
2596
  // Group - members and the groups themselves
2397
- "GET /SASjsApi/group": { implemented: false, reason: IDENTITY_RECORDS },
2398
- "GET /SASjsApi/group/{uid}": { implemented: false, reason: IDENTITY_RECORDS },
2399
- "GET /SASjsApi/group/by/groupname/{name}": {
2400
- implemented: false,
2401
- reason: IDENTITY_RECORDS
2402
- },
2403
- "POST /SASjsApi/group": { implemented: false, reason: IDENTITY_RECORDS },
2404
- "POST /SASjsApi/group/{groupUid}/{userUid}": {
2405
- implemented: false,
2406
- reason: IDENTITY_RECORDS
2407
- },
2408
- "DELETE /SASjsApi/group/{groupUid}/{userUid}": {
2409
- implemented: false,
2410
- reason: IDENTITY_RECORDS
2411
- },
2412
- "DELETE /SASjsApi/group/{uid}": {
2413
- implemented: false,
2414
- reason: IDENTITY_RECORDS
2415
- },
2597
+ "GET /SASjsApi/group": { implemented: true },
2598
+ "GET /SASjsApi/group/{uid}": { implemented: true },
2599
+ "GET /SASjsApi/group/by/groupname/{name}": { implemented: true },
2600
+ "POST /SASjsApi/group": { implemented: true },
2601
+ "POST /SASjsApi/group/{groupUid}/{userUid}": { implemented: true },
2602
+ "DELETE /SASjsApi/group/{groupUid}/{userUid}": { implemented: true },
2603
+ "DELETE /SASjsApi/group/{uid}": { implemented: true },
2416
2604
  // Info - what the host is, and what this caller may use
2417
2605
  "GET /SASjsApi/info": { implemented: false, reason: HOST_CONFIG },
2418
2606
  "GET /SASjsApi/info/authorizedRoutes": {
@@ -2428,16 +2616,24 @@ var SASjsApi = (() => {
2428
2616
  reason: PERMISSIONS
2429
2617
  },
2430
2618
  // Permission - the rules the inventory above is derived from
2431
- "GET /SASjsApi/permission": { implemented: false, reason: IDENTITY_RECORDS },
2432
- "POST /SASjsApi/permission": { implemented: false, reason: IDENTITY_RECORDS },
2433
- "PATCH /SASjsApi/permission/{uid}": {
2434
- implemented: false,
2435
- reason: IDENTITY_RECORDS
2436
- },
2437
- "DELETE /SASjsApi/permission/{uid}": {
2438
- implemented: false,
2439
- reason: IDENTITY_RECORDS
2440
- },
2619
+ "GET /SASjsApi/permission": {
2620
+ implemented: true,
2621
+ // The document states 404 for a caller whose record has gone - a token that
2622
+ // outlived its account. Here a token stops resolving when the account does,
2623
+ // so this package answers 401 before it gets that far; the host's own store
2624
+ // is what can still see the difference.
2625
+ hostResponses: [404]
2626
+ },
2627
+ "POST /SASjsApi/permission": { implemented: true },
2628
+ "PATCH /SASjsApi/permission/{uid}": { implemented: true },
2629
+ // The three deletes answer 204 here and 200 on the wire: the server's route
2630
+ // handlers send `res.status(200).send('Account Deleted!')` and
2631
+ // `'Permission Deleted!'`, while tsoa emitted 204 for a void handler, so the
2632
+ // document understates the status. Correcting it is a decorator in
2633
+ // sasjs/server (`@SuccessResponse('200', ...)`), and this package follows the
2634
+ // document until that release lands - a local 200 would fail the schema check
2635
+ // that keeps this table honest.
2636
+ "DELETE /SASjsApi/permission/{uid}": { implemented: true },
2441
2637
  // Session - the caller's own session, then the record behind a triggered one
2442
2638
  "GET /SASjsApi/session": {
2443
2639
  implemented: true,
@@ -2464,29 +2660,26 @@ var SASjsApi = (() => {
2464
2660
  "POST /SASjsApi/stp/execute": { implemented: false, reason: RUNTIME },
2465
2661
  "POST /SASjsApi/stp/trigger": { implemented: false, reason: RUNTIME },
2466
2662
  // User - the accounts the identity store already holds for sign-in
2467
- "GET /SASjsApi/user": { implemented: false, reason: IDENTITY_RECORDS },
2468
- "GET /SASjsApi/user/{uid}": { implemented: false, reason: IDENTITY_RECORDS },
2469
- "GET /SASjsApi/user/by/username/{username}": {
2470
- implemented: false,
2471
- reason: IDENTITY_RECORDS
2472
- },
2473
- "POST /SASjsApi/user": { implemented: false, reason: IDENTITY_RECORDS },
2474
- "PATCH /SASjsApi/user/{uid}": {
2475
- implemented: false,
2476
- reason: IDENTITY_RECORDS
2477
- },
2478
- "PATCH /SASjsApi/user/by/username/{username}": {
2479
- implemented: false,
2480
- reason: IDENTITY_RECORDS
2481
- },
2482
- "DELETE /SASjsApi/user/{uid}": {
2483
- implemented: false,
2484
- reason: IDENTITY_RECORDS
2485
- },
2486
- "DELETE /SASjsApi/user/by/username/{username}": {
2487
- implemented: false,
2488
- reason: IDENTITY_RECORDS
2489
- }
2663
+ // Reading accounts is open to any authenticated caller, which is what
2664
+ // sasjs/server's routes do (`authenticateAccessToken` only): the document once
2665
+ // said `Admin account required` here, and sasjs/server#551 corrected it to
2666
+ // `Unauthorized` because that is the refusal a caller actually receives. The
2667
+ // breadth is the server's behaviour, copied deliberately - the auto-exec code
2668
+ // is the one field withheld from anyone but an administrator or the account
2669
+ // itself.
2670
+ "GET /SASjsApi/user": { implemented: true },
2671
+ "GET /SASjsApi/user/{uid}": { implemented: true },
2672
+ "GET /SASjsApi/user/by/username/{username}": { implemented: true },
2673
+ "POST /SASjsApi/user": { implemented: true },
2674
+ "PATCH /SASjsApi/user/{uid}": { implemented: true },
2675
+ "PATCH /SASjsApi/user/by/username/{username}": { implemented: true },
2676
+ // The document also marks `password` optional on these deletes, while the
2677
+ // server's schema requires it for anyone who is not an administrator, and for
2678
+ // an account deleting itself. A host should treat a passwordless delete from a
2679
+ // non-admin as the refusal it is; this package answers 401 `Invalid password.`
2680
+ // where the server's schema answers 400 `"password" is required`.
2681
+ "DELETE /SASjsApi/user/{uid}": { implemented: true },
2682
+ "DELETE /SASjsApi/user/by/username/{username}": { implemented: true }
2490
2683
  };
2491
2684
 
2492
2685
  // src/routes/index.ts
@@ -2578,6 +2771,7 @@ var SASjsApi = (() => {
2578
2771
  };
2579
2772
 
2580
2773
  // src/host/dispatch.ts
2774
+ var SECONDS_IN_A_DAY = 86400;
2581
2775
  var validate = (route, request) => {
2582
2776
  for (const param of route.params) {
2583
2777
  if (!param.required || param.in === "formData" || param.in === "header")
@@ -2598,10 +2792,31 @@ var SASjsApi = (() => {
2598
2792
  var dispatch = async (services, routeId, request = {}) => {
2599
2793
  const route = routeById(routeId);
2600
2794
  if (!route) throw new ApiError(404, `No such operation: ${routeId}`);
2795
+ const { drive, identity, records } = services;
2796
+ const identityScoped = ["User", "Group", "Permission", "Client"].includes(route.tag) || routeId === "PATCH /SASjsApi/auth/updatePassword";
2797
+ if (identityScoped && records) {
2798
+ const subject = request.subject ?? (identity ? await identity.authenticate(request.token) : void 0);
2799
+ if (!subject) {
2800
+ if (!identity)
2801
+ throw new ApiError(
2802
+ 501,
2803
+ `This host has no identity service, so it cannot answer ${routeId}.`
2804
+ );
2805
+ throw new ApiError(401, "Unauthorized");
2806
+ }
2807
+ }
2601
2808
  validate(route, request);
2602
- const { drive, identity } = services;
2603
2809
  const query = request.query || {};
2604
2810
  const body = request.body || {};
2811
+ const params = request.params || {};
2812
+ const required = () => {
2813
+ if (!records)
2814
+ throw new ApiError(
2815
+ 501,
2816
+ `This host has no record service, so it cannot answer ${routeId}.`
2817
+ );
2818
+ return records;
2819
+ };
2605
2820
  const caller = async () => {
2606
2821
  if (request.subject) return request.subject;
2607
2822
  return identity?.authenticate(request.token);
@@ -2693,6 +2908,169 @@ var SASjsApi = (() => {
2693
2908
  }
2694
2909
  case "GET /SASjsApi/drive/filetree":
2695
2910
  return { status: 200, body: await drive.tree(query._folderPath) };
2911
+ // ------------------------------------------------------------- records
2912
+ //
2913
+ // The administration routes: users, groups, permission rules, clients and the
2914
+ // caller's own password. Each handler resolves the caller and passes it on;
2915
+ // the admin gate and the wording live in the service.
2916
+ case "GET /SASjsApi/user":
2917
+ return { status: 200, body: await required().listUsers(await caller()) };
2918
+ case "GET /SASjsApi/user/{uid}":
2919
+ return {
2920
+ status: 200,
2921
+ body: await required().getUser(await caller(), {
2922
+ uid: String(params.uid ?? "")
2923
+ })
2924
+ };
2925
+ case "GET /SASjsApi/user/by/username/{username}":
2926
+ return {
2927
+ status: 200,
2928
+ body: await required().getUser(await caller(), {
2929
+ username: String(params.username ?? "")
2930
+ })
2931
+ };
2932
+ case "POST /SASjsApi/user":
2933
+ return {
2934
+ status: 200,
2935
+ body: await required().createUser(await caller(), {
2936
+ username: String(body.username ?? ""),
2937
+ displayName: String(body.displayName ?? ""),
2938
+ password: String(body.password ?? ""),
2939
+ isAdmin: body.isAdmin,
2940
+ isActive: body.isActive,
2941
+ autoExec: body.autoExec
2942
+ })
2943
+ };
2944
+ case "PATCH /SASjsApi/user/{uid}":
2945
+ return {
2946
+ status: 200,
2947
+ body: await required().updateUser(
2948
+ await caller(),
2949
+ { uid: String(params.uid ?? "") },
2950
+ body
2951
+ )
2952
+ };
2953
+ case "PATCH /SASjsApi/user/by/username/{username}":
2954
+ return {
2955
+ status: 200,
2956
+ body: await required().updateUser(
2957
+ await caller(),
2958
+ { username: String(params.username ?? "") },
2959
+ body
2960
+ )
2961
+ };
2962
+ case "DELETE /SASjsApi/user/{uid}":
2963
+ await required().deleteUser(
2964
+ await caller(),
2965
+ { uid: String(params.uid ?? "") },
2966
+ { password: body.password }
2967
+ );
2968
+ return { status: 204 };
2969
+ case "DELETE /SASjsApi/user/by/username/{username}":
2970
+ await required().deleteUser(
2971
+ await caller(),
2972
+ { username: String(params.username ?? "") },
2973
+ { password: body.password }
2974
+ );
2975
+ return { status: 204 };
2976
+ case "GET /SASjsApi/group":
2977
+ return { status: 200, body: await required().listGroups(await caller()) };
2978
+ case "GET /SASjsApi/group/{uid}":
2979
+ return {
2980
+ status: 200,
2981
+ body: await required().getGroup(await caller(), {
2982
+ uid: String(params.uid ?? "")
2983
+ })
2984
+ };
2985
+ case "GET /SASjsApi/group/by/groupname/{name}":
2986
+ return {
2987
+ status: 200,
2988
+ body: await required().getGroup(await caller(), {
2989
+ name: String(params.name ?? "")
2990
+ })
2991
+ };
2992
+ case "POST /SASjsApi/group":
2993
+ return {
2994
+ status: 200,
2995
+ body: await required().createGroup(await caller(), {
2996
+ name: String(body.name ?? ""),
2997
+ description: String(body.description ?? ""),
2998
+ isActive: body.isActive
2999
+ })
3000
+ };
3001
+ case "POST /SASjsApi/group/{groupUid}/{userUid}":
3002
+ return {
3003
+ status: 200,
3004
+ body: await required().addUserToGroup(
3005
+ await caller(),
3006
+ String(params.groupUid ?? ""),
3007
+ String(params.userUid ?? "")
3008
+ )
3009
+ };
3010
+ case "DELETE /SASjsApi/group/{groupUid}/{userUid}":
3011
+ return {
3012
+ status: 200,
3013
+ body: await required().removeUserFromGroup(
3014
+ await caller(),
3015
+ String(params.groupUid ?? ""),
3016
+ String(params.userUid ?? "")
3017
+ )
3018
+ };
3019
+ case "DELETE /SASjsApi/group/{uid}":
3020
+ await required().deleteGroup(await caller(), String(params.uid ?? ""));
3021
+ return { status: 200 };
3022
+ case "GET /SASjsApi/permission":
3023
+ return {
3024
+ status: 200,
3025
+ body: await required().listPermissions(await caller())
3026
+ };
3027
+ case "POST /SASjsApi/permission":
3028
+ return {
3029
+ status: 200,
3030
+ body: await required().createPermission(await caller(), {
3031
+ path: String(body.path ?? ""),
3032
+ type: String(body.type ?? ""),
3033
+ setting: String(body.setting ?? ""),
3034
+ principalType: String(body.principalType ?? ""),
3035
+ principalId: String(body.principalId ?? "")
3036
+ })
3037
+ };
3038
+ case "PATCH /SASjsApi/permission/{uid}":
3039
+ return {
3040
+ status: 200,
3041
+ body: await required().updatePermission(
3042
+ await caller(),
3043
+ String(params.uid ?? ""),
3044
+ { setting: String(body.setting ?? "") }
3045
+ )
3046
+ };
3047
+ case "DELETE /SASjsApi/permission/{uid}":
3048
+ await required().deletePermission(
3049
+ await caller(),
3050
+ String(params.uid ?? "")
3051
+ );
3052
+ return { status: 204 };
3053
+ case "GET /SASjsApi/client":
3054
+ return { status: 200, body: await required().listClients(await caller()) };
3055
+ case "POST /SASjsApi/client":
3056
+ return {
3057
+ status: 200,
3058
+ // An omitted lifetime is the schema's default, not NaN: the server's
3059
+ // Client model declares 86400 and 2592000, and a NaN lifetime would make
3060
+ // every token the client buys expire on arrival.
3061
+ body: await required().createClient(await caller(), {
3062
+ clientId: String(body.clientId ?? ""),
3063
+ clientSecret: String(body.clientSecret ?? ""),
3064
+ accessTokenExpiration: body.accessTokenExpiration === void 0 ? SECONDS_IN_A_DAY : Number(body.accessTokenExpiration),
3065
+ refreshTokenExpiration: body.refreshTokenExpiration === void 0 ? SECONDS_IN_A_DAY * 30 : Number(body.refreshTokenExpiration)
3066
+ })
3067
+ };
3068
+ case "PATCH /SASjsApi/auth/updatePassword":
3069
+ await required().updatePassword(await caller(), {
3070
+ currentPassword: String(body.currentPassword ?? ""),
3071
+ newPassword: String(body.newPassword ?? "")
3072
+ });
3073
+ return { status: 204 };
2696
3074
  default: {
2697
3075
  const reason = routeById(routeId)?.reason;
2698
3076
  throw new ApiError(
@@ -2719,7 +3097,29 @@ var SASjsApi = (() => {
2719
3097
  "POST /SASLogon/login",
2720
3098
  "POST /SASLogon/authorize",
2721
3099
  "GET /SASLogon/logout",
2722
- "GET /SASjsApi/session"
3100
+ "GET /SASjsApi/session",
3101
+ "GET /SASjsApi/user",
3102
+ "GET /SASjsApi/user/{uid}",
3103
+ "GET /SASjsApi/user/by/username/{username}",
3104
+ "POST /SASjsApi/user",
3105
+ "PATCH /SASjsApi/user/{uid}",
3106
+ "PATCH /SASjsApi/user/by/username/{username}",
3107
+ "DELETE /SASjsApi/user/{uid}",
3108
+ "DELETE /SASjsApi/user/by/username/{username}",
3109
+ "GET /SASjsApi/group",
3110
+ "GET /SASjsApi/group/{uid}",
3111
+ "GET /SASjsApi/group/by/groupname/{name}",
3112
+ "POST /SASjsApi/group",
3113
+ "POST /SASjsApi/group/{groupUid}/{userUid}",
3114
+ "DELETE /SASjsApi/group/{groupUid}/{userUid}",
3115
+ "DELETE /SASjsApi/group/{uid}",
3116
+ "GET /SASjsApi/permission",
3117
+ "POST /SASjsApi/permission",
3118
+ "PATCH /SASjsApi/permission/{uid}",
3119
+ "DELETE /SASjsApi/permission/{uid}",
3120
+ "GET /SASjsApi/client",
3121
+ "POST /SASjsApi/client",
3122
+ "PATCH /SASjsApi/auth/updatePassword"
2723
3123
  ];
2724
3124
 
2725
3125
  // src/identity/service.ts
@@ -2808,31 +3208,359 @@ var SASjsApi = (() => {
2808
3208
  needsToUpdatePassword: user3.needsToUpdatePassword
2809
3209
  });
2810
3210
 
2811
- // src/identity/fixture.ts
2812
- var IDENTITY_FORMAT = 1;
2813
- var SECONDS_IN_A_DAY = 86400;
2814
- var DEFAULT_IDENTITY = {
2815
- format: IDENTITY_FORMAT,
2816
- users: [
2817
- {
2818
- uid: "user-admin",
2819
- username: "admin",
2820
- displayName: "Demo Admin",
2821
- // long enough to pass the API's own password rule (six characters), so a
2822
- // demo can sign in against a real server as well as a mock
2823
- password: "admin123",
2824
- isAdmin: true,
2825
- isActive: true,
2826
- needsToUpdatePassword: false
2827
- },
2828
- {
2829
- uid: "user-restricted",
2830
- username: "user2",
2831
- displayName: "Demo User",
2832
- password: "user123",
2833
- isAdmin: false,
2834
- isActive: true,
2835
- needsToUpdatePassword: false
3211
+ // src/identity/records.ts
3212
+ var ALL_USERS_GROUP = "all-users";
3213
+ var PUBLIC_GROUP = "public";
3214
+ var unauthorized2 = () => new ApiError(401, "Unauthorized");
3215
+ var requireCaller = async (store, caller) => {
3216
+ if (!caller) throw unauthorized2();
3217
+ const user3 = await store.userById(caller.userId);
3218
+ if (!user3 || !user3.isActive) throw unauthorized2();
3219
+ return user3;
3220
+ };
3221
+ var requireAdmin = async (store, caller) => {
3222
+ const user3 = await requireCaller(store, caller);
3223
+ if (!user3.isAdmin) throw new ApiError(401, "Admin account required");
3224
+ return user3;
3225
+ };
3226
+ var requireSelfOrAdmin = async (store, caller, target) => {
3227
+ const user3 = await requireCaller(store, caller);
3228
+ if (!user3.isAdmin && user3.uid !== target?.uid)
3229
+ throw new ApiError(401, "Admin account required");
3230
+ return user3;
3231
+ };
3232
+ var MAX_DISPLAY_NAME_LENGTH = 100;
3233
+ var sanitiseDisplayName = (value) => String(value ?? "").replace(/[\u0000-\u001F\u007F]/g, "").trim().slice(0, MAX_DISPLAY_NAME_LENGTH);
3234
+ var asUser = (user3) => ({
3235
+ uid: user3.uid,
3236
+ username: user3.username,
3237
+ displayName: user3.displayName,
3238
+ isAdmin: user3.isAdmin
3239
+ });
3240
+ var asGroup = (group) => ({
3241
+ uid: group.uid,
3242
+ name: group.name,
3243
+ description: group.description
3244
+ });
3245
+ var createRecordService = (store) => {
3246
+ const groupDetails = async (group) => {
3247
+ const members = [];
3248
+ for (const uid of group.users) {
3249
+ const user3 = await store.userById(uid);
3250
+ if (user3) members.push(asUser(user3));
3251
+ }
3252
+ return { ...asGroup(group), isActive: group.isActive, users: members };
3253
+ };
3254
+ const userDetails = async (user3, withAutoExec) => {
3255
+ const groups = [];
3256
+ for (const uid of user3.groups) {
3257
+ const group = await store.groupById(uid);
3258
+ if (group) groups.push(asGroup(group));
3259
+ }
3260
+ const details = {
3261
+ ...asUser(user3),
3262
+ isActive: user3.isActive,
3263
+ needsToUpdatePassword: user3.needsToUpdatePassword,
3264
+ groups
3265
+ };
3266
+ if (withAutoExec) details.autoExec = user3.autoExec ?? "";
3267
+ return details;
3268
+ };
3269
+ const findBy = async (by) => by.uid !== void 0 ? store.userById(by.uid) : store.userByName(by.username ?? "");
3270
+ const permissionDetails = async (permission) => {
3271
+ const response = {
3272
+ uid: permission.uid,
3273
+ path: permission.path,
3274
+ type: permission.type,
3275
+ setting: permission.setting
3276
+ };
3277
+ if (permission.user) {
3278
+ const user3 = await store.userById(permission.user);
3279
+ if (user3) response.user = asUser(user3);
3280
+ }
3281
+ if (permission.group) {
3282
+ const group = await store.groupById(permission.group);
3283
+ if (group) response.group = await groupDetails(group);
3284
+ }
3285
+ return response;
3286
+ };
3287
+ return {
3288
+ async listUsers(caller) {
3289
+ await requireCaller(store, caller);
3290
+ const users = await store.users();
3291
+ return users.map(asUser);
3292
+ },
3293
+ async getUser(caller, by) {
3294
+ const reader = await requireCaller(store, caller);
3295
+ const user3 = await findBy(by);
3296
+ if (!user3) throw new ApiError(404, "User is not found.");
3297
+ const withAutoExec = reader.isAdmin || reader.uid === user3.uid;
3298
+ return userDetails(user3, withAutoExec);
3299
+ },
3300
+ async createUser(caller, body) {
3301
+ await requireAdmin(store, caller);
3302
+ if (await store.userByName(body.username))
3303
+ throw new ApiError(409, "Username already exists.");
3304
+ const created = await store.createUser({
3305
+ username: body.username,
3306
+ displayName: sanitiseDisplayName(body.displayName),
3307
+ isAdmin: body.isAdmin === true,
3308
+ isActive: body.isActive !== false,
3309
+ autoExec: body.autoExec
3310
+ });
3311
+ await store.setPassword(created.uid, body.password);
3312
+ const allUsers = await store.groupByName(ALL_USERS_GROUP);
3313
+ if (allUsers) await store.addToGroup(allUsers.uid, created.uid);
3314
+ const stored = await store.userById(created.uid);
3315
+ return userDetails(stored ?? created, true);
3316
+ },
3317
+ async updateUser(caller, by, body) {
3318
+ const current = await findBy(by);
3319
+ await requireSelfOrAdmin(store, caller, current);
3320
+ const change = {};
3321
+ const displayName = sanitiseDisplayName(body.displayName);
3322
+ if (body.username && body.username !== current?.username && current?.authProvider)
3323
+ throw new ApiError(
3324
+ 405,
3325
+ "Can not update username of user that is created by an external auth provider."
3326
+ );
3327
+ if (displayName && displayName !== current?.displayName && current?.authProvider)
3328
+ throw new ApiError(
3329
+ 405,
3330
+ "Can not update display name of user that is created by an external auth provider."
3331
+ );
3332
+ if (body.username) {
3333
+ const holder = await store.userByName(body.username);
3334
+ if (holder && holder.uid !== current?.uid)
3335
+ throw new ApiError(409, "Username already exists.");
3336
+ change.username = body.username;
3337
+ }
3338
+ if (displayName) change.displayName = displayName;
3339
+ if (body.isAdmin !== void 0) change.isAdmin = body.isAdmin;
3340
+ if (body.isActive !== void 0) change.isActive = body.isActive;
3341
+ if (body.autoExec !== void 0) change.autoExec = body.autoExec;
3342
+ const updated = current ? await store.updateUser(current.uid, change) : void 0;
3343
+ if (!updated)
3344
+ throw new ApiError(
3345
+ 404,
3346
+ `Unable to find user with ${by.uid || by.username}`
3347
+ );
3348
+ if (body.password) await store.setPassword(updated.uid, body.password);
3349
+ const stored = await store.userById(updated.uid);
3350
+ return userDetails(stored ?? updated, true);
3351
+ },
3352
+ async deleteUser(caller, by, body) {
3353
+ const current = await findBy(by);
3354
+ const actor = await requireSelfOrAdmin(store, caller, current);
3355
+ if (!current) throw new ApiError(404, "User is not found.");
3356
+ if (!actor.isAdmin) {
3357
+ if (!await store.passwordMatches(current, body.password ?? ""))
3358
+ throw new ApiError(401, "Invalid password.");
3359
+ }
3360
+ await store.deleteUser(current.uid);
3361
+ },
3362
+ async listGroups(caller) {
3363
+ await requireCaller(store, caller);
3364
+ const groups = await store.groups();
3365
+ return groups.map(asGroup);
3366
+ },
3367
+ async getGroup(caller, by) {
3368
+ await requireCaller(store, caller);
3369
+ const group = by.uid !== void 0 ? await store.groupById(by.uid) : await store.groupByName(by.name ?? "");
3370
+ if (!group) throw new ApiError(404, "Group not found.");
3371
+ return groupDetails(group);
3372
+ },
3373
+ async createGroup(caller, body) {
3374
+ await requireAdmin(store, caller);
3375
+ if (await store.groupByName(body.name))
3376
+ throw new ApiError(409, "Group name already exists.");
3377
+ const created = await store.createGroup({
3378
+ name: body.name,
3379
+ description: body.description,
3380
+ isActive: body.isActive !== false
3381
+ });
3382
+ return groupDetails(created);
3383
+ },
3384
+ async deleteGroup(caller, uid) {
3385
+ await requireAdmin(store, caller);
3386
+ if (!await store.groupById(uid))
3387
+ throw new ApiError(404, "Group not found.");
3388
+ await store.deleteGroup(uid);
3389
+ },
3390
+ async addUserToGroup(caller, groupUid, userUid) {
3391
+ await requireAdmin(store, caller);
3392
+ const group = await store.groupById(groupUid);
3393
+ if (!group) throw new ApiError(404, "Group not found.");
3394
+ if (group.name === PUBLIC_GROUP)
3395
+ throw new ApiError(
3396
+ 400,
3397
+ `Can't add/remove user to '${PUBLIC_GROUP}' group.`
3398
+ );
3399
+ if (group.authProvider)
3400
+ throw new ApiError(
3401
+ 405,
3402
+ "Can't add/remove user to group created by external auth provider."
3403
+ );
3404
+ const user3 = await store.userById(userUid);
3405
+ if (!user3) throw new ApiError(404, "User not found.");
3406
+ if (user3.authProvider)
3407
+ throw new ApiError(
3408
+ 405,
3409
+ "Can't add/remove user to group created by external auth provider."
3410
+ );
3411
+ const updated = await store.addToGroup(groupUid, userUid);
3412
+ if (!updated) throw new ApiError(400, "Unable to update group.");
3413
+ return groupDetails(updated);
3414
+ },
3415
+ async removeUserFromGroup(caller, groupUid, userUid) {
3416
+ await requireAdmin(store, caller);
3417
+ const group = await store.groupById(groupUid);
3418
+ if (!group) throw new ApiError(404, "Group not found.");
3419
+ if (group.name === PUBLIC_GROUP)
3420
+ throw new ApiError(
3421
+ 400,
3422
+ `Can't add/remove user to '${PUBLIC_GROUP}' group.`
3423
+ );
3424
+ if (group.authProvider)
3425
+ throw new ApiError(
3426
+ 405,
3427
+ "Can't add/remove user to group created by external auth provider."
3428
+ );
3429
+ const user3 = await store.userById(userUid);
3430
+ if (!user3) throw new ApiError(404, "User not found.");
3431
+ if (user3.authProvider)
3432
+ throw new ApiError(
3433
+ 405,
3434
+ "Can't add/remove user to group created by external auth provider."
3435
+ );
3436
+ const updated = await store.removeFromGroup(groupUid, userUid);
3437
+ if (!updated) throw new ApiError(400, "Unable to update group.");
3438
+ return groupDetails(updated);
3439
+ },
3440
+ async listPermissions(caller) {
3441
+ const reader = await requireCaller(store, caller);
3442
+ const rules = await store.permissions();
3443
+ const visible = reader.isAdmin ? rules : rules.filter(
3444
+ (rule) => rule.user === reader.uid || rule.group !== void 0 && reader.groups.includes(rule.group)
3445
+ );
3446
+ const out = [];
3447
+ for (const rule of visible) out.push(await permissionDetails(rule));
3448
+ return out;
3449
+ },
3450
+ async createPermission(caller, body) {
3451
+ await requireAdmin(store, caller);
3452
+ if (body.principalType === "user") {
3453
+ const user3 = await store.userById(body.principalId);
3454
+ if (!user3) throw new ApiError(404, "User not found.");
3455
+ if (user3.isAdmin)
3456
+ throw new ApiError(400, "Can not add permission for admin user.");
3457
+ const exists = (await store.permissions()).some(
3458
+ (rule) => rule.path === body.path && rule.type === body.type && rule.user === body.principalId
3459
+ );
3460
+ if (exists)
3461
+ throw new ApiError(
3462
+ 409,
3463
+ "Permission already exists with provided Path, Type and User."
3464
+ );
3465
+ const created = await store.createPermission({
3466
+ path: body.path,
3467
+ type: body.type,
3468
+ setting: body.setting,
3469
+ user: body.principalId
3470
+ });
3471
+ return permissionDetails(created);
3472
+ }
3473
+ if (body.principalType === "group") {
3474
+ const group = await store.groupById(body.principalId);
3475
+ if (!group) throw new ApiError(404, "Group not found.");
3476
+ const exists = (await store.permissions()).some(
3477
+ (rule) => rule.path === body.path && rule.type === body.type && rule.group === body.principalId
3478
+ );
3479
+ if (exists)
3480
+ throw new ApiError(
3481
+ 409,
3482
+ "Permission already exists with provided Path, Type and Group."
3483
+ );
3484
+ const created = await store.createPermission({
3485
+ path: body.path,
3486
+ type: body.type,
3487
+ setting: body.setting,
3488
+ group: body.principalId
3489
+ });
3490
+ return permissionDetails(created);
3491
+ }
3492
+ throw new ApiError(
3493
+ 400,
3494
+ "Invalid principal type. Valid types are user or group."
3495
+ );
3496
+ },
3497
+ async updatePermission(caller, uid, body) {
3498
+ await requireAdmin(store, caller);
3499
+ const updated = await store.setPermissionSetting(uid, body.setting);
3500
+ if (!updated) throw new ApiError(404, "Permission not found.");
3501
+ return permissionDetails(updated);
3502
+ },
3503
+ async deletePermission(caller, uid) {
3504
+ await requireAdmin(store, caller);
3505
+ if (!await store.permissionById(uid))
3506
+ throw new ApiError(404, "Permission not found.");
3507
+ await store.deletePermission(uid);
3508
+ },
3509
+ async listClients(caller) {
3510
+ await requireAdmin(store, caller);
3511
+ return store.clients();
3512
+ },
3513
+ async createClient(caller, body) {
3514
+ await requireAdmin(store, caller);
3515
+ if (await store.client(body.clientId))
3516
+ throw new ApiError(403, "Error: Client ID already exists.");
3517
+ return store.createClient({
3518
+ clientId: body.clientId,
3519
+ clientSecret: body.clientSecret,
3520
+ accessTokenExpiration: body.accessTokenExpiration,
3521
+ refreshTokenExpiration: body.refreshTokenExpiration
3522
+ });
3523
+ },
3524
+ async updatePassword(caller, body) {
3525
+ const reader = await requireCaller(store, caller);
3526
+ if (reader.authProvider)
3527
+ throw new ApiError(
3528
+ 405,
3529
+ "Can not update password of user that is created by an external auth provider."
3530
+ );
3531
+ if (!await store.passwordMatches(reader, body.currentPassword))
3532
+ throw new ApiError(403, "Invalid current password!");
3533
+ await store.setPassword(reader.uid, body.newPassword);
3534
+ await store.updateUser(reader.uid, { needsToUpdatePassword: false });
3535
+ }
3536
+ };
3537
+ };
3538
+
3539
+ // src/identity/fixture.ts
3540
+ var IDENTITY_FORMAT = 1;
3541
+ var SECONDS_IN_A_DAY2 = 86400;
3542
+ var DEFAULT_IDENTITY = {
3543
+ format: IDENTITY_FORMAT,
3544
+ users: [
3545
+ {
3546
+ uid: "user-admin",
3547
+ username: "admin",
3548
+ displayName: "Demo Admin",
3549
+ // long enough to pass the API's own password rule (six characters), so a
3550
+ // demo can sign in against a real server as well as a mock
3551
+ password: "admin123",
3552
+ isAdmin: true,
3553
+ isActive: true,
3554
+ needsToUpdatePassword: false
3555
+ },
3556
+ {
3557
+ uid: "user-restricted",
3558
+ username: "user2",
3559
+ displayName: "Demo User",
3560
+ password: "user123",
3561
+ isAdmin: false,
3562
+ isActive: true,
3563
+ needsToUpdatePassword: false
2836
3564
  }
2837
3565
  ],
2838
3566
  groups: [
@@ -2878,14 +3606,14 @@ var SASjsApi = (() => {
2878
3606
  {
2879
3607
  clientId: "web_app",
2880
3608
  clientSecret: "web_app",
2881
- accessTokenExpiration: SECONDS_IN_A_DAY,
2882
- refreshTokenExpiration: SECONDS_IN_A_DAY * 30
3609
+ accessTokenExpiration: SECONDS_IN_A_DAY2,
3610
+ refreshTokenExpiration: SECONDS_IN_A_DAY2 * 30
2883
3611
  },
2884
3612
  {
2885
3613
  clientId: "sasjs",
2886
3614
  clientSecret: "sasjs",
2887
3615
  accessTokenExpiration: 3600,
2888
- refreshTokenExpiration: SECONDS_IN_A_DAY
3616
+ refreshTokenExpiration: SECONDS_IN_A_DAY2
2889
3617
  }
2890
3618
  ]
2891
3619
  };
@@ -2931,9 +3659,12 @@ var SASjsApi = (() => {
2931
3659
  var live = (issued, now) => issued && (issued.expiresAt === void 0 || issued.expiresAt > now) ? issued.subject : void 0;
2932
3660
  var MemoryIdentityStore = class {
2933
3661
  constructor(fixture = DEFAULT_IDENTITY) {
2934
- this.users = [];
3662
+ this.userList = [];
2935
3663
  this.passwords = /* @__PURE__ */ new Map();
2936
- this.clients = /* @__PURE__ */ new Map();
3664
+ this.groupList = /* @__PURE__ */ new Map();
3665
+ this.permissionList = /* @__PURE__ */ new Map();
3666
+ this.clientRecords = /* @__PURE__ */ new Map();
3667
+ this.clientMap = /* @__PURE__ */ new Map();
2937
3668
  this.pairs = /* @__PURE__ */ new Map();
2938
3669
  this.access = /* @__PURE__ */ new Map();
2939
3670
  this.refresh = /* @__PURE__ */ new Map();
@@ -2945,24 +3676,56 @@ var SASjsApi = (() => {
2945
3676
  `The identity fixture states format ${String(fixture.format)}; this store reads ${IDENTITY_FORMAT}.`
2946
3677
  );
2947
3678
  for (const user3 of fixture.users) {
2948
- this.users.push({
3679
+ this.userList.push({
2949
3680
  uid: user3.uid,
2950
3681
  username: user3.username,
2951
3682
  displayName: user3.displayName,
2952
3683
  isAdmin: user3.isAdmin,
2953
3684
  isActive: user3.isActive,
2954
- needsToUpdatePassword: user3.needsToUpdatePassword
3685
+ needsToUpdatePassword: user3.needsToUpdatePassword,
3686
+ authProvider: user3.authProvider,
3687
+ autoExec: user3.autoExec
2955
3688
  });
2956
3689
  this.passwords.set(user3.uid, user3.password);
2957
3690
  }
2958
- for (const client of fixture.clients)
2959
- this.clients.set(client.clientId, {
3691
+ for (const client of fixture.clients) {
3692
+ this.clientMap.set(client.clientId, {
3693
+ clientId: client.clientId,
3694
+ accessTokenExpiration: client.accessTokenExpiration,
3695
+ refreshTokenExpiration: client.refreshTokenExpiration
3696
+ });
3697
+ this.clientRecords.set(client.clientId, {
2960
3698
  clientId: client.clientId,
3699
+ clientSecret: client.clientSecret,
2961
3700
  accessTokenExpiration: client.accessTokenExpiration,
2962
3701
  refreshTokenExpiration: client.refreshTokenExpiration
2963
3702
  });
3703
+ }
3704
+ for (const group of fixture.groups) {
3705
+ const uid = group.uid;
3706
+ this.groupList.set(uid, {
3707
+ uid,
3708
+ name: group.name,
3709
+ description: group.description,
3710
+ isActive: group.isActive,
3711
+ authProvider: group.authProvider,
3712
+ users: group.users.map(
3713
+ (username) => this.userList.find((user3) => user3.username === username)?.uid
3714
+ ).filter((member) => member !== void 0)
3715
+ });
3716
+ }
3717
+ for (const permission of fixture.permissions) {
3718
+ this.permissionList.set(permission.uid, {
3719
+ uid: permission.uid,
3720
+ path: permission.path,
3721
+ type: permission.type,
3722
+ setting: permission.setting,
3723
+ user: permission.user ? this.userList.find((user3) => user3.uid === permission.user)?.uid : void 0,
3724
+ group: permission.group ? this.groupList.get(permission.group)?.uid : void 0
3725
+ });
3726
+ }
2964
3727
  for (const token of fixture.tokens || []) {
2965
- const user3 = this.users.find(
3728
+ const user3 = this.userList.find(
2966
3729
  (candidate) => candidate.username === token.username
2967
3730
  );
2968
3731
  if (!user3) continue;
@@ -2976,7 +3739,7 @@ var SASjsApi = (() => {
2976
3739
  });
2977
3740
  }
2978
3741
  for (const code of fixture.codes || []) {
2979
- const user3 = this.users.find(
3742
+ const user3 = this.userList.find(
2980
3743
  (candidate) => candidate.username === code.username
2981
3744
  );
2982
3745
  if (!user3) continue;
@@ -2986,17 +3749,21 @@ var SASjsApi = (() => {
2986
3749
  }
2987
3750
  }
2988
3751
  async userByName(username) {
2989
- return this.users.find((user3) => user3.username === username);
3752
+ const user3 = this.userList.find(
3753
+ (candidate) => candidate.username === username
3754
+ );
3755
+ return user3 ? this.recordOf(user3) : void 0;
2990
3756
  }
2991
3757
  async userById(uid) {
2992
- return this.users.find((user3) => user3.uid === uid);
3758
+ const user3 = this.userList.find((candidate) => candidate.uid === uid);
3759
+ return user3 ? this.recordOf(user3) : void 0;
2993
3760
  }
2994
3761
  /** A fixture holds the password in clear, so the check is an equality. */
2995
3762
  async passwordMatches(user3, password) {
2996
3763
  return this.passwords.get(user3.uid) === password;
2997
3764
  }
2998
3765
  async client(clientId) {
2999
- return this.clients.get(clientId);
3766
+ return this.clientMap.get(clientId);
3000
3767
  }
3001
3768
  async issue(subject, client) {
3002
3769
  const issuedAt = Date.now();
@@ -3074,6 +3841,134 @@ var SASjsApi = (() => {
3074
3841
  this.claimed.add(code);
3075
3842
  return true;
3076
3843
  }
3844
+ // ---------------------------------------------------------------- records
3845
+ async users() {
3846
+ return this.userList.map((user3) => this.recordOf(user3));
3847
+ }
3848
+ /** A user as the administration routes report one: groups derived, not stored. */
3849
+ recordOf(user3) {
3850
+ return {
3851
+ ...user3,
3852
+ groups: [...this.groupList.values()].filter((group) => group.users.includes(user3.uid)).map((group) => group.uid)
3853
+ };
3854
+ }
3855
+ async createUser(input) {
3856
+ const user3 = {
3857
+ uid: `user-${randomHex(6)}`,
3858
+ username: input.username,
3859
+ displayName: input.displayName,
3860
+ isAdmin: input.isAdmin,
3861
+ isActive: input.isActive,
3862
+ needsToUpdatePassword: false,
3863
+ autoExec: input.autoExec,
3864
+ authProvider: input.authProvider
3865
+ };
3866
+ this.userList.push(user3);
3867
+ return this.recordOf(user3);
3868
+ }
3869
+ async updateUser(uid, change) {
3870
+ const user3 = this.userList.find((candidate) => candidate.uid === uid);
3871
+ if (!user3) return void 0;
3872
+ if (change.username !== void 0) user3.username = change.username;
3873
+ if (change.displayName !== void 0) user3.displayName = change.displayName;
3874
+ if (change.isAdmin !== void 0) user3.isAdmin = change.isAdmin;
3875
+ if (change.isActive !== void 0) user3.isActive = change.isActive;
3876
+ if (change.needsToUpdatePassword !== void 0)
3877
+ user3.needsToUpdatePassword = change.needsToUpdatePassword;
3878
+ if (change.autoExec !== void 0) user3.autoExec = change.autoExec;
3879
+ return this.recordOf(user3);
3880
+ }
3881
+ async deleteUser(uid) {
3882
+ const index = this.userList.findIndex((user3) => user3.uid === uid);
3883
+ if (index === -1) return;
3884
+ this.userList.splice(index, 1);
3885
+ this.passwords.delete(uid);
3886
+ for (const group of this.groupList.values())
3887
+ group.users = group.users.filter((member) => member !== uid);
3888
+ for (const [key, permission] of this.permissionList)
3889
+ if (permission.user === uid) this.permissionList.delete(key);
3890
+ for (const key of this.pairs.keys())
3891
+ if (key.startsWith(`${uid}|`)) this.pairs.delete(key);
3892
+ }
3893
+ async setPassword(uid, password) {
3894
+ this.passwords.set(uid, password);
3895
+ }
3896
+ async groups() {
3897
+ return [...this.groupList.values()];
3898
+ }
3899
+ async groupById(uid) {
3900
+ return this.groupList.get(uid);
3901
+ }
3902
+ async groupByName(name) {
3903
+ return [...this.groupList.values()].find((group) => group.name === name);
3904
+ }
3905
+ async createGroup(input) {
3906
+ const group = {
3907
+ uid: `group-${randomHex(6)}`,
3908
+ name: input.name,
3909
+ description: input.description,
3910
+ isActive: input.isActive,
3911
+ users: []
3912
+ };
3913
+ this.groupList.set(group.uid, group);
3914
+ return group;
3915
+ }
3916
+ async deleteGroup(uid) {
3917
+ this.groupList.delete(uid);
3918
+ for (const [key, permission] of this.permissionList)
3919
+ if (permission.group === uid) this.permissionList.delete(key);
3920
+ }
3921
+ async addToGroup(groupUid, userUid) {
3922
+ const group = this.groupList.get(groupUid);
3923
+ if (!group) return void 0;
3924
+ if (!group.users.includes(userUid)) group.users.push(userUid);
3925
+ return group;
3926
+ }
3927
+ async removeFromGroup(groupUid, userUid) {
3928
+ const group = this.groupList.get(groupUid);
3929
+ if (!group) return void 0;
3930
+ group.users = group.users.filter((member) => member !== userUid);
3931
+ return group;
3932
+ }
3933
+ async permissions() {
3934
+ return [...this.permissionList.values()];
3935
+ }
3936
+ async permissionById(uid) {
3937
+ return this.permissionList.get(uid);
3938
+ }
3939
+ async createPermission(input) {
3940
+ const permission = {
3941
+ uid: `rule-${randomHex(6)}`,
3942
+ path: input.path,
3943
+ type: input.type,
3944
+ setting: input.setting,
3945
+ user: input.user,
3946
+ group: input.group
3947
+ };
3948
+ this.permissionList.set(permission.uid, permission);
3949
+ return permission;
3950
+ }
3951
+ async setPermissionSetting(uid, setting) {
3952
+ const permission = this.permissionList.get(uid);
3953
+ if (!permission) return void 0;
3954
+ permission.setting = setting;
3955
+ return permission;
3956
+ }
3957
+ async deletePermission(uid) {
3958
+ this.permissionList.delete(uid);
3959
+ }
3960
+ async clients() {
3961
+ return [...this.clientRecords.values()];
3962
+ }
3963
+ async createClient(input) {
3964
+ this.clientRecords.set(input.clientId, input);
3965
+ this.clientMap.set(input.clientId, {
3966
+ clientId: input.clientId,
3967
+ accessTokenExpiration: input.accessTokenExpiration,
3968
+ refreshTokenExpiration: input.refreshTokenExpiration
3969
+ });
3970
+ return input;
3971
+ }
3077
3972
  };
3078
3973
 
3079
3974
  // src/corpus/drive.ts
@@ -3761,15 +4656,1233 @@ var SASjsApi = (() => {
3761
4656
  }
3762
4657
  ];
3763
4658
 
3764
- // src/corpus/runner.ts
3765
- var servicesFor = (testCase) => ({
3766
- drive: createDriveService(
3767
- testCase.unwritable ? new RefusingDriveStore(testCase.seed || {}) : new MemoryDriveStore(testCase.seed || {})
3768
- ),
3769
- identity: createIdentityService(
3770
- new MemoryIdentityStore(testCase.identity || DEFAULT_IDENTITY)
3771
- )
3772
- });
4659
+ // src/corpus/records.ts
4660
+ var SIGNED_IN_TOKENS = [
4661
+ {
4662
+ username: "admin",
4663
+ clientId: "web_app",
4664
+ accessToken: "admin-access",
4665
+ refreshToken: "admin-refresh"
4666
+ },
4667
+ {
4668
+ username: "user2",
4669
+ clientId: "web_app",
4670
+ accessToken: "user-access",
4671
+ refreshToken: "user-refresh"
4672
+ }
4673
+ ];
4674
+ var ADMIN_SIGNED_IN = {
4675
+ ...DEFAULT_IDENTITY,
4676
+ tokens: SIGNED_IN_TOKENS
4677
+ };
4678
+ var PROVIDER_MANAGED = {
4679
+ ...ADMIN_SIGNED_IN,
4680
+ tokens: [
4681
+ ...SIGNED_IN_TOKENS,
4682
+ {
4683
+ username: "diruser",
4684
+ clientId: "web_app",
4685
+ accessToken: "dir-access",
4686
+ refreshToken: "dir-refresh"
4687
+ }
4688
+ ],
4689
+ users: [
4690
+ ...ADMIN_SIGNED_IN.users,
4691
+ {
4692
+ uid: "user-directory",
4693
+ username: "diruser",
4694
+ displayName: "Directory User",
4695
+ password: "dir123",
4696
+ isAdmin: false,
4697
+ isActive: true,
4698
+ needsToUpdatePassword: false,
4699
+ authProvider: "oidc"
4700
+ }
4701
+ ],
4702
+ groups: [
4703
+ ...ADMIN_SIGNED_IN.groups,
4704
+ {
4705
+ uid: "group-directory",
4706
+ name: "Directory",
4707
+ description: "Managed by the directory",
4708
+ isActive: true,
4709
+ users: [],
4710
+ authProvider: "oidc"
4711
+ },
4712
+ {
4713
+ uid: "group-public",
4714
+ name: "public",
4715
+ description: "Everyone",
4716
+ isActive: true,
4717
+ users: []
4718
+ }
4719
+ ]
4720
+ };
4721
+ var ADMIN = "admin-access";
4722
+ var RESTRICTED = "user-access";
4723
+ var RESTRICTED_UID = "user-restricted";
4724
+ var ADMIN_UID = "user-admin";
4725
+ var RECORD_CORPUS = [
4726
+ // ------------------------------------------------------------------ users
4727
+ {
4728
+ id: "list-users",
4729
+ route: "GET /SASjsApi/user",
4730
+ description: "lists the accounts any signed-in caller may see",
4731
+ identity: ADMIN_SIGNED_IN,
4732
+ request: { token: ADMIN },
4733
+ expect: {
4734
+ status: 200,
4735
+ body: [
4736
+ {
4737
+ uid: ADMIN_UID,
4738
+ username: "admin",
4739
+ displayName: "Demo Admin",
4740
+ isAdmin: true
4741
+ },
4742
+ {
4743
+ uid: RESTRICTED_UID,
4744
+ username: "user2",
4745
+ displayName: "Demo User",
4746
+ isAdmin: false
4747
+ }
4748
+ ]
4749
+ }
4750
+ },
4751
+ {
4752
+ id: "get-user-self",
4753
+ route: "GET /SASjsApi/user/{uid}",
4754
+ description: "returns the caller their own details, with the auto-exec code",
4755
+ identity: ADMIN_SIGNED_IN,
4756
+ request: { token: RESTRICTED, params: { uid: RESTRICTED_UID } },
4757
+ expect: {
4758
+ status: 200,
4759
+ partial: {
4760
+ uid: RESTRICTED_UID,
4761
+ username: "user2",
4762
+ isActive: true,
4763
+ isAdmin: false,
4764
+ autoExec: ""
4765
+ }
4766
+ }
4767
+ },
4768
+ {
4769
+ id: "get-user-unauthenticated",
4770
+ route: "GET /SASjsApi/user/{uid}",
4771
+ description: "refuses a read with no caller at all",
4772
+ identity: ADMIN_SIGNED_IN,
4773
+ request: { params: { uid: RESTRICTED_UID } },
4774
+ expect: { status: 401, error: "Unauthorized" }
4775
+ },
4776
+ {
4777
+ id: "get-user-missing",
4778
+ route: "GET /SASjsApi/user/{uid}",
4779
+ description: "answers 404 for an identifier no account has",
4780
+ identity: ADMIN_SIGNED_IN,
4781
+ request: { token: ADMIN, params: { uid: "user-none" } },
4782
+ expect: { status: 404, error: "User is not found." }
4783
+ },
4784
+ {
4785
+ id: "get-user-by-username",
4786
+ route: "GET /SASjsApi/user/by/username/{username}",
4787
+ description: "finds an account by username",
4788
+ identity: ADMIN_SIGNED_IN,
4789
+ request: { token: ADMIN, params: { username: "user2" } },
4790
+ expect: {
4791
+ status: 200,
4792
+ partial: { uid: RESTRICTED_UID, username: "user2", isAdmin: false }
4793
+ }
4794
+ },
4795
+ {
4796
+ id: "get-user-by-username-unauthenticated",
4797
+ route: "GET /SASjsApi/user/by/username/{username}",
4798
+ description: "refuses the by-username read with no caller",
4799
+ identity: ADMIN_SIGNED_IN,
4800
+ request: { params: { username: "user2" } },
4801
+ expect: { status: 401, error: "Unauthorized" }
4802
+ },
4803
+ {
4804
+ id: "get-user-by-username-missing",
4805
+ route: "GET /SASjsApi/user/by/username/{username}",
4806
+ description: "answers 404 for a username no account has",
4807
+ identity: ADMIN_SIGNED_IN,
4808
+ request: { token: ADMIN, params: { username: "nobody" } },
4809
+ expect: { status: 404, error: "User is not found." }
4810
+ },
4811
+ {
4812
+ id: "create-user",
4813
+ route: "POST /SASjsApi/user",
4814
+ description: "creates an account, sanitised, and reports it",
4815
+ identity: ADMIN_SIGNED_IN,
4816
+ request: {
4817
+ token: ADMIN,
4818
+ body: {
4819
+ username: "newuser",
4820
+ displayName: " New User ",
4821
+ password: "newpass",
4822
+ isAdmin: false,
4823
+ isActive: true
4824
+ }
4825
+ },
4826
+ expect: {
4827
+ status: 200,
4828
+ partial: {
4829
+ username: "newuser",
4830
+ displayName: "New User",
4831
+ isAdmin: false,
4832
+ isActive: true
4833
+ }
4834
+ }
4835
+ },
4836
+ {
4837
+ id: "create-user-not-admin",
4838
+ route: "POST /SASjsApi/user",
4839
+ description: "refuses a creation by a caller who is not an administrator",
4840
+ identity: ADMIN_SIGNED_IN,
4841
+ request: {
4842
+ token: RESTRICTED,
4843
+ body: {
4844
+ username: "newuser",
4845
+ displayName: "New User",
4846
+ password: "newpass"
4847
+ }
4848
+ },
4849
+ expect: { status: 401, error: "Admin account required" }
4850
+ },
4851
+ {
4852
+ id: "create-user-anonymous-incomplete",
4853
+ route: "POST /SASjsApi/user",
4854
+ description: "answers 401 before the payload schema, anonymously and empty",
4855
+ identity: ADMIN_SIGNED_IN,
4856
+ request: { body: {} },
4857
+ expect: { status: 401, error: "Unauthorized" }
4858
+ },
4859
+ {
4860
+ id: "create-user-duplicate",
4861
+ route: "POST /SASjsApi/user",
4862
+ description: "refuses a username that is taken",
4863
+ identity: ADMIN_SIGNED_IN,
4864
+ request: {
4865
+ token: ADMIN,
4866
+ body: { username: "user2", displayName: "Another", password: "newpass" }
4867
+ },
4868
+ expect: { status: 409, error: "Username already exists." }
4869
+ },
4870
+ {
4871
+ id: "update-user",
4872
+ route: "PATCH /SASjsApi/user/{uid}",
4873
+ description: "changes an account's display name",
4874
+ identity: ADMIN_SIGNED_IN,
4875
+ request: {
4876
+ token: ADMIN,
4877
+ params: { uid: RESTRICTED_UID },
4878
+ body: {
4879
+ displayName: "Renamed User",
4880
+ username: "user2",
4881
+ password: "user123"
4882
+ }
4883
+ },
4884
+ expect: {
4885
+ status: 200,
4886
+ partial: { uid: RESTRICTED_UID, displayName: "Renamed User" }
4887
+ }
4888
+ },
4889
+ {
4890
+ id: "update-user-not-admin",
4891
+ route: "PATCH /SASjsApi/user/{uid}",
4892
+ description: "refuses a change to somebody else's account",
4893
+ identity: ADMIN_SIGNED_IN,
4894
+ request: {
4895
+ token: RESTRICTED,
4896
+ params: { uid: ADMIN_UID },
4897
+ body: { displayName: "Hijacked", username: "admin", password: "admin123" }
4898
+ },
4899
+ expect: { status: 401, error: "Admin account required" }
4900
+ },
4901
+ {
4902
+ id: "update-user-missing",
4903
+ route: "PATCH /SASjsApi/user/{uid}",
4904
+ description: "answers 404 with the identifier it looked up",
4905
+ identity: ADMIN_SIGNED_IN,
4906
+ request: {
4907
+ token: ADMIN,
4908
+ params: { uid: "user-none" },
4909
+ body: { displayName: "Nobody", username: "nobody", password: "none123" }
4910
+ },
4911
+ expect: { status: 404, error: "Unable to find user with user-none" }
4912
+ },
4913
+ {
4914
+ id: "update-user-provider",
4915
+ route: "PATCH /SASjsApi/user/{uid}",
4916
+ description: "refuses a username change on a provider-managed account",
4917
+ identity: PROVIDER_MANAGED,
4918
+ request: {
4919
+ token: ADMIN,
4920
+ params: { uid: "user-directory" },
4921
+ body: {
4922
+ username: "renamed",
4923
+ displayName: "Directory User",
4924
+ password: "dir123"
4925
+ }
4926
+ },
4927
+ expect: {
4928
+ status: 405,
4929
+ error: "Can not update username of user that is created by an external auth provider."
4930
+ }
4931
+ },
4932
+ {
4933
+ id: "update-user-username-taken",
4934
+ route: "PATCH /SASjsApi/user/{uid}",
4935
+ description: "refuses a username another account already has",
4936
+ identity: ADMIN_SIGNED_IN,
4937
+ request: {
4938
+ token: ADMIN,
4939
+ params: { uid: RESTRICTED_UID },
4940
+ body: { username: "admin", displayName: "Demo User", password: "user123" }
4941
+ },
4942
+ expect: { status: 409, error: "Username already exists." }
4943
+ },
4944
+ {
4945
+ id: "update-user-by-username",
4946
+ route: "PATCH /SASjsApi/user/by/username/{username}",
4947
+ description: "changes an account looked up by username",
4948
+ identity: ADMIN_SIGNED_IN,
4949
+ request: {
4950
+ token: ADMIN,
4951
+ params: { username: "user2" },
4952
+ body: {
4953
+ isActive: false,
4954
+ username: "user2",
4955
+ displayName: "Demo User",
4956
+ password: "user123"
4957
+ }
4958
+ },
4959
+ expect: { status: 200, partial: { username: "user2", isActive: false } }
4960
+ },
4961
+ {
4962
+ id: "update-user-by-username-not-admin",
4963
+ route: "PATCH /SASjsApi/user/by/username/{username}",
4964
+ description: "refuses a change to another account by username",
4965
+ identity: ADMIN_SIGNED_IN,
4966
+ request: {
4967
+ token: RESTRICTED,
4968
+ params: { username: "admin" },
4969
+ body: { displayName: "Hijacked", username: "admin", password: "admin123" }
4970
+ },
4971
+ expect: { status: 401, error: "Admin account required" }
4972
+ },
4973
+ {
4974
+ id: "update-user-by-username-missing",
4975
+ route: "PATCH /SASjsApi/user/by/username/{username}",
4976
+ description: "answers 404 with the username it looked up",
4977
+ identity: ADMIN_SIGNED_IN,
4978
+ request: {
4979
+ token: ADMIN,
4980
+ params: { username: "nobody" },
4981
+ body: { displayName: "Nobody", username: "nobody", password: "none123" }
4982
+ },
4983
+ expect: { status: 404, error: "Unable to find user with nobody" }
4984
+ },
4985
+ {
4986
+ id: "update-user-by-username-provider",
4987
+ route: "PATCH /SASjsApi/user/by/username/{username}",
4988
+ description: "refuses a display-name change on a provider-managed account",
4989
+ identity: PROVIDER_MANAGED,
4990
+ request: {
4991
+ token: ADMIN,
4992
+ params: { username: "diruser" },
4993
+ body: { displayName: "Renamed", username: "diruser", password: "dir123" }
4994
+ },
4995
+ expect: {
4996
+ status: 405,
4997
+ error: "Can not update display name of user that is created by an external auth provider."
4998
+ }
4999
+ },
5000
+ {
5001
+ id: "update-user-by-username-taken",
5002
+ route: "PATCH /SASjsApi/user/by/username/{username}",
5003
+ description: "refuses a username another account already has",
5004
+ identity: ADMIN_SIGNED_IN,
5005
+ request: {
5006
+ token: ADMIN,
5007
+ params: { username: "user2" },
5008
+ body: { username: "admin", displayName: "Demo User", password: "user123" }
5009
+ },
5010
+ expect: { status: 409, error: "Username already exists." }
5011
+ },
5012
+ {
5013
+ id: "delete-user",
5014
+ route: "DELETE /SASjsApi/user/{uid}",
5015
+ description: "deletes an account as an administrator, without a password",
5016
+ identity: ADMIN_SIGNED_IN,
5017
+ request: { token: ADMIN, params: { uid: RESTRICTED_UID }, body: {} },
5018
+ expect: { status: 204 }
5019
+ },
5020
+ {
5021
+ id: "delete-user-not-admin",
5022
+ route: "DELETE /SASjsApi/user/{uid}",
5023
+ description: "refuses a deletion of somebody else's account",
5024
+ identity: ADMIN_SIGNED_IN,
5025
+ request: { token: RESTRICTED, params: { uid: ADMIN_UID }, body: {} },
5026
+ expect: { status: 401, error: "Admin account required" }
5027
+ },
5028
+ {
5029
+ id: "delete-user-missing",
5030
+ route: "DELETE /SASjsApi/user/{uid}",
5031
+ description: "answers 404 for an identifier no account has",
5032
+ identity: ADMIN_SIGNED_IN,
5033
+ request: { token: ADMIN, params: { uid: "user-none" }, body: {} },
5034
+ expect: { status: 404, error: "User is not found." }
5035
+ },
5036
+ {
5037
+ id: "delete-user-by-username",
5038
+ route: "DELETE /SASjsApi/user/by/username/{username}",
5039
+ description: "deletes an account looked up by username",
5040
+ identity: ADMIN_SIGNED_IN,
5041
+ request: {
5042
+ token: ADMIN,
5043
+ params: { username: "user2" },
5044
+ body: {}
5045
+ },
5046
+ expect: { status: 204 }
5047
+ },
5048
+ {
5049
+ id: "delete-user-by-username-wrong-password",
5050
+ route: "DELETE /SASjsApi/user/by/username/{username}",
5051
+ description: "refuses a self-deletion whose password is wrong",
5052
+ identity: ADMIN_SIGNED_IN,
5053
+ request: {
5054
+ token: RESTRICTED,
5055
+ params: { username: "user2" },
5056
+ body: { password: "wrong" }
5057
+ },
5058
+ expect: { status: 401, error: "Invalid password." }
5059
+ },
5060
+ {
5061
+ id: "delete-user-by-username-missing",
5062
+ route: "DELETE /SASjsApi/user/by/username/{username}",
5063
+ description: "answers 404 for a username no account has",
5064
+ identity: ADMIN_SIGNED_IN,
5065
+ request: { token: ADMIN, params: { username: "nobody" }, body: {} },
5066
+ expect: { status: 404, error: "User is not found." }
5067
+ },
5068
+ // ----------------------------------------------------------------- groups
5069
+ {
5070
+ id: "list-groups",
5071
+ route: "GET /SASjsApi/group",
5072
+ description: "lists the groups",
5073
+ identity: ADMIN_SIGNED_IN,
5074
+ request: { token: RESTRICTED },
5075
+ expect: {
5076
+ status: 200,
5077
+ body: [
5078
+ {
5079
+ uid: "group-data-controller",
5080
+ name: "DCGroup",
5081
+ description: "Data Controller users"
5082
+ },
5083
+ {
5084
+ uid: "group-admins",
5085
+ name: "Admins",
5086
+ description: "Everything the API offers"
5087
+ }
5088
+ ]
5089
+ }
5090
+ },
5091
+ {
5092
+ id: "get-group",
5093
+ route: "GET /SASjsApi/group/{uid}",
5094
+ description: "returns a group and its members",
5095
+ identity: ADMIN_SIGNED_IN,
5096
+ request: { token: RESTRICTED, params: { uid: "group-admins" } },
5097
+ expect: {
5098
+ status: 200,
5099
+ partial: {
5100
+ uid: "group-admins",
5101
+ name: "Admins",
5102
+ isActive: true,
5103
+ users: [
5104
+ {
5105
+ uid: ADMIN_UID,
5106
+ username: "admin",
5107
+ displayName: "Demo Admin",
5108
+ isAdmin: true
5109
+ }
5110
+ ]
5111
+ }
5112
+ }
5113
+ },
5114
+ {
5115
+ id: "get-group-missing",
5116
+ route: "GET /SASjsApi/group/{uid}",
5117
+ description: "answers 404 for an identifier no group has",
5118
+ identity: ADMIN_SIGNED_IN,
5119
+ request: { token: ADMIN, params: { uid: "group-none" } },
5120
+ expect: { status: 404, error: "Group not found." }
5121
+ },
5122
+ {
5123
+ id: "get-group-by-name",
5124
+ route: "GET /SASjsApi/group/by/groupname/{name}",
5125
+ description: "finds a group by name",
5126
+ identity: ADMIN_SIGNED_IN,
5127
+ request: { token: ADMIN, params: { name: "DCGroup" } },
5128
+ expect: {
5129
+ status: 200,
5130
+ partial: { uid: "group-data-controller", name: "DCGroup" }
5131
+ }
5132
+ },
5133
+ {
5134
+ id: "get-group-by-name-missing",
5135
+ route: "GET /SASjsApi/group/by/groupname/{name}",
5136
+ description: "answers 404 for a name no group has",
5137
+ identity: ADMIN_SIGNED_IN,
5138
+ request: { token: ADMIN, params: { name: "nope" } },
5139
+ expect: { status: 404, error: "Group not found." }
5140
+ },
5141
+ {
5142
+ id: "create-group",
5143
+ route: "POST /SASjsApi/group",
5144
+ description: "creates a group with no members",
5145
+ identity: ADMIN_SIGNED_IN,
5146
+ request: {
5147
+ token: ADMIN,
5148
+ body: { name: "NewGroup", description: "A new group", isActive: true }
5149
+ },
5150
+ expect: {
5151
+ status: 200,
5152
+ partial: {
5153
+ name: "NewGroup",
5154
+ description: "A new group",
5155
+ isActive: true,
5156
+ users: []
5157
+ }
5158
+ }
5159
+ },
5160
+ {
5161
+ id: "create-group-not-admin",
5162
+ route: "POST /SASjsApi/group",
5163
+ description: "refuses a creation by a caller who is not an administrator",
5164
+ identity: ADMIN_SIGNED_IN,
5165
+ request: {
5166
+ token: RESTRICTED,
5167
+ body: { name: "NewGroup", description: "x" }
5168
+ },
5169
+ expect: { status: 401, error: "Admin account required" }
5170
+ },
5171
+ {
5172
+ id: "create-group-duplicate",
5173
+ route: "POST /SASjsApi/group",
5174
+ description: "refuses a name that is taken",
5175
+ identity: ADMIN_SIGNED_IN,
5176
+ request: { token: ADMIN, body: { name: "DCGroup", description: "again" } },
5177
+ expect: { status: 409, error: "Group name already exists." }
5178
+ },
5179
+ {
5180
+ id: "add-user-to-group",
5181
+ route: "POST /SASjsApi/group/{groupUid}/{userUid}",
5182
+ description: "adds a member and returns the group",
5183
+ identity: ADMIN_SIGNED_IN,
5184
+ request: {
5185
+ token: ADMIN,
5186
+ params: { groupUid: "group-admins", userUid: RESTRICTED_UID }
5187
+ },
5188
+ expect: {
5189
+ status: 200,
5190
+ partial: { uid: "group-admins", name: "Admins" }
5191
+ }
5192
+ },
5193
+ {
5194
+ id: "add-user-to-public-group",
5195
+ route: "POST /SASjsApi/group/{groupUid}/{userUid}",
5196
+ description: "refuses a change to the reserved public group",
5197
+ identity: PROVIDER_MANAGED,
5198
+ request: {
5199
+ token: ADMIN,
5200
+ params: { groupUid: "group-public", userUid: RESTRICTED_UID }
5201
+ },
5202
+ expect: { status: 400, error: "Can't add/remove user to 'public' group." }
5203
+ },
5204
+ {
5205
+ id: "add-user-to-group-not-admin",
5206
+ route: "POST /SASjsApi/group/{groupUid}/{userUid}",
5207
+ description: "refuses a membership change by a non-administrator",
5208
+ identity: ADMIN_SIGNED_IN,
5209
+ request: {
5210
+ token: RESTRICTED,
5211
+ params: { groupUid: "group-admins", userUid: RESTRICTED_UID }
5212
+ },
5213
+ expect: { status: 401, error: "Admin account required" }
5214
+ },
5215
+ {
5216
+ id: "add-user-to-group-missing-group",
5217
+ route: "POST /SASjsApi/group/{groupUid}/{userUid}",
5218
+ description: "answers 404 for a group that does not exist",
5219
+ identity: ADMIN_SIGNED_IN,
5220
+ request: {
5221
+ token: ADMIN,
5222
+ params: { groupUid: "group-none", userUid: RESTRICTED_UID }
5223
+ },
5224
+ expect: { status: 404, error: "Group not found." }
5225
+ },
5226
+ {
5227
+ id: "add-user-to-group-provider",
5228
+ route: "POST /SASjsApi/group/{groupUid}/{userUid}",
5229
+ description: "refuses a membership change on a provider-managed group",
5230
+ identity: PROVIDER_MANAGED,
5231
+ request: {
5232
+ token: ADMIN,
5233
+ params: { groupUid: "group-directory", userUid: RESTRICTED_UID }
5234
+ },
5235
+ expect: {
5236
+ status: 405,
5237
+ error: "Can't add/remove user to group created by external auth provider."
5238
+ }
5239
+ },
5240
+ {
5241
+ id: "remove-user-from-group",
5242
+ route: "DELETE /SASjsApi/group/{groupUid}/{userUid}",
5243
+ description: "removes a member and returns the group",
5244
+ identity: ADMIN_SIGNED_IN,
5245
+ request: {
5246
+ token: ADMIN,
5247
+ params: { groupUid: "group-data-controller", userUid: RESTRICTED_UID }
5248
+ },
5249
+ expect: {
5250
+ status: 200,
5251
+ partial: { uid: "group-data-controller", name: "DCGroup", users: [] }
5252
+ }
5253
+ },
5254
+ {
5255
+ id: "add-provider-user-to-group",
5256
+ route: "POST /SASjsApi/group/{groupUid}/{userUid}",
5257
+ description: "refuses a membership change for a provider-managed user",
5258
+ identity: PROVIDER_MANAGED,
5259
+ request: {
5260
+ token: ADMIN,
5261
+ params: { groupUid: "group-admins", userUid: "user-directory" }
5262
+ },
5263
+ expect: {
5264
+ status: 405,
5265
+ error: "Can't add/remove user to group created by external auth provider."
5266
+ }
5267
+ },
5268
+ {
5269
+ id: "remove-provider-user-from-group",
5270
+ route: "DELETE /SASjsApi/group/{groupUid}/{userUid}",
5271
+ description: "refuses a membership change for a provider-managed user",
5272
+ identity: PROVIDER_MANAGED,
5273
+ request: {
5274
+ token: ADMIN,
5275
+ params: { groupUid: "group-admins", userUid: "user-directory" }
5276
+ },
5277
+ expect: {
5278
+ status: 405,
5279
+ error: "Can't add/remove user to group created by external auth provider."
5280
+ }
5281
+ },
5282
+ {
5283
+ id: "remove-user-from-public-group",
5284
+ route: "DELETE /SASjsApi/group/{groupUid}/{userUid}",
5285
+ description: "refuses a change to the reserved public group",
5286
+ identity: PROVIDER_MANAGED,
5287
+ request: {
5288
+ token: ADMIN,
5289
+ params: { groupUid: "group-public", userUid: RESTRICTED_UID }
5290
+ },
5291
+ expect: { status: 400, error: "Can't add/remove user to 'public' group." }
5292
+ },
5293
+ {
5294
+ id: "remove-user-from-group-not-admin",
5295
+ route: "DELETE /SASjsApi/group/{groupUid}/{userUid}",
5296
+ description: "refuses a membership change by a non-administrator",
5297
+ identity: ADMIN_SIGNED_IN,
5298
+ request: {
5299
+ token: RESTRICTED,
5300
+ params: { groupUid: "group-data-controller", userUid: RESTRICTED_UID }
5301
+ },
5302
+ expect: { status: 401, error: "Admin account required" }
5303
+ },
5304
+ {
5305
+ id: "remove-user-from-group-missing-user",
5306
+ route: "DELETE /SASjsApi/group/{groupUid}/{userUid}",
5307
+ description: "answers 404 for a member that does not exist",
5308
+ identity: ADMIN_SIGNED_IN,
5309
+ request: {
5310
+ token: ADMIN,
5311
+ params: { groupUid: "group-admins", userUid: "user-none" }
5312
+ },
5313
+ expect: { status: 404, error: "User not found." }
5314
+ },
5315
+ {
5316
+ id: "remove-user-from-group-provider",
5317
+ route: "DELETE /SASjsApi/group/{groupUid}/{userUid}",
5318
+ description: "refuses a membership change on a provider-managed group",
5319
+ identity: PROVIDER_MANAGED,
5320
+ request: {
5321
+ token: ADMIN,
5322
+ params: { groupUid: "group-directory", userUid: RESTRICTED_UID }
5323
+ },
5324
+ expect: {
5325
+ status: 405,
5326
+ error: "Can't add/remove user to group created by external auth provider."
5327
+ }
5328
+ },
5329
+ {
5330
+ id: "delete-group",
5331
+ route: "DELETE /SASjsApi/group/{uid}",
5332
+ description: "deletes a group",
5333
+ identity: ADMIN_SIGNED_IN,
5334
+ request: { token: ADMIN, params: { uid: "group-data-controller" } },
5335
+ expect: { status: 200 }
5336
+ },
5337
+ {
5338
+ id: "delete-group-not-admin",
5339
+ route: "DELETE /SASjsApi/group/{uid}",
5340
+ description: "refuses a deletion by a caller who is not an administrator",
5341
+ identity: ADMIN_SIGNED_IN,
5342
+ request: { token: RESTRICTED, params: { uid: "group-admins" } },
5343
+ expect: { status: 401, error: "Admin account required" }
5344
+ },
5345
+ {
5346
+ id: "delete-group-missing",
5347
+ route: "DELETE /SASjsApi/group/{uid}",
5348
+ description: "answers 404 for an identifier no group has",
5349
+ identity: ADMIN_SIGNED_IN,
5350
+ request: { token: ADMIN, params: { uid: "group-none" } },
5351
+ expect: { status: 404, error: "Group not found." }
5352
+ },
5353
+ // ------------------------------------------------------------ permissions
5354
+ {
5355
+ id: "list-permissions-as-admin",
5356
+ route: "GET /SASjsApi/permission",
5357
+ description: "returns every rule to an administrator",
5358
+ identity: ADMIN_SIGNED_IN,
5359
+ request: { token: ADMIN },
5360
+ expect: {
5361
+ status: 200,
5362
+ body: [
5363
+ {
5364
+ uid: "rule-routes",
5365
+ path: "/SASjsApi/info",
5366
+ type: "Route",
5367
+ setting: "Grant",
5368
+ group: {
5369
+ uid: "group-data-controller",
5370
+ name: "DCGroup",
5371
+ description: "Data Controller users",
5372
+ isActive: true,
5373
+ users: [
5374
+ {
5375
+ uid: RESTRICTED_UID,
5376
+ username: "user2",
5377
+ displayName: "Demo User",
5378
+ isAdmin: false
5379
+ }
5380
+ ]
5381
+ }
5382
+ },
5383
+ {
5384
+ uid: "rule-execute",
5385
+ path: "/SASjsApi/stp/execute",
5386
+ type: "Route",
5387
+ setting: "Grant",
5388
+ group: {
5389
+ uid: "group-data-controller",
5390
+ name: "DCGroup",
5391
+ description: "Data Controller users",
5392
+ isActive: true,
5393
+ users: [
5394
+ {
5395
+ uid: RESTRICTED_UID,
5396
+ username: "user2",
5397
+ displayName: "Demo User",
5398
+ isAdmin: false
5399
+ }
5400
+ ]
5401
+ }
5402
+ },
5403
+ {
5404
+ uid: "rule-code",
5405
+ path: "/SASjsApi/code/execute",
5406
+ type: "Route",
5407
+ setting: "Deny",
5408
+ user: {
5409
+ uid: RESTRICTED_UID,
5410
+ username: "user2",
5411
+ displayName: "Demo User",
5412
+ isAdmin: false
5413
+ }
5414
+ }
5415
+ ]
5416
+ }
5417
+ },
5418
+ {
5419
+ id: "list-permissions-as-user",
5420
+ route: "GET /SASjsApi/permission",
5421
+ description: "returns a caller only their own rules and their groups'",
5422
+ identity: ADMIN_SIGNED_IN,
5423
+ request: { token: RESTRICTED },
5424
+ expect: {
5425
+ status: 200,
5426
+ body: [
5427
+ {
5428
+ uid: "rule-routes",
5429
+ path: "/SASjsApi/info",
5430
+ type: "Route",
5431
+ setting: "Grant",
5432
+ group: {
5433
+ uid: "group-data-controller",
5434
+ name: "DCGroup",
5435
+ description: "Data Controller users",
5436
+ isActive: true,
5437
+ users: [
5438
+ {
5439
+ uid: RESTRICTED_UID,
5440
+ username: "user2",
5441
+ displayName: "Demo User",
5442
+ isAdmin: false
5443
+ }
5444
+ ]
5445
+ }
5446
+ },
5447
+ {
5448
+ uid: "rule-execute",
5449
+ path: "/SASjsApi/stp/execute",
5450
+ type: "Route",
5451
+ setting: "Grant",
5452
+ group: {
5453
+ uid: "group-data-controller",
5454
+ name: "DCGroup",
5455
+ description: "Data Controller users",
5456
+ isActive: true,
5457
+ users: [
5458
+ {
5459
+ uid: RESTRICTED_UID,
5460
+ username: "user2",
5461
+ displayName: "Demo User",
5462
+ isAdmin: false
5463
+ }
5464
+ ]
5465
+ }
5466
+ },
5467
+ {
5468
+ uid: "rule-code",
5469
+ path: "/SASjsApi/code/execute",
5470
+ type: "Route",
5471
+ setting: "Deny",
5472
+ user: {
5473
+ uid: RESTRICTED_UID,
5474
+ username: "user2",
5475
+ displayName: "Demo User",
5476
+ isAdmin: false
5477
+ }
5478
+ }
5479
+ ]
5480
+ }
5481
+ },
5482
+ {
5483
+ id: "list-permissions-unauthenticated",
5484
+ route: "GET /SASjsApi/permission",
5485
+ description: "refuses the list with no caller",
5486
+ identity: ADMIN_SIGNED_IN,
5487
+ request: {},
5488
+ expect: { status: 401, error: "Unauthorized" }
5489
+ },
5490
+ {
5491
+ id: "create-permission-user",
5492
+ route: "POST /SASjsApi/permission",
5493
+ description: "creates a rule for a user",
5494
+ identity: ADMIN_SIGNED_IN,
5495
+ request: {
5496
+ token: ADMIN,
5497
+ body: {
5498
+ path: "/SASjsApi/code/lint",
5499
+ type: "Route",
5500
+ setting: "Grant",
5501
+ principalType: "user",
5502
+ principalId: RESTRICTED_UID
5503
+ }
5504
+ },
5505
+ expect: {
5506
+ status: 200,
5507
+ partial: { path: "/SASjsApi/code/lint", setting: "Grant" }
5508
+ }
5509
+ },
5510
+ {
5511
+ id: "create-permission-group",
5512
+ route: "POST /SASjsApi/permission",
5513
+ description: "creates a rule for a group",
5514
+ identity: ADMIN_SIGNED_IN,
5515
+ request: {
5516
+ token: ADMIN,
5517
+ body: {
5518
+ path: "/SASjsApi/code/lint",
5519
+ type: "Route",
5520
+ setting: "Grant",
5521
+ principalType: "group",
5522
+ principalId: "group-data-controller"
5523
+ }
5524
+ },
5525
+ expect: {
5526
+ status: 200,
5527
+ partial: {
5528
+ path: "/SASjsApi/code/lint",
5529
+ setting: "Grant",
5530
+ group: { uid: "group-data-controller", name: "DCGroup" }
5531
+ }
5532
+ }
5533
+ },
5534
+ {
5535
+ id: "create-permission-invalid-principal-type",
5536
+ route: "POST /SASjsApi/permission",
5537
+ description: "refuses a principal type that is neither a user nor a group",
5538
+ identity: ADMIN_SIGNED_IN,
5539
+ request: {
5540
+ token: ADMIN,
5541
+ body: {
5542
+ path: "/SASjsApi/code/lint",
5543
+ type: "Route",
5544
+ setting: "Grant",
5545
+ principalType: "role",
5546
+ principalId: "anything"
5547
+ }
5548
+ },
5549
+ expect: {
5550
+ status: 400,
5551
+ error: "Invalid principal type. Valid types are user or group."
5552
+ }
5553
+ },
5554
+ {
5555
+ id: "create-permission-admin-principal",
5556
+ route: "POST /SASjsApi/permission",
5557
+ description: "refuses a rule assigned to an administrator",
5558
+ identity: ADMIN_SIGNED_IN,
5559
+ request: {
5560
+ token: ADMIN,
5561
+ body: {
5562
+ path: "/SASjsApi/code/lint",
5563
+ type: "Route",
5564
+ setting: "Grant",
5565
+ principalType: "user",
5566
+ principalId: ADMIN_UID
5567
+ }
5568
+ },
5569
+ expect: {
5570
+ status: 400,
5571
+ error: "Can not add permission for admin user."
5572
+ }
5573
+ },
5574
+ {
5575
+ id: "create-permission-not-admin",
5576
+ route: "POST /SASjsApi/permission",
5577
+ description: "refuses a creation by a non-administrator",
5578
+ identity: ADMIN_SIGNED_IN,
5579
+ request: {
5580
+ token: RESTRICTED,
5581
+ body: {
5582
+ path: "/SASjsApi/code/lint",
5583
+ type: "Route",
5584
+ setting: "Grant",
5585
+ principalType: "user",
5586
+ principalId: RESTRICTED_UID
5587
+ }
5588
+ },
5589
+ expect: { status: 401, error: "Admin account required" }
5590
+ },
5591
+ {
5592
+ id: "create-permission-missing-principal",
5593
+ route: "POST /SASjsApi/permission",
5594
+ description: "answers 404 for a principal that does not exist",
5595
+ identity: ADMIN_SIGNED_IN,
5596
+ request: {
5597
+ token: ADMIN,
5598
+ body: {
5599
+ path: "/SASjsApi/code/lint",
5600
+ type: "Route",
5601
+ setting: "Grant",
5602
+ principalType: "group",
5603
+ principalId: "group-none"
5604
+ }
5605
+ },
5606
+ expect: { status: 404, error: "Group not found." }
5607
+ },
5608
+ {
5609
+ id: "create-permission-duplicate-group",
5610
+ route: "POST /SASjsApi/permission",
5611
+ description: "refuses a group rule that already exists",
5612
+ identity: ADMIN_SIGNED_IN,
5613
+ request: {
5614
+ token: ADMIN,
5615
+ body: {
5616
+ path: "/SASjsApi/info",
5617
+ type: "Route",
5618
+ setting: "Grant",
5619
+ principalType: "group",
5620
+ principalId: "group-data-controller"
5621
+ }
5622
+ },
5623
+ expect: {
5624
+ status: 409,
5625
+ error: "Permission already exists with provided Path, Type and Group."
5626
+ }
5627
+ },
5628
+ {
5629
+ id: "create-permission-duplicate",
5630
+ route: "POST /SASjsApi/permission",
5631
+ description: "refuses a rule that already exists",
5632
+ identity: ADMIN_SIGNED_IN,
5633
+ request: {
5634
+ token: ADMIN,
5635
+ body: {
5636
+ path: "/SASjsApi/code/execute",
5637
+ type: "Route",
5638
+ setting: "Deny",
5639
+ principalType: "user",
5640
+ principalId: RESTRICTED_UID
5641
+ }
5642
+ },
5643
+ expect: {
5644
+ status: 409,
5645
+ error: "Permission already exists with provided Path, Type and User."
5646
+ }
5647
+ },
5648
+ {
5649
+ id: "update-permission",
5650
+ route: "PATCH /SASjsApi/permission/{uid}",
5651
+ description: "flips a rule from Deny to Grant",
5652
+ identity: ADMIN_SIGNED_IN,
5653
+ request: {
5654
+ token: ADMIN,
5655
+ params: { uid: "rule-code" },
5656
+ body: { setting: "Grant" }
5657
+ },
5658
+ expect: { status: 200, partial: { uid: "rule-code", setting: "Grant" } }
5659
+ },
5660
+ {
5661
+ id: "update-permission-not-admin",
5662
+ route: "PATCH /SASjsApi/permission/{uid}",
5663
+ description: "refuses a change by a non-administrator",
5664
+ identity: ADMIN_SIGNED_IN,
5665
+ request: {
5666
+ token: RESTRICTED,
5667
+ params: { uid: "rule-code" },
5668
+ body: { setting: "Grant" }
5669
+ },
5670
+ expect: { status: 401, error: "Admin account required" }
5671
+ },
5672
+ {
5673
+ id: "update-permission-missing",
5674
+ route: "PATCH /SASjsApi/permission/{uid}",
5675
+ description: "answers 404 for a rule that does not exist",
5676
+ identity: ADMIN_SIGNED_IN,
5677
+ request: {
5678
+ token: ADMIN,
5679
+ params: { uid: "rule-none" },
5680
+ body: { setting: "Grant" }
5681
+ },
5682
+ expect: { status: 404, error: "Permission not found." }
5683
+ },
5684
+ {
5685
+ id: "delete-permission",
5686
+ route: "DELETE /SASjsApi/permission/{uid}",
5687
+ description: "deletes a rule",
5688
+ identity: ADMIN_SIGNED_IN,
5689
+ request: { token: ADMIN, params: { uid: "rule-code" } },
5690
+ expect: { status: 204 }
5691
+ },
5692
+ {
5693
+ id: "delete-permission-not-admin",
5694
+ route: "DELETE /SASjsApi/permission/{uid}",
5695
+ description: "refuses a deletion by a non-administrator",
5696
+ identity: ADMIN_SIGNED_IN,
5697
+ request: { token: RESTRICTED, params: { uid: "rule-code" } },
5698
+ expect: { status: 401, error: "Admin account required" }
5699
+ },
5700
+ {
5701
+ id: "delete-permission-missing",
5702
+ route: "DELETE /SASjsApi/permission/{uid}",
5703
+ description: "answers 404 for a rule that does not exist",
5704
+ identity: ADMIN_SIGNED_IN,
5705
+ request: { token: ADMIN, params: { uid: "rule-none" } },
5706
+ expect: { status: 404, error: "Permission not found." }
5707
+ },
5708
+ // --------------------------------------------------------------- clients
5709
+ {
5710
+ id: "list-clients",
5711
+ route: "GET /SASjsApi/client",
5712
+ description: "lists the registered clients",
5713
+ identity: ADMIN_SIGNED_IN,
5714
+ request: { token: ADMIN },
5715
+ expect: {
5716
+ status: 200,
5717
+ body: [
5718
+ {
5719
+ clientId: "web_app",
5720
+ clientSecret: "web_app",
5721
+ accessTokenExpiration: 86400,
5722
+ refreshTokenExpiration: 2592e3
5723
+ },
5724
+ {
5725
+ clientId: "sasjs",
5726
+ clientSecret: "sasjs",
5727
+ accessTokenExpiration: 3600,
5728
+ refreshTokenExpiration: 86400
5729
+ }
5730
+ ]
5731
+ }
5732
+ },
5733
+ {
5734
+ id: "list-clients-not-admin",
5735
+ route: "GET /SASjsApi/client",
5736
+ description: "refuses the list to a caller who is not an administrator",
5737
+ identity: ADMIN_SIGNED_IN,
5738
+ request: { token: RESTRICTED },
5739
+ expect: { status: 401, error: "Admin account required" }
5740
+ },
5741
+ {
5742
+ id: "create-client",
5743
+ route: "POST /SASjsApi/client",
5744
+ description: "registers a client",
5745
+ identity: ADMIN_SIGNED_IN,
5746
+ request: {
5747
+ token: ADMIN,
5748
+ body: {
5749
+ clientId: "new_app",
5750
+ clientSecret: "new_secret",
5751
+ accessTokenExpiration: 3600,
5752
+ refreshTokenExpiration: 86400
5753
+ }
5754
+ },
5755
+ expect: {
5756
+ status: 200,
5757
+ body: {
5758
+ clientId: "new_app",
5759
+ clientSecret: "new_secret",
5760
+ accessTokenExpiration: 3600,
5761
+ refreshTokenExpiration: 86400
5762
+ }
5763
+ }
5764
+ },
5765
+ {
5766
+ id: "create-client-defaults",
5767
+ route: "POST /SASjsApi/client",
5768
+ description: "applies the schema defaults when the lifetimes are omitted",
5769
+ identity: ADMIN_SIGNED_IN,
5770
+ request: {
5771
+ token: ADMIN,
5772
+ body: { clientId: "default_app", clientSecret: "default_secret" }
5773
+ },
5774
+ expect: {
5775
+ status: 200,
5776
+ body: {
5777
+ clientId: "default_app",
5778
+ clientSecret: "default_secret",
5779
+ accessTokenExpiration: 86400,
5780
+ refreshTokenExpiration: 2592e3
5781
+ }
5782
+ }
5783
+ },
5784
+ {
5785
+ id: "create-client-not-admin",
5786
+ route: "POST /SASjsApi/client",
5787
+ description: "refuses a registration by a non-administrator",
5788
+ identity: ADMIN_SIGNED_IN,
5789
+ request: {
5790
+ token: RESTRICTED,
5791
+ body: {
5792
+ clientId: "new_app",
5793
+ clientSecret: "new_secret",
5794
+ accessTokenExpiration: 3600,
5795
+ refreshTokenExpiration: 86400
5796
+ }
5797
+ },
5798
+ expect: { status: 401, error: "Admin account required" }
5799
+ },
5800
+ {
5801
+ id: "create-client-duplicate",
5802
+ route: "POST /SASjsApi/client",
5803
+ description: "refuses a client id that is registered, with the 403 the server sends",
5804
+ identity: ADMIN_SIGNED_IN,
5805
+ request: {
5806
+ token: ADMIN,
5807
+ body: {
5808
+ clientId: "web_app",
5809
+ clientSecret: "other",
5810
+ accessTokenExpiration: 3600,
5811
+ refreshTokenExpiration: 86400
5812
+ }
5813
+ },
5814
+ expect: { status: 403, error: "Error: Client ID already exists." }
5815
+ },
5816
+ // -------------------------------------------------------------- passwords
5817
+ {
5818
+ id: "update-password",
5819
+ route: "PATCH /SASjsApi/auth/updatePassword",
5820
+ description: "changes the caller’s own password",
5821
+ identity: ADMIN_SIGNED_IN,
5822
+ request: {
5823
+ token: RESTRICTED,
5824
+ body: { currentPassword: "user123", newPassword: "changed123" }
5825
+ },
5826
+ expect: { status: 204 }
5827
+ },
5828
+ {
5829
+ id: "update-password-missing-field",
5830
+ route: "PATCH /SASjsApi/auth/updatePassword",
5831
+ description: "refuses a body without the current password",
5832
+ identity: ADMIN_SIGNED_IN,
5833
+ request: { token: RESTRICTED, body: { newPassword: "changed123" } },
5834
+ expect: { status: 400, error: '"currentPassword" is required' }
5835
+ },
5836
+ {
5837
+ id: "update-password-unauthenticated",
5838
+ route: "PATCH /SASjsApi/auth/updatePassword",
5839
+ description: "refuses the change with no caller",
5840
+ identity: ADMIN_SIGNED_IN,
5841
+ request: {
5842
+ body: { currentPassword: "user123", newPassword: "changed123" }
5843
+ },
5844
+ expect: { status: 401, error: "Unauthorized" }
5845
+ },
5846
+ {
5847
+ id: "update-password-wrong-current",
5848
+ route: "PATCH /SASjsApi/auth/updatePassword",
5849
+ description: "refuses a change whose current password is wrong",
5850
+ identity: ADMIN_SIGNED_IN,
5851
+ request: {
5852
+ token: RESTRICTED,
5853
+ body: { currentPassword: "wrong", newPassword: "changed123" }
5854
+ },
5855
+ expect: { status: 403, error: "Invalid current password!" }
5856
+ },
5857
+ {
5858
+ id: "update-password-provider",
5859
+ route: "PATCH /SASjsApi/auth/updatePassword",
5860
+ description: "refuses a password change on a provider-managed account",
5861
+ identity: PROVIDER_MANAGED,
5862
+ request: {
5863
+ token: "dir-access",
5864
+ body: { currentPassword: "dir123", newPassword: "changed123" }
5865
+ },
5866
+ expect: {
5867
+ status: 405,
5868
+ error: "Can not update password of user that is created by an external auth provider."
5869
+ }
5870
+ }
5871
+ ];
5872
+
5873
+ // src/corpus/runner.ts
5874
+ var servicesFor = (testCase) => {
5875
+ const identity = new MemoryIdentityStore(
5876
+ testCase.identity || DEFAULT_IDENTITY
5877
+ );
5878
+ return {
5879
+ drive: createDriveService(
5880
+ testCase.unwritable ? new RefusingDriveStore(testCase.seed || {}) : new MemoryDriveStore(testCase.seed || {})
5881
+ ),
5882
+ identity: createIdentityService(identity),
5883
+ records: createRecordService(identity)
5884
+ };
5885
+ };
3773
5886
  var RefusingDriveStore = class extends MemoryDriveStore {
3774
5887
  async write() {
3775
5888
  throw new Error("the store refused the write");