@sasjs/api 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +3 -2
  2. package/build/browser.js +2218 -118
  3. package/build/cjs/corpus/records.js +1222 -0
  4. package/build/cjs/corpus/runner.js +15 -6
  5. package/build/cjs/host/dispatch.js +190 -2
  6. package/build/cjs/identity/memory-store.js +196 -11
  7. package/build/cjs/identity/records.js +398 -0
  8. package/build/cjs/index.js +8 -1
  9. package/build/cjs/routes/generated.js +184 -4
  10. package/build/cjs/routes/overlay.js +53 -58
  11. package/build/esm/corpus/records.d.ts +25 -0
  12. package/build/esm/corpus/records.d.ts.map +1 -0
  13. package/build/esm/corpus/records.js +1219 -0
  14. package/build/esm/corpus/runner.d.ts.map +1 -1
  15. package/build/esm/corpus/runner.js +15 -6
  16. package/build/esm/host/dispatch.d.ts +9 -0
  17. package/build/esm/host/dispatch.d.ts.map +1 -1
  18. package/build/esm/host/dispatch.js +190 -2
  19. package/build/esm/identity/fixture.d.ts +6 -0
  20. package/build/esm/identity/fixture.d.ts.map +1 -1
  21. package/build/esm/identity/memory-store.d.ts +29 -5
  22. package/build/esm/identity/memory-store.d.ts.map +1 -1
  23. package/build/esm/identity/memory-store.js +196 -11
  24. package/build/esm/identity/records.d.ts +102 -0
  25. package/build/esm/identity/records.d.ts.map +1 -0
  26. package/build/esm/identity/records.js +393 -0
  27. package/build/esm/identity/types.d.ts +102 -2
  28. package/build/esm/identity/types.d.ts.map +1 -1
  29. package/build/esm/index.d.ts +4 -1
  30. package/build/esm/index.d.ts.map +1 -1
  31. package/build/esm/index.js +2 -0
  32. package/build/esm/routes/generated.d.ts.map +1 -1
  33. package/build/esm/routes/generated.js +184 -4
  34. package/build/esm/routes/overlay.d.ts.map +1 -1
  35. package/build/esm/routes/overlay.js +53 -58
  36. package/package.json +1 -1
package/build/browser.js CHANGED
@@ -1,4 +1,4 @@
1
- /* @sasjs/api 0.6.1 - 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
 
@@ -678,7 +683,7 @@ var SASjsApi = (() => {
678
683
  },
679
684
  {
680
685
  status: 401,
681
- 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`"
682
687
  }
683
688
  ]
684
689
  },
@@ -772,6 +777,10 @@ var SASjsApi = (() => {
772
777
  {
773
778
  status: 200,
774
779
  description: "Ok"
780
+ },
781
+ {
782
+ status: 401,
783
+ description: "Admin account required"
775
784
  }
776
785
  ]
777
786
  },
@@ -812,6 +821,14 @@ var SASjsApi = (() => {
812
821
  {
813
822
  status: 200,
814
823
  description: "Ok"
824
+ },
825
+ {
826
+ status: 401,
827
+ description: "Admin account required"
828
+ },
829
+ {
830
+ status: 403,
831
+ description: "Client ID already exists."
815
832
  }
816
833
  ]
817
834
  },
@@ -1301,7 +1318,7 @@ var SASjsApi = (() => {
1301
1318
  },
1302
1319
  {
1303
1320
  status: 409,
1304
- description: "Folder already exists"
1321
+ description: "Folder already exists. The body is that message as plain text."
1305
1322
  }
1306
1323
  ]
1307
1324
  },
@@ -1341,7 +1358,7 @@ var SASjsApi = (() => {
1341
1358
  },
1342
1359
  {
1343
1360
  status: 409,
1344
- 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."
1345
1362
  }
1346
1363
  ]
1347
1364
  },
@@ -1391,6 +1408,14 @@ var SASjsApi = (() => {
1391
1408
  {
1392
1409
  status: 200,
1393
1410
  description: "Ok"
1411
+ },
1412
+ {
1413
+ status: 401,
1414
+ description: "Admin account required"
1415
+ },
1416
+ {
1417
+ status: 409,
1418
+ description: "Group name already exists."
1394
1419
  }
1395
1420
  ]
1396
1421
  },
@@ -1419,6 +1444,22 @@ var SASjsApi = (() => {
1419
1444
  {
1420
1445
  status: 200,
1421
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."
1422
1463
  }
1423
1464
  ]
1424
1465
  },
@@ -1447,6 +1488,22 @@ var SASjsApi = (() => {
1447
1488
  {
1448
1489
  status: 200,
1449
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."
1450
1507
  }
1451
1508
  ]
1452
1509
  },
@@ -1469,6 +1526,14 @@ var SASjsApi = (() => {
1469
1526
  {
1470
1527
  status: 200,
1471
1528
  description: "Ok"
1529
+ },
1530
+ {
1531
+ status: 401,
1532
+ description: "Admin account required"
1533
+ },
1534
+ {
1535
+ status: 404,
1536
+ description: "Group not found."
1472
1537
  }
1473
1538
  ]
1474
1539
  },
@@ -1491,6 +1556,10 @@ var SASjsApi = (() => {
1491
1556
  {
1492
1557
  status: 200,
1493
1558
  description: "Ok"
1559
+ },
1560
+ {
1561
+ status: 404,
1562
+ description: "Group not found."
1494
1563
  }
1495
1564
  ]
1496
1565
  },
@@ -1513,6 +1582,10 @@ var SASjsApi = (() => {
1513
1582
  {
1514
1583
  status: 200,
1515
1584
  description: "Ok"
1585
+ },
1586
+ {
1587
+ status: 404,
1588
+ description: "Group not found."
1516
1589
  }
1517
1590
  ]
1518
1591
  },
@@ -1573,6 +1646,14 @@ var SASjsApi = (() => {
1573
1646
  {
1574
1647
  status: 200,
1575
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."
1576
1657
  }
1577
1658
  ]
1578
1659
  },
@@ -1619,6 +1700,22 @@ var SASjsApi = (() => {
1619
1700
  {
1620
1701
  status: 200,
1621
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."
1622
1719
  }
1623
1720
  ]
1624
1721
  },
@@ -1641,6 +1738,14 @@ var SASjsApi = (() => {
1641
1738
  {
1642
1739
  status: 204,
1643
1740
  description: "No content"
1741
+ },
1742
+ {
1743
+ status: 401,
1744
+ description: "Admin account required"
1745
+ },
1746
+ {
1747
+ status: 404,
1748
+ description: "Permission not found."
1644
1749
  }
1645
1750
  ]
1646
1751
  },
@@ -1669,6 +1774,14 @@ var SASjsApi = (() => {
1669
1774
  {
1670
1775
  status: 200,
1671
1776
  description: "Ok"
1777
+ },
1778
+ {
1779
+ status: 401,
1780
+ description: "Admin account required"
1781
+ },
1782
+ {
1783
+ status: 404,
1784
+ description: "Permission not found."
1672
1785
  }
1673
1786
  ]
1674
1787
  },
@@ -1933,6 +2046,14 @@ var SASjsApi = (() => {
1933
2046
  {
1934
2047
  status: 200,
1935
2048
  description: "Ok"
2049
+ },
2050
+ {
2051
+ status: 401,
2052
+ description: "Admin account required"
2053
+ },
2054
+ {
2055
+ status: 409,
2056
+ description: "Username already exists."
1936
2057
  }
1937
2058
  ]
1938
2059
  },
@@ -1961,6 +2082,14 @@ var SASjsApi = (() => {
1961
2082
  {
1962
2083
  status: 204,
1963
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."
1964
2093
  }
1965
2094
  ]
1966
2095
  },
@@ -1983,6 +2112,14 @@ var SASjsApi = (() => {
1983
2112
  {
1984
2113
  status: 200,
1985
2114
  description: "Ok"
2115
+ },
2116
+ {
2117
+ status: 401,
2118
+ description: "Unauthorized"
2119
+ },
2120
+ {
2121
+ status: 404,
2122
+ description: "User is not found."
1986
2123
  }
1987
2124
  ]
1988
2125
  },
@@ -2041,6 +2178,22 @@ var SASjsApi = (() => {
2041
2178
  {
2042
2179
  status: 200,
2043
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."
2044
2197
  }
2045
2198
  ]
2046
2199
  },
@@ -2069,6 +2222,14 @@ var SASjsApi = (() => {
2069
2222
  {
2070
2223
  status: 204,
2071
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."
2072
2233
  }
2073
2234
  ]
2074
2235
  },
@@ -2091,6 +2252,14 @@ var SASjsApi = (() => {
2091
2252
  {
2092
2253
  status: 200,
2093
2254
  description: "Ok"
2255
+ },
2256
+ {
2257
+ status: 401,
2258
+ description: "Unauthorized"
2259
+ },
2260
+ {
2261
+ status: 404,
2262
+ description: "User is not found."
2094
2263
  }
2095
2264
  ]
2096
2265
  },
@@ -2149,6 +2318,22 @@ var SASjsApi = (() => {
2149
2318
  {
2150
2319
  status: 200,
2151
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."
2152
2337
  }
2153
2338
  ]
2154
2339
  },
@@ -2182,7 +2367,7 @@ var SASjsApi = (() => {
2182
2367
  },
2183
2368
  {
2184
2369
  status: 403,
2185
- 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"
2186
2371
  }
2187
2372
  ]
2188
2373
  },
@@ -2267,7 +2452,7 @@ var SASjsApi = (() => {
2267
2452
  Client: ["identity"],
2268
2453
  Web: ["identity"]
2269
2454
  };
2270
- 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.";
2271
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.";
2272
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.";
2273
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.";
@@ -2322,18 +2507,20 @@ var SASjsApi = (() => {
2322
2507
  },
2323
2508
  "POST /SASjsApi/auth/logout": { implemented: true },
2324
2509
  "PATCH /SASjsApi/auth/updatePassword": {
2325
- implemented: false,
2326
- 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]
2327
2514
  },
2328
2515
  // Auth Config - the providers a host was started with, and an LDAP import
2329
2516
  "GET /SASjsApi/authConfig": { implemented: false, reason: HOST_CONFIG },
2330
2517
  "POST /SASjsApi/authConfig/synchroniseWithLDAP": {
2331
2518
  implemented: false,
2332
- reason: IDENTITY_RECORDS
2519
+ reason: DIRECTORY
2333
2520
  },
2334
2521
  // Client - the OAuth clients the identity store already holds for sign-in
2335
- "GET /SASjsApi/client": { implemented: false, reason: IDENTITY_RECORDS },
2336
- "POST /SASjsApi/client": { implemented: false, reason: IDENTITY_RECORDS },
2522
+ "GET /SASjsApi/client": { implemented: true },
2523
+ "POST /SASjsApi/client": { implemented: true },
2337
2524
  // Code - the editor's three calls, and the runtime one
2338
2525
  "POST /SASjsApi/code/execute": { implemented: false, reason: RUNTIME },
2339
2526
  "POST /SASjsApi/code/trigger": { implemented: false, reason: RUNTIME },
@@ -2407,25 +2594,13 @@ var SASjsApi = (() => {
2407
2594
  reason: UNPUBLISH
2408
2595
  },
2409
2596
  // Group - members and the groups themselves
2410
- "GET /SASjsApi/group": { implemented: false, reason: IDENTITY_RECORDS },
2411
- "GET /SASjsApi/group/{uid}": { implemented: false, reason: IDENTITY_RECORDS },
2412
- "GET /SASjsApi/group/by/groupname/{name}": {
2413
- implemented: false,
2414
- reason: IDENTITY_RECORDS
2415
- },
2416
- "POST /SASjsApi/group": { implemented: false, reason: IDENTITY_RECORDS },
2417
- "POST /SASjsApi/group/{groupUid}/{userUid}": {
2418
- implemented: false,
2419
- reason: IDENTITY_RECORDS
2420
- },
2421
- "DELETE /SASjsApi/group/{groupUid}/{userUid}": {
2422
- implemented: false,
2423
- reason: IDENTITY_RECORDS
2424
- },
2425
- "DELETE /SASjsApi/group/{uid}": {
2426
- implemented: false,
2427
- reason: IDENTITY_RECORDS
2428
- },
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 },
2429
2604
  // Info - what the host is, and what this caller may use
2430
2605
  "GET /SASjsApi/info": { implemented: false, reason: HOST_CONFIG },
2431
2606
  "GET /SASjsApi/info/authorizedRoutes": {
@@ -2441,16 +2616,24 @@ var SASjsApi = (() => {
2441
2616
  reason: PERMISSIONS
2442
2617
  },
2443
2618
  // Permission - the rules the inventory above is derived from
2444
- "GET /SASjsApi/permission": { implemented: false, reason: IDENTITY_RECORDS },
2445
- "POST /SASjsApi/permission": { implemented: false, reason: IDENTITY_RECORDS },
2446
- "PATCH /SASjsApi/permission/{uid}": {
2447
- implemented: false,
2448
- reason: IDENTITY_RECORDS
2449
- },
2450
- "DELETE /SASjsApi/permission/{uid}": {
2451
- implemented: false,
2452
- reason: IDENTITY_RECORDS
2453
- },
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 },
2454
2637
  // Session - the caller's own session, then the record behind a triggered one
2455
2638
  "GET /SASjsApi/session": {
2456
2639
  implemented: true,
@@ -2477,29 +2660,26 @@ var SASjsApi = (() => {
2477
2660
  "POST /SASjsApi/stp/execute": { implemented: false, reason: RUNTIME },
2478
2661
  "POST /SASjsApi/stp/trigger": { implemented: false, reason: RUNTIME },
2479
2662
  // User - the accounts the identity store already holds for sign-in
2480
- "GET /SASjsApi/user": { implemented: false, reason: IDENTITY_RECORDS },
2481
- "GET /SASjsApi/user/{uid}": { implemented: false, reason: IDENTITY_RECORDS },
2482
- "GET /SASjsApi/user/by/username/{username}": {
2483
- implemented: false,
2484
- reason: IDENTITY_RECORDS
2485
- },
2486
- "POST /SASjsApi/user": { implemented: false, reason: IDENTITY_RECORDS },
2487
- "PATCH /SASjsApi/user/{uid}": {
2488
- implemented: false,
2489
- reason: IDENTITY_RECORDS
2490
- },
2491
- "PATCH /SASjsApi/user/by/username/{username}": {
2492
- implemented: false,
2493
- reason: IDENTITY_RECORDS
2494
- },
2495
- "DELETE /SASjsApi/user/{uid}": {
2496
- implemented: false,
2497
- reason: IDENTITY_RECORDS
2498
- },
2499
- "DELETE /SASjsApi/user/by/username/{username}": {
2500
- implemented: false,
2501
- reason: IDENTITY_RECORDS
2502
- }
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 }
2503
2683
  };
2504
2684
 
2505
2685
  // src/routes/index.ts
@@ -2591,6 +2771,7 @@ var SASjsApi = (() => {
2591
2771
  };
2592
2772
 
2593
2773
  // src/host/dispatch.ts
2774
+ var SECONDS_IN_A_DAY = 86400;
2594
2775
  var validate = (route, request) => {
2595
2776
  for (const param of route.params) {
2596
2777
  if (!param.required || param.in === "formData" || param.in === "header")
@@ -2611,10 +2792,31 @@ var SASjsApi = (() => {
2611
2792
  var dispatch = async (services, routeId, request = {}) => {
2612
2793
  const route = routeById(routeId);
2613
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
+ }
2614
2808
  validate(route, request);
2615
- const { drive, identity } = services;
2616
2809
  const query = request.query || {};
2617
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
+ };
2618
2820
  const caller = async () => {
2619
2821
  if (request.subject) return request.subject;
2620
2822
  return identity?.authenticate(request.token);
@@ -2706,6 +2908,169 @@ var SASjsApi = (() => {
2706
2908
  }
2707
2909
  case "GET /SASjsApi/drive/filetree":
2708
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 };
2709
3074
  default: {
2710
3075
  const reason = routeById(routeId)?.reason;
2711
3076
  throw new ApiError(
@@ -2732,7 +3097,29 @@ var SASjsApi = (() => {
2732
3097
  "POST /SASLogon/login",
2733
3098
  "POST /SASLogon/authorize",
2734
3099
  "GET /SASLogon/logout",
2735
- "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"
2736
3123
  ];
2737
3124
 
2738
3125
  // src/identity/service.ts
@@ -2821,37 +3208,365 @@ var SASjsApi = (() => {
2821
3208
  needsToUpdatePassword: user3.needsToUpdatePassword
2822
3209
  });
2823
3210
 
2824
- // src/identity/fixture.ts
2825
- var IDENTITY_FORMAT = 1;
2826
- var SECONDS_IN_A_DAY = 86400;
2827
- var DEFAULT_IDENTITY = {
2828
- format: IDENTITY_FORMAT,
2829
- users: [
2830
- {
2831
- uid: "user-admin",
2832
- username: "admin",
2833
- displayName: "Demo Admin",
2834
- // long enough to pass the API's own password rule (six characters), so a
2835
- // demo can sign in against a real server as well as a mock
2836
- password: "admin123",
2837
- isAdmin: true,
2838
- isActive: true,
2839
- needsToUpdatePassword: false
2840
- },
2841
- {
2842
- uid: "user-restricted",
2843
- username: "user2",
2844
- displayName: "Demo User",
2845
- password: "user123",
2846
- isAdmin: false,
2847
- isActive: true,
2848
- 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));
2849
3251
  }
2850
- ],
2851
- groups: [
2852
- {
2853
- uid: "group-data-controller",
2854
- name: "DCGroup",
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
3564
+ }
3565
+ ],
3566
+ groups: [
3567
+ {
3568
+ uid: "group-data-controller",
3569
+ name: "DCGroup",
2855
3570
  description: "Data Controller users",
2856
3571
  isActive: true,
2857
3572
  users: ["user2"]
@@ -2891,14 +3606,14 @@ var SASjsApi = (() => {
2891
3606
  {
2892
3607
  clientId: "web_app",
2893
3608
  clientSecret: "web_app",
2894
- accessTokenExpiration: SECONDS_IN_A_DAY,
2895
- refreshTokenExpiration: SECONDS_IN_A_DAY * 30
3609
+ accessTokenExpiration: SECONDS_IN_A_DAY2,
3610
+ refreshTokenExpiration: SECONDS_IN_A_DAY2 * 30
2896
3611
  },
2897
3612
  {
2898
3613
  clientId: "sasjs",
2899
3614
  clientSecret: "sasjs",
2900
3615
  accessTokenExpiration: 3600,
2901
- refreshTokenExpiration: SECONDS_IN_A_DAY
3616
+ refreshTokenExpiration: SECONDS_IN_A_DAY2
2902
3617
  }
2903
3618
  ]
2904
3619
  };
@@ -2944,9 +3659,12 @@ var SASjsApi = (() => {
2944
3659
  var live = (issued, now) => issued && (issued.expiresAt === void 0 || issued.expiresAt > now) ? issued.subject : void 0;
2945
3660
  var MemoryIdentityStore = class {
2946
3661
  constructor(fixture = DEFAULT_IDENTITY) {
2947
- this.users = [];
3662
+ this.userList = [];
2948
3663
  this.passwords = /* @__PURE__ */ new Map();
2949
- 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();
2950
3668
  this.pairs = /* @__PURE__ */ new Map();
2951
3669
  this.access = /* @__PURE__ */ new Map();
2952
3670
  this.refresh = /* @__PURE__ */ new Map();
@@ -2958,24 +3676,56 @@ var SASjsApi = (() => {
2958
3676
  `The identity fixture states format ${String(fixture.format)}; this store reads ${IDENTITY_FORMAT}.`
2959
3677
  );
2960
3678
  for (const user3 of fixture.users) {
2961
- this.users.push({
3679
+ this.userList.push({
2962
3680
  uid: user3.uid,
2963
3681
  username: user3.username,
2964
3682
  displayName: user3.displayName,
2965
3683
  isAdmin: user3.isAdmin,
2966
3684
  isActive: user3.isActive,
2967
- needsToUpdatePassword: user3.needsToUpdatePassword
3685
+ needsToUpdatePassword: user3.needsToUpdatePassword,
3686
+ authProvider: user3.authProvider,
3687
+ autoExec: user3.autoExec
2968
3688
  });
2969
3689
  this.passwords.set(user3.uid, user3.password);
2970
3690
  }
2971
- for (const client of fixture.clients)
2972
- 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, {
2973
3698
  clientId: client.clientId,
3699
+ clientSecret: client.clientSecret,
2974
3700
  accessTokenExpiration: client.accessTokenExpiration,
2975
3701
  refreshTokenExpiration: client.refreshTokenExpiration
2976
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
+ }
2977
3727
  for (const token of fixture.tokens || []) {
2978
- const user3 = this.users.find(
3728
+ const user3 = this.userList.find(
2979
3729
  (candidate) => candidate.username === token.username
2980
3730
  );
2981
3731
  if (!user3) continue;
@@ -2989,7 +3739,7 @@ var SASjsApi = (() => {
2989
3739
  });
2990
3740
  }
2991
3741
  for (const code of fixture.codes || []) {
2992
- const user3 = this.users.find(
3742
+ const user3 = this.userList.find(
2993
3743
  (candidate) => candidate.username === code.username
2994
3744
  );
2995
3745
  if (!user3) continue;
@@ -2999,17 +3749,21 @@ var SASjsApi = (() => {
2999
3749
  }
3000
3750
  }
3001
3751
  async userByName(username) {
3002
- 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;
3003
3756
  }
3004
3757
  async userById(uid) {
3005
- 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;
3006
3760
  }
3007
3761
  /** A fixture holds the password in clear, so the check is an equality. */
3008
3762
  async passwordMatches(user3, password) {
3009
3763
  return this.passwords.get(user3.uid) === password;
3010
3764
  }
3011
3765
  async client(clientId) {
3012
- return this.clients.get(clientId);
3766
+ return this.clientMap.get(clientId);
3013
3767
  }
3014
3768
  async issue(subject, client) {
3015
3769
  const issuedAt = Date.now();
@@ -3087,6 +3841,134 @@ var SASjsApi = (() => {
3087
3841
  this.claimed.add(code);
3088
3842
  return true;
3089
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
+ }
3090
3972
  };
3091
3973
 
3092
3974
  // src/corpus/drive.ts
@@ -3774,15 +4656,1233 @@ var SASjsApi = (() => {
3774
4656
  }
3775
4657
  ];
3776
4658
 
3777
- // src/corpus/runner.ts
3778
- var servicesFor = (testCase) => ({
3779
- drive: createDriveService(
3780
- testCase.unwritable ? new RefusingDriveStore(testCase.seed || {}) : new MemoryDriveStore(testCase.seed || {})
3781
- ),
3782
- identity: createIdentityService(
3783
- new MemoryIdentityStore(testCase.identity || DEFAULT_IDENTITY)
3784
- )
3785
- });
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
+ };
3786
5886
  var RefusingDriveStore = class extends MemoryDriveStore {
3787
5887
  async write() {
3788
5888
  throw new Error("the store refused the write");