@sasjs/api 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/browser.js CHANGED
@@ -1,4 +1,4 @@
1
- /* @sasjs/api 0.8.0 - the SASjs Server API, for a service worker. https://github.com/sasjs/api */
1
+ /* @sasjs/api 0.9.0 - the SASjs Server API, for a service worker. https://github.com/sasjs/api */
2
2
  "use strict";
3
3
  var SASjsApi = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -23,7 +23,9 @@ var SASjsApi = (() => {
23
23
  var index_exports = {};
24
24
  __export(index_exports, {
25
25
  ADMIN_SIGNED_IN: () => ADMIN_SIGNED_IN,
26
+ ALWAYS_VISIBLE_ROUTES: () => ALWAYS_VISIBLE_ROUTES,
26
27
  ApiError: () => ApiError,
28
+ DEFAULT_GRANTABLE_ROUTES: () => DEFAULT_GRANTABLE_ROUTES,
27
29
  DEFAULT_IDENTITY: () => DEFAULT_IDENTITY,
28
30
  DEPLOY_LIMITS: () => DEPLOY_LIMITS,
29
31
  DEPLOY_PAYLOAD_BYTES: () => DEPLOY_PAYLOAD_BYTES,
@@ -38,7 +40,9 @@ var SASjsApi = (() => {
38
40
  PROVIDER_MANAGED: () => PROVIDER_MANAGED,
39
41
  RECORD_CORPUS: () => RECORD_CORPUS,
40
42
  ROUTES: () => ROUTES,
43
+ STATIC_AUTHORIZED_ROUTES: () => STATIC_AUTHORIZED_ROUTES,
41
44
  TAG_CAPABILITIES: () => TAG_CAPABILITIES,
45
+ TOP_LEVEL_ROUTES: () => TOP_LEVEL_ROUTES,
42
46
  basename: () => basename,
43
47
  checkDeployTree: () => checkDeployTree,
44
48
  createDriveService: () => createDriveService,
@@ -53,13 +57,15 @@ var SASjsApi = (() => {
53
57
  isApiError: () => isApiError,
54
58
  isDeployTree: () => isDeployTree,
55
59
  isRoot: () => isRoot,
60
+ isRouteGranted: () => isRouteGranted,
56
61
  measureDeployTree: () => measureDeployTree,
57
62
  normaliseDrivePath: () => normaliseDrivePath,
58
63
  parseIdentity: () => parseIdentity,
59
64
  routeById: () => routeById,
60
65
  runCase: () => runCase,
61
66
  sanitiseDisplayName: () => sanitiseDisplayName,
62
- servicesFor: () => servicesFor
67
+ servicesFor: () => servicesFor,
68
+ topLevelRouteFor: () => topLevelRouteFor
63
69
  });
64
70
 
65
71
  // src/types.ts
@@ -2457,7 +2463,6 @@ var SASjsApi = (() => {
2457
2463
  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.";
2458
2464
  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.";
2459
2465
  var HOST_CONFIG = "The body is the host's own configuration - ports, run times and providers - which a mock states for itself and this package cannot know.";
2460
- var PERMISSIONS = "The route inventory is the host's permission model, which this package does not hold.";
2461
2466
  var HOST_OWNED = "The host owns this report: it describes the host's own process and files, which a browser build has no equivalent of.";
2462
2467
  var WEB_BUNDLE = "The host serves its own web bundle; there is no API decision here for this package to answer.";
2463
2468
  var ARCHIVE = "Expanding the uploaded archive is the host's transport work - multer and a zip library on the server, `DecompressionStream` in a worker - and the host hands this package the tree it produces, which is the same tree `POST /SASjsApi/drive/deploy` takes.";
@@ -2604,16 +2609,12 @@ var SASjsApi = (() => {
2604
2609
  // Info - what the host is, and what this caller may use
2605
2610
  "GET /SASjsApi/info": { implemented: false, reason: HOST_CONFIG },
2606
2611
  "GET /SASjsApi/info/authorizedRoutes": {
2607
- implemented: false,
2608
- // the route inventory reports what the CALLER may use, so it needs an
2609
- // identity even though the rest of the Info tag needs nothing
2610
- capabilities: ["identity"],
2611
- reason: PERMISSIONS
2612
+ implemented: true,
2613
+ capabilities: ["identity"]
2612
2614
  },
2613
2615
  "GET /SASjsApi/info/myAuthorizedRoutes": {
2614
- implemented: false,
2615
- capabilities: ["identity"],
2616
- reason: PERMISSIONS
2616
+ implemented: true,
2617
+ capabilities: ["identity"]
2617
2618
  },
2618
2619
  // Permission - the rules the inventory above is derived from
2619
2620
  "GET /SASjsApi/permission": {
@@ -2970,6 +2971,16 @@ var SASjsApi = (() => {
2970
2971
  { password: body.password }
2971
2972
  )
2972
2973
  };
2974
+ case "GET /SASjsApi/info/authorizedRoutes":
2975
+ return {
2976
+ status: 200,
2977
+ body: await required().listAuthorizedRoutes(await caller())
2978
+ };
2979
+ case "GET /SASjsApi/info/myAuthorizedRoutes":
2980
+ return {
2981
+ status: 200,
2982
+ body: await required().myAuthorizedRoutes(await caller())
2983
+ };
2973
2984
  case "GET /SASjsApi/group":
2974
2985
  return { status: 200, body: await required().listGroups(await caller()) };
2975
2986
  case "GET /SASjsApi/group/{uid}":
@@ -3080,6 +3091,8 @@ var SASjsApi = (() => {
3080
3091
  }
3081
3092
  };
3082
3093
  var HANDLED_ROUTES = [
3094
+ "GET /SASjsApi/info/authorizedRoutes",
3095
+ "GET /SASjsApi/info/myAuthorizedRoutes",
3083
3096
  "GET /SASjsApi/drive/file",
3084
3097
  "POST /SASjsApi/drive/file",
3085
3098
  "PATCH /SASjsApi/drive/file",
@@ -3121,6 +3134,40 @@ var SASjsApi = (() => {
3121
3134
  "PATCH /SASjsApi/auth/updatePassword"
3122
3135
  ];
3123
3136
 
3137
+ // src/identity/routeAccess.ts
3138
+ var TOP_LEVEL_ROUTES = ["/AppStream", "/SASjsApi"];
3139
+ var ALWAYS_VISIBLE_ROUTES = ["/AppStream"];
3140
+ var STATIC_AUTHORIZED_ROUTES = [
3141
+ "/SASjsApi/code/execute",
3142
+ "/SASjsApi/stp/execute",
3143
+ "/SASjsApi/drive/deploy",
3144
+ "/SASjsApi/drive/deploy/upload",
3145
+ "/SASjsApi/drive/file",
3146
+ "/SASjsApi/drive/folder",
3147
+ "/SASjsApi/drive/fileTree",
3148
+ "/SASjsApi/drive/rename"
3149
+ ];
3150
+ var DEFAULT_GRANTABLE_ROUTES = [
3151
+ ...TOP_LEVEL_ROUTES,
3152
+ ...STATIC_AUTHORIZED_ROUTES
3153
+ ];
3154
+ var topLevelRouteFor = (routePath) => TOP_LEVEL_ROUTES.find(
3155
+ (route) => routePath.toLowerCase().startsWith(route.toLowerCase())
3156
+ ) ?? routePath;
3157
+ var settingOn = (rules, path) => rules.find((rule) => rule.path === path)?.setting;
3158
+ var GRANT = "Grant";
3159
+ var DENY = "Deny";
3160
+ var isRouteGranted = (rules, path, topLevelRoute = topLevelRouteFor(path)) => {
3161
+ const userSetting = settingOn(rules.user, path) ?? settingOn(rules.user, topLevelRoute);
3162
+ if (userSetting) return userSetting === GRANT;
3163
+ const onPath = rules.group.filter((rule) => rule.path === path);
3164
+ if (onPath.some((rule) => rule.setting === GRANT)) return true;
3165
+ if (onPath.some((rule) => rule.setting === DENY)) return false;
3166
+ return rules.group.some(
3167
+ (rule) => rule.path === topLevelRoute && rule.setting === GRANT
3168
+ );
3169
+ };
3170
+
3124
3171
  // src/identity/service.ts
3125
3172
  var unauthorized = () => new ApiError(401, "Unauthorized");
3126
3173
  var liveSubject = async (store, subject) => {
@@ -3241,7 +3288,8 @@ var SASjsApi = (() => {
3241
3288
  name: group.name,
3242
3289
  description: group.description
3243
3290
  });
3244
- var createRecordService = (store) => {
3291
+ var createRecordService = (store, options = {}) => {
3292
+ const grantableRoutes = options.grantableRoutes || DEFAULT_GRANTABLE_ROUTES;
3245
3293
  const groupDetails = async (group) => {
3246
3294
  const members = [];
3247
3295
  for (const uid of group.users) {
@@ -3428,6 +3476,32 @@ var SASjsApi = (() => {
3428
3476
  if (!updated) throw new ApiError(400, "Unable to update group.");
3429
3477
  return groupDetails(updated);
3430
3478
  },
3479
+ async listAuthorizedRoutes(caller) {
3480
+ await requireCaller(store, caller);
3481
+ return { paths: [...grantableRoutes] };
3482
+ },
3483
+ async myAuthorizedRoutes(caller) {
3484
+ const reader = await requireCaller(store, caller);
3485
+ if (reader.isAdmin) return { paths: [...grantableRoutes] };
3486
+ const rules = await store.permissions();
3487
+ const mine = {
3488
+ user: rules.filter((rule) => rule.user === reader.uid).map((rule) => ({ path: rule.path, setting: rule.setting })),
3489
+ group: rules.filter(
3490
+ (rule) => rule.group !== void 0 && reader.groups.includes(rule.group)
3491
+ ).map((rule) => ({ path: rule.path, setting: rule.setting }))
3492
+ };
3493
+ const publicGroup = (await store.groups()).find(
3494
+ (group) => group.name === "public"
3495
+ );
3496
+ const publicGrants = publicGroup ? rules.filter(
3497
+ (rule) => rule.group === publicGroup.uid && rule.setting === "Grant"
3498
+ ).map((rule) => rule.path) : [];
3499
+ return {
3500
+ paths: grantableRoutes.filter(
3501
+ (path) => ALWAYS_VISIBLE_ROUTES.includes(path) || publicGrants.includes(path) || isRouteGranted(mine, path, topLevelRouteFor(path))
3502
+ )
3503
+ };
3504
+ },
3431
3505
  async listPermissions(caller) {
3432
3506
  const reader = await requireCaller(store, caller);
3433
3507
  const rules = await store.permissions();
@@ -5343,6 +5417,70 @@ var SASjsApi = (() => {
5343
5417
  expect: { status: 404, error: "Group not found." }
5344
5418
  },
5345
5419
  // ------------------------------------------------------------ permissions
5420
+ {
5421
+ id: "authorized-routes",
5422
+ route: "GET /SASjsApi/info/authorizedRoutes",
5423
+ description: "answers the routes that accept a rule, to any caller",
5424
+ identity: ADMIN_SIGNED_IN,
5425
+ request: { token: RESTRICTED },
5426
+ expect: {
5427
+ status: 200,
5428
+ body: {
5429
+ paths: [
5430
+ "/AppStream",
5431
+ "/SASjsApi",
5432
+ "/SASjsApi/code/execute",
5433
+ "/SASjsApi/stp/execute",
5434
+ "/SASjsApi/drive/deploy",
5435
+ "/SASjsApi/drive/deploy/upload",
5436
+ "/SASjsApi/drive/file",
5437
+ "/SASjsApi/drive/folder",
5438
+ "/SASjsApi/drive/fileTree",
5439
+ "/SASjsApi/drive/rename"
5440
+ ]
5441
+ }
5442
+ }
5443
+ },
5444
+ {
5445
+ id: "my-authorized-routes-as-admin",
5446
+ route: "GET /SASjsApi/info/myAuthorizedRoutes",
5447
+ description: "answers every route to an administrator",
5448
+ identity: ADMIN_SIGNED_IN,
5449
+ request: { token: ADMIN },
5450
+ expect: {
5451
+ status: 200,
5452
+ body: {
5453
+ paths: [
5454
+ "/AppStream",
5455
+ "/SASjsApi",
5456
+ "/SASjsApi/code/execute",
5457
+ "/SASjsApi/stp/execute",
5458
+ "/SASjsApi/drive/deploy",
5459
+ "/SASjsApi/drive/deploy/upload",
5460
+ "/SASjsApi/drive/file",
5461
+ "/SASjsApi/drive/folder",
5462
+ "/SASjsApi/drive/fileTree",
5463
+ "/SASjsApi/drive/rename"
5464
+ ]
5465
+ }
5466
+ }
5467
+ },
5468
+ {
5469
+ id: "my-authorized-routes-as-user",
5470
+ route: "GET /SASjsApi/info/myAuthorizedRoutes",
5471
+ description: "answers a caller only the routes its rules grant",
5472
+ identity: ADMIN_SIGNED_IN,
5473
+ request: { token: RESTRICTED },
5474
+ expect: {
5475
+ status: 200,
5476
+ // /AppStream is visible whatever the rules say; the two grants come from
5477
+ // DCGroup; /SASjsApi/code/execute is denied to this user by a user rule,
5478
+ // and nothing grants the rest
5479
+ body: {
5480
+ paths: ["/AppStream", "/SASjsApi/stp/execute", "/SASjsApi/drive/file"]
5481
+ }
5482
+ }
5483
+ },
5346
5484
  {
5347
5485
  id: "list-permissions-as-admin",
5348
5486
  route: "GET /SASjsApi/permission",
@@ -702,6 +702,70 @@ exports.RECORD_CORPUS = [
702
702
  expect: { status: 404, error: 'Group not found.' }
703
703
  },
704
704
  // ------------------------------------------------------------ permissions
705
+ {
706
+ id: 'authorized-routes',
707
+ route: 'GET /SASjsApi/info/authorizedRoutes',
708
+ description: 'answers the routes that accept a rule, to any caller',
709
+ identity: exports.ADMIN_SIGNED_IN,
710
+ request: { token: RESTRICTED },
711
+ expect: {
712
+ status: 200,
713
+ body: {
714
+ paths: [
715
+ '/AppStream',
716
+ '/SASjsApi',
717
+ '/SASjsApi/code/execute',
718
+ '/SASjsApi/stp/execute',
719
+ '/SASjsApi/drive/deploy',
720
+ '/SASjsApi/drive/deploy/upload',
721
+ '/SASjsApi/drive/file',
722
+ '/SASjsApi/drive/folder',
723
+ '/SASjsApi/drive/fileTree',
724
+ '/SASjsApi/drive/rename'
725
+ ]
726
+ }
727
+ }
728
+ },
729
+ {
730
+ id: 'my-authorized-routes-as-admin',
731
+ route: 'GET /SASjsApi/info/myAuthorizedRoutes',
732
+ description: 'answers every route to an administrator',
733
+ identity: exports.ADMIN_SIGNED_IN,
734
+ request: { token: ADMIN },
735
+ expect: {
736
+ status: 200,
737
+ body: {
738
+ paths: [
739
+ '/AppStream',
740
+ '/SASjsApi',
741
+ '/SASjsApi/code/execute',
742
+ '/SASjsApi/stp/execute',
743
+ '/SASjsApi/drive/deploy',
744
+ '/SASjsApi/drive/deploy/upload',
745
+ '/SASjsApi/drive/file',
746
+ '/SASjsApi/drive/folder',
747
+ '/SASjsApi/drive/fileTree',
748
+ '/SASjsApi/drive/rename'
749
+ ]
750
+ }
751
+ }
752
+ },
753
+ {
754
+ id: 'my-authorized-routes-as-user',
755
+ route: 'GET /SASjsApi/info/myAuthorizedRoutes',
756
+ description: 'answers a caller only the routes its rules grant',
757
+ identity: exports.ADMIN_SIGNED_IN,
758
+ request: { token: RESTRICTED },
759
+ expect: {
760
+ status: 200,
761
+ // /AppStream is visible whatever the rules say; the two grants come from
762
+ // DCGroup; /SASjsApi/code/execute is denied to this user by a user rule,
763
+ // and nothing grants the rest
764
+ body: {
765
+ paths: ['/AppStream', '/SASjsApi/stp/execute', '/SASjsApi/drive/file']
766
+ }
767
+ }
768
+ },
705
769
  {
706
770
  id: 'list-permissions-as-admin',
707
771
  route: 'GET /SASjsApi/permission',
@@ -219,6 +219,16 @@ const dispatch = async (services, routeId, request = {}) => {
219
219
  status: 200,
220
220
  body: await required().deleteUser(await caller(), { username: String(params.username ?? '') }, { password: body.password })
221
221
  };
222
+ case 'GET /SASjsApi/info/authorizedRoutes':
223
+ return {
224
+ status: 200,
225
+ body: await required().listAuthorizedRoutes(await caller())
226
+ };
227
+ case 'GET /SASjsApi/info/myAuthorizedRoutes':
228
+ return {
229
+ status: 200,
230
+ body: await required().myAuthorizedRoutes(await caller())
231
+ };
222
232
  case 'GET /SASjsApi/group':
223
233
  return { status: 200, body: await required().listGroups(await caller()) };
224
234
  case 'GET /SASjsApi/group/{uid}':
@@ -322,6 +332,8 @@ const dispatch = async (services, routeId, request = {}) => {
322
332
  exports.dispatch = dispatch;
323
333
  /** Whether a route is one `dispatch` can answer today. */
324
334
  exports.HANDLED_ROUTES = [
335
+ 'GET /SASjsApi/info/authorizedRoutes',
336
+ 'GET /SASjsApi/info/myAuthorizedRoutes',
325
337
  'GET /SASjsApi/drive/file',
326
338
  'POST /SASjsApi/drive/file',
327
339
  'PATCH /SASjsApi/drive/file',
@@ -17,6 +17,7 @@ exports.createRecordService = exports.sanitiseDisplayName = void 0;
17
17
  * the server sends.
18
18
  */
19
19
  const types_js_1 = require("../types.js");
20
+ const routeAccess_js_1 = require("./routeAccess.js");
20
21
  /** The name the server gives the group every account is added to on creation. */
21
22
  const ALL_USERS_GROUP = 'all-users';
22
23
  /** The group whose membership the API refuses to change. */
@@ -83,7 +84,8 @@ const asGroup = (group) => ({
83
84
  name: group.name,
84
85
  description: group.description
85
86
  });
86
- const createRecordService = (store) => {
87
+ const createRecordService = (store, options = {}) => {
88
+ const grantableRoutes = options.grantableRoutes || routeAccess_js_1.DEFAULT_GRANTABLE_ROUTES;
87
89
  const groupDetails = async (group) => {
88
90
  const members = [];
89
91
  for (const uid of group.users) {
@@ -285,6 +287,41 @@ const createRecordService = (store) => {
285
287
  throw new types_js_1.ApiError(400, 'Unable to update group.');
286
288
  return groupDetails(updated);
287
289
  },
290
+ async listAuthorizedRoutes(caller) {
291
+ // an identity is required because the route is about a caller's access,
292
+ // even though what it returns is the whole inventory
293
+ await requireCaller(store, caller);
294
+ return { paths: [...grantableRoutes] };
295
+ },
296
+ async myAuthorizedRoutes(caller) {
297
+ const reader = await requireCaller(store, caller);
298
+ // an administrator holds every route, and the always-visible ones are
299
+ // visible whatever the rules say
300
+ if (reader.isAdmin)
301
+ return { paths: [...grantableRoutes] };
302
+ const rules = await store.permissions();
303
+ const mine = {
304
+ user: rules
305
+ .filter((rule) => rule.user === reader.uid)
306
+ .map((rule) => ({ path: rule.path, setting: rule.setting })),
307
+ group: rules
308
+ .filter((rule) => rule.group !== undefined && reader.groups.includes(rule.group))
309
+ .map((rule) => ({ path: rule.path, setting: rule.setting }))
310
+ };
311
+ // a grant on the reserved public group admits any caller, and it is
312
+ // checked before the rules so no user or group rule can take it away
313
+ const publicGroup = (await store.groups()).find((group) => group.name === 'public');
314
+ const publicGrants = publicGroup
315
+ ? rules
316
+ .filter((rule) => rule.group === publicGroup.uid && rule.setting === 'Grant')
317
+ .map((rule) => rule.path)
318
+ : [];
319
+ return {
320
+ paths: grantableRoutes.filter((path) => routeAccess_js_1.ALWAYS_VISIBLE_ROUTES.includes(path) ||
321
+ publicGrants.includes(path) ||
322
+ (0, routeAccess_js_1.isRouteGranted)(mine, path, (0, routeAccess_js_1.topLevelRouteFor)(path)))
323
+ };
324
+ },
288
325
  async listPermissions(caller) {
289
326
  const reader = await requireCaller(store, caller);
290
327
  const rules = await store.permissions();
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ /**
3
+ * Which routes a caller may use, from the permission rules alone.
4
+ *
5
+ * This is the answer `GET /SASjsApi/info/myAuthorizedRoutes` gives, and it is the
6
+ * same decision the gate makes when the route is actually requested - a
7
+ * web interface that offers a route the gate then refuses is the bug this exists
8
+ * to prevent, so the two must not be two implementations. A host that gates its
9
+ * own middleware should call `isRouteGranted` rather than re-derive it.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.isRouteGranted = exports.topLevelRouteFor = exports.DEFAULT_GRANTABLE_ROUTES = exports.STATIC_AUTHORIZED_ROUTES = exports.ALWAYS_VISIBLE_ROUTES = exports.TOP_LEVEL_ROUTES = void 0;
13
+ /** The two top-level routes, which a rule can address to cover everything below. */
14
+ exports.TOP_LEVEL_ROUTES = ['/AppStream', '/SASjsApi'];
15
+ /**
16
+ * Routes any authenticated caller may use whatever the rules say. `/AppStream` is
17
+ * the navigation page whose job is to list the apps the caller may open, so
18
+ * gating it on a grant that lists them would hide the page that leads to them.
19
+ */
20
+ exports.ALWAYS_VISIBLE_ROUTES = ['/AppStream'];
21
+ /**
22
+ * The routes that accept a rule, before a host adds its own. A host with
23
+ * streamed applications extends this with `/AppStream/<app>` for each one, which
24
+ * is why the inventory is passed in rather than derived from a fixed list here.
25
+ */
26
+ exports.STATIC_AUTHORIZED_ROUTES = [
27
+ '/SASjsApi/code/execute',
28
+ '/SASjsApi/stp/execute',
29
+ '/SASjsApi/drive/deploy',
30
+ '/SASjsApi/drive/deploy/upload',
31
+ '/SASjsApi/drive/file',
32
+ '/SASjsApi/drive/folder',
33
+ '/SASjsApi/drive/fileTree',
34
+ '/SASjsApi/drive/rename'
35
+ ];
36
+ /** The route inventory when the host states no additions of its own. */
37
+ exports.DEFAULT_GRANTABLE_ROUTES = [
38
+ ...exports.TOP_LEVEL_ROUTES,
39
+ ...exports.STATIC_AUTHORIZED_ROUTES
40
+ ];
41
+ /**
42
+ * The top-level route that governs a request: `/SASjsApi/code/execute` is
43
+ * governed by `/SASjsApi`, and a rule on the top-level route applies to
44
+ * everything beneath it.
45
+ */
46
+ const topLevelRouteFor = (routePath) => exports.TOP_LEVEL_ROUTES.find((route) => routePath.toLowerCase().startsWith(route.toLowerCase())) ?? routePath;
47
+ exports.topLevelRouteFor = topLevelRouteFor;
48
+ const settingOn = (rules, path) => rules.find((rule) => rule.path === path)?.setting;
49
+ const GRANT = 'Grant';
50
+ const DENY = 'Deny';
51
+ /**
52
+ * Whether these rules grant access to `path`.
53
+ *
54
+ * The order is the documented one and the only implementation of it:
55
+ *
56
+ * 1. a rule addressed to the user on the path itself decides either way;
57
+ * 2. otherwise a rule addressed to the user on the top-level route decides;
58
+ * 3. otherwise a group grant on the path allows, and a group deny on the path
59
+ * blocks the top-level check - but a grant from another group still allows;
60
+ * 4. otherwise a group grant on the top-level route allows;
61
+ * 5. otherwise denied.
62
+ *
63
+ * A grant beats a deny at the same level, which is why step 3 looks at every
64
+ * group rule rather than stopping at the first deny.
65
+ */
66
+ const isRouteGranted = (rules, path, topLevelRoute = (0, exports.topLevelRouteFor)(path)) => {
67
+ const userSetting = settingOn(rules.user, path) ?? settingOn(rules.user, topLevelRoute);
68
+ if (userSetting)
69
+ return userSetting === GRANT;
70
+ const onPath = rules.group.filter((rule) => rule.path === path);
71
+ if (onPath.some((rule) => rule.setting === GRANT))
72
+ return true;
73
+ if (onPath.some((rule) => rule.setting === DENY))
74
+ return false;
75
+ return rules.group.some((rule) => rule.path === topLevelRoute && rule.setting === GRANT);
76
+ };
77
+ exports.isRouteGranted = isRouteGranted;
@@ -60,6 +60,8 @@ Object.defineProperty(exports, "TAG_CAPABILITIES", { enumerable: true, get: func
60
60
  var dispatch_js_1 = require("./host/dispatch.js");
61
61
  Object.defineProperty(exports, "dispatch", { enumerable: true, get: function () { return dispatch_js_1.dispatch; } });
62
62
  Object.defineProperty(exports, "HANDLED_ROUTES", { enumerable: true, get: function () { return dispatch_js_1.HANDLED_ROUTES; } });
63
+ // a host gating its own middleware reads the same decision the inventory reports
64
+ __exportStar(require("./identity/routeAccess.js"), exports);
63
65
  var service_js_2 = require("./identity/service.js");
64
66
  Object.defineProperty(exports, "createIdentityService", { enumerable: true, get: function () { return service_js_2.createIdentityService; } });
65
67
  var records_js_1 = require("./identity/records.js");
@@ -30,7 +30,6 @@ const EDITOR = 'Linting and formatting come from @sasjs/lint, which this package
30
30
  const 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.';
31
31
  const 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.";
32
32
  const HOST_CONFIG = "The body is the host's own configuration - ports, run times and providers - which a mock states for itself and this package cannot know.";
33
- const PERMISSIONS = "The route inventory is the host's permission model, which this package does not hold.";
34
33
  const HOST_OWNED = "The host owns this report: it describes the host's own process and files, which a browser build has no equivalent of.";
35
34
  const WEB_BUNDLE = 'The host serves its own web bundle; there is no API decision here for this package to answer.';
36
35
  const ARCHIVE = "Expanding the uploaded archive is the host's transport work - multer and a zip library on the server, `DecompressionStream` in a worker - and the host hands this package the tree it produces, which is the same tree `POST /SASjsApi/drive/deploy` takes.";
@@ -177,16 +176,12 @@ exports.OVERLAY = {
177
176
  // Info - what the host is, and what this caller may use
178
177
  'GET /SASjsApi/info': { implemented: false, reason: HOST_CONFIG },
179
178
  'GET /SASjsApi/info/authorizedRoutes': {
180
- implemented: false,
181
- // the route inventory reports what the CALLER may use, so it needs an
182
- // identity even though the rest of the Info tag needs nothing
183
- capabilities: ['identity'],
184
- reason: PERMISSIONS
179
+ implemented: true,
180
+ capabilities: ['identity']
185
181
  },
186
182
  'GET /SASjsApi/info/myAuthorizedRoutes': {
187
- implemented: false,
188
- capabilities: ['identity'],
189
- reason: PERMISSIONS
183
+ implemented: true,
184
+ capabilities: ['identity']
190
185
  },
191
186
  // Permission - the rules the inventory above is derived from
192
187
  'GET /SASjsApi/permission': {
@@ -1 +1 @@
1
- {"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../../src/corpus/records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAA;AAkB/B,qFAAqF;AACrF,eAAO,MAAM,eAAe,EAAE,eAG7B,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,eA0C9B,CAAA;AAOD,eAAO,MAAM,aAAa,EAAE,UAAU,EA4kCrC,CAAA"}
1
+ {"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../../src/corpus/records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAA;AAkB/B,qFAAqF;AACrF,eAAO,MAAM,eAAe,EAAE,eAG7B,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,eA0C9B,CAAA;AAOD,eAAO,MAAM,aAAa,EAAE,UAAU,EA4oCrC,CAAA"}
@@ -699,6 +699,70 @@ export const RECORD_CORPUS = [
699
699
  expect: { status: 404, error: 'Group not found.' }
700
700
  },
701
701
  // ------------------------------------------------------------ permissions
702
+ {
703
+ id: 'authorized-routes',
704
+ route: 'GET /SASjsApi/info/authorizedRoutes',
705
+ description: 'answers the routes that accept a rule, to any caller',
706
+ identity: ADMIN_SIGNED_IN,
707
+ request: { token: RESTRICTED },
708
+ expect: {
709
+ status: 200,
710
+ body: {
711
+ paths: [
712
+ '/AppStream',
713
+ '/SASjsApi',
714
+ '/SASjsApi/code/execute',
715
+ '/SASjsApi/stp/execute',
716
+ '/SASjsApi/drive/deploy',
717
+ '/SASjsApi/drive/deploy/upload',
718
+ '/SASjsApi/drive/file',
719
+ '/SASjsApi/drive/folder',
720
+ '/SASjsApi/drive/fileTree',
721
+ '/SASjsApi/drive/rename'
722
+ ]
723
+ }
724
+ }
725
+ },
726
+ {
727
+ id: 'my-authorized-routes-as-admin',
728
+ route: 'GET /SASjsApi/info/myAuthorizedRoutes',
729
+ description: 'answers every route to an administrator',
730
+ identity: ADMIN_SIGNED_IN,
731
+ request: { token: ADMIN },
732
+ expect: {
733
+ status: 200,
734
+ body: {
735
+ paths: [
736
+ '/AppStream',
737
+ '/SASjsApi',
738
+ '/SASjsApi/code/execute',
739
+ '/SASjsApi/stp/execute',
740
+ '/SASjsApi/drive/deploy',
741
+ '/SASjsApi/drive/deploy/upload',
742
+ '/SASjsApi/drive/file',
743
+ '/SASjsApi/drive/folder',
744
+ '/SASjsApi/drive/fileTree',
745
+ '/SASjsApi/drive/rename'
746
+ ]
747
+ }
748
+ }
749
+ },
750
+ {
751
+ id: 'my-authorized-routes-as-user',
752
+ route: 'GET /SASjsApi/info/myAuthorizedRoutes',
753
+ description: 'answers a caller only the routes its rules grant',
754
+ identity: ADMIN_SIGNED_IN,
755
+ request: { token: RESTRICTED },
756
+ expect: {
757
+ status: 200,
758
+ // /AppStream is visible whatever the rules say; the two grants come from
759
+ // DCGroup; /SASjsApi/code/execute is denied to this user by a user rule,
760
+ // and nothing grants the rest
761
+ body: {
762
+ paths: ['/AppStream', '/SASjsApi/stp/execute', '/SASjsApi/drive/file']
763
+ }
764
+ }
765
+ },
702
766
  {
703
767
  id: 'list-permissions-as-admin',
704
768
  route: 'GET /SASjsApi/permission',
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAOnD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,yFAAyF;IACzF,OAAO,CAAC,EAAE,aAAa,CAAA;CACxB;AAsCD,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,EAClB,SAAS,MAAM,EACf,UAAS,UAAe,KACvB,OAAO,CAAC,WAAW,CAqWrB,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,cAAc,EAAE,MAAM,EAwClC,CAAA"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAOnD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,yFAAyF;IACzF,OAAO,CAAC,EAAE,aAAa,CAAA;CACxB;AAsCD,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,EAClB,SAAS,MAAM,EACf,UAAS,UAAe,KACvB,OAAO,CAAC,WAAW,CAgXrB,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,cAAc,EAAE,MAAM,EA0ClC,CAAA"}
@@ -216,6 +216,16 @@ export const dispatch = async (services, routeId, request = {}) => {
216
216
  status: 200,
217
217
  body: await required().deleteUser(await caller(), { username: String(params.username ?? '') }, { password: body.password })
218
218
  };
219
+ case 'GET /SASjsApi/info/authorizedRoutes':
220
+ return {
221
+ status: 200,
222
+ body: await required().listAuthorizedRoutes(await caller())
223
+ };
224
+ case 'GET /SASjsApi/info/myAuthorizedRoutes':
225
+ return {
226
+ status: 200,
227
+ body: await required().myAuthorizedRoutes(await caller())
228
+ };
219
229
  case 'GET /SASjsApi/group':
220
230
  return { status: 200, body: await required().listGroups(await caller()) };
221
231
  case 'GET /SASjsApi/group/{uid}':
@@ -318,6 +328,8 @@ export const dispatch = async (services, routeId, request = {}) => {
318
328
  };
319
329
  /** Whether a route is one `dispatch` can answer today. */
320
330
  export const HANDLED_ROUTES = [
331
+ 'GET /SASjsApi/info/authorizedRoutes',
332
+ 'GET /SASjsApi/info/myAuthorizedRoutes',
321
333
  'GET /SASjsApi/drive/file',
322
334
  'POST /SASjsApi/drive/file',
323
335
  'PATCH /SASjsApi/drive/file',
@@ -44,6 +44,10 @@ export interface PermissionResponse {
44
44
  /** The uid of the group the rule is assigned to, when it is a group's. */
45
45
  group?: string;
46
46
  }
47
+ /** What `authorizedRoutes` and `myAuthorizedRoutes` answer. */
48
+ export interface AuthorizedRoutesResponse {
49
+ paths: string[];
50
+ }
47
51
  export interface RecordService {
48
52
  listUsers(caller: Subject | undefined): Promise<UserResponse[]>;
49
53
  getUser(caller: Subject | undefined, by: {
@@ -88,6 +92,13 @@ export interface RecordService {
88
92
  deleteGroup(caller: Subject | undefined, uid: string): Promise<void>;
89
93
  addUserToGroup(caller: Subject | undefined, groupUid: string, userUid: string): Promise<GroupDetailsResponse>;
90
94
  removeUserFromGroup(caller: Subject | undefined, groupUid: string, userUid: string): Promise<GroupDetailsResponse>;
95
+ /**
96
+ * The routes that accept a rule. Any authenticated caller may read it: it is
97
+ * the inventory, not a statement about what this caller holds.
98
+ */
99
+ listAuthorizedRoutes(caller: Subject | undefined): Promise<AuthorizedRoutesResponse>;
100
+ /** The routes this caller may use, from the rules that bear on them. */
101
+ myAuthorizedRoutes(caller: Subject | undefined): Promise<AuthorizedRoutesResponse>;
91
102
  listPermissions(caller: Subject | undefined): Promise<PermissionResponse[]>;
92
103
  createPermission(caller: Subject | undefined, body: {
93
104
  path: string;
@@ -109,5 +120,13 @@ export interface RecordService {
109
120
  }): Promise<void>;
110
121
  }
111
122
  export declare const sanitiseDisplayName: (value: unknown) => string;
112
- export declare const createRecordService: (store: IdentityStore) => RecordService;
123
+ export interface RecordServiceOptions {
124
+ /**
125
+ * The routes that accept a permission rule. Defaults to the ones this package
126
+ * knows; a host that streams applications adds `/AppStream/<app>` for each,
127
+ * because that set is the host's configuration and not a fact about the API.
128
+ */
129
+ grantableRoutes?: string[];
130
+ }
131
+ export declare const createRecordService: (store: IdentityStore, options?: RecordServiceOptions) => RecordService;
113
132
  //# sourceMappingURL=records.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../../src/identity/records.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"records.d.ts","sourceRoot":"","sources":["../../../src/identity/records.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,7 @@
14
14
  * the server sends.
15
15
  */
16
16
  import { ApiError } from '../types.js';
17
+ import { ALWAYS_VISIBLE_ROUTES, DEFAULT_GRANTABLE_ROUTES, isRouteGranted, topLevelRouteFor } from './routeAccess.js';
17
18
  /** The name the server gives the group every account is added to on creation. */
18
19
  const ALL_USERS_GROUP = 'all-users';
19
20
  /** The group whose membership the API refuses to change. */
@@ -79,7 +80,8 @@ const asGroup = (group) => ({
79
80
  name: group.name,
80
81
  description: group.description
81
82
  });
82
- export const createRecordService = (store) => {
83
+ export const createRecordService = (store, options = {}) => {
84
+ const grantableRoutes = options.grantableRoutes || DEFAULT_GRANTABLE_ROUTES;
83
85
  const groupDetails = async (group) => {
84
86
  const members = [];
85
87
  for (const uid of group.users) {
@@ -281,6 +283,41 @@ export const createRecordService = (store) => {
281
283
  throw new ApiError(400, 'Unable to update group.');
282
284
  return groupDetails(updated);
283
285
  },
286
+ async listAuthorizedRoutes(caller) {
287
+ // an identity is required because the route is about a caller's access,
288
+ // even though what it returns is the whole inventory
289
+ await requireCaller(store, caller);
290
+ return { paths: [...grantableRoutes] };
291
+ },
292
+ async myAuthorizedRoutes(caller) {
293
+ const reader = await requireCaller(store, caller);
294
+ // an administrator holds every route, and the always-visible ones are
295
+ // visible whatever the rules say
296
+ if (reader.isAdmin)
297
+ return { paths: [...grantableRoutes] };
298
+ const rules = await store.permissions();
299
+ const mine = {
300
+ user: rules
301
+ .filter((rule) => rule.user === reader.uid)
302
+ .map((rule) => ({ path: rule.path, setting: rule.setting })),
303
+ group: rules
304
+ .filter((rule) => rule.group !== undefined && reader.groups.includes(rule.group))
305
+ .map((rule) => ({ path: rule.path, setting: rule.setting }))
306
+ };
307
+ // a grant on the reserved public group admits any caller, and it is
308
+ // checked before the rules so no user or group rule can take it away
309
+ const publicGroup = (await store.groups()).find((group) => group.name === 'public');
310
+ const publicGrants = publicGroup
311
+ ? rules
312
+ .filter((rule) => rule.group === publicGroup.uid && rule.setting === 'Grant')
313
+ .map((rule) => rule.path)
314
+ : [];
315
+ return {
316
+ paths: grantableRoutes.filter((path) => ALWAYS_VISIBLE_ROUTES.includes(path) ||
317
+ publicGrants.includes(path) ||
318
+ isRouteGranted(mine, path, topLevelRouteFor(path)))
319
+ };
320
+ },
284
321
  async listPermissions(caller) {
285
322
  const reader = await requireCaller(store, caller);
286
323
  const rules = await store.permissions();
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Which routes a caller may use, from the permission rules alone.
3
+ *
4
+ * This is the answer `GET /SASjsApi/info/myAuthorizedRoutes` gives, and it is the
5
+ * same decision the gate makes when the route is actually requested - a
6
+ * web interface that offers a route the gate then refuses is the bug this exists
7
+ * to prevent, so the two must not be two implementations. A host that gates its
8
+ * own middleware should call `isRouteGranted` rather than re-derive it.
9
+ */
10
+ /** The two top-level routes, which a rule can address to cover everything below. */
11
+ export declare const TOP_LEVEL_ROUTES: string[];
12
+ /**
13
+ * Routes any authenticated caller may use whatever the rules say. `/AppStream` is
14
+ * the navigation page whose job is to list the apps the caller may open, so
15
+ * gating it on a grant that lists them would hide the page that leads to them.
16
+ */
17
+ export declare const ALWAYS_VISIBLE_ROUTES: string[];
18
+ /**
19
+ * The routes that accept a rule, before a host adds its own. A host with
20
+ * streamed applications extends this with `/AppStream/<app>` for each one, which
21
+ * is why the inventory is passed in rather than derived from a fixed list here.
22
+ */
23
+ export declare const STATIC_AUTHORIZED_ROUTES: string[];
24
+ /** The route inventory when the host states no additions of its own. */
25
+ export declare const DEFAULT_GRANTABLE_ROUTES: string[];
26
+ export interface RouteRule {
27
+ path: string;
28
+ setting: string;
29
+ }
30
+ /** The rules that can bear on one caller. */
31
+ export interface RouteRules {
32
+ user: RouteRule[];
33
+ group: RouteRule[];
34
+ }
35
+ /**
36
+ * The top-level route that governs a request: `/SASjsApi/code/execute` is
37
+ * governed by `/SASjsApi`, and a rule on the top-level route applies to
38
+ * everything beneath it.
39
+ */
40
+ export declare const topLevelRouteFor: (routePath: string) => string;
41
+ /**
42
+ * Whether these rules grant access to `path`.
43
+ *
44
+ * The order is the documented one and the only implementation of it:
45
+ *
46
+ * 1. a rule addressed to the user on the path itself decides either way;
47
+ * 2. otherwise a rule addressed to the user on the top-level route decides;
48
+ * 3. otherwise a group grant on the path allows, and a group deny on the path
49
+ * blocks the top-level check - but a grant from another group still allows;
50
+ * 4. otherwise a group grant on the top-level route allows;
51
+ * 5. otherwise denied.
52
+ *
53
+ * A grant beats a deny at the same level, which is why step 3 looks at every
54
+ * group rule rather than stopping at the first deny.
55
+ */
56
+ export declare const isRouteGranted: (rules: RouteRules, path: string, topLevelRoute?: string) => boolean;
57
+ //# sourceMappingURL=routeAccess.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routeAccess.d.ts","sourceRoot":"","sources":["../../../src/identity/routeAccess.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,UAA8B,CAAA;AAE3D;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,UAAiB,CAAA;AAEnD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,UASpC,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,UAGpC,CAAA;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,6CAA6C;AAC7C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,SAAS,EAAE,CAAA;IACjB,KAAK,EAAE,SAAS,EAAE,CAAA;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,WAAW,MAAM,KAAG,MAGrC,CAAA;AAQhB;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,UAAU,EACjB,MAAM,MAAM,EACZ,gBAAe,MAA+B,KAC7C,OAcF,CAAA"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Which routes a caller may use, from the permission rules alone.
3
+ *
4
+ * This is the answer `GET /SASjsApi/info/myAuthorizedRoutes` gives, and it is the
5
+ * same decision the gate makes when the route is actually requested - a
6
+ * web interface that offers a route the gate then refuses is the bug this exists
7
+ * to prevent, so the two must not be two implementations. A host that gates its
8
+ * own middleware should call `isRouteGranted` rather than re-derive it.
9
+ */
10
+ /** The two top-level routes, which a rule can address to cover everything below. */
11
+ export const TOP_LEVEL_ROUTES = ['/AppStream', '/SASjsApi'];
12
+ /**
13
+ * Routes any authenticated caller may use whatever the rules say. `/AppStream` is
14
+ * the navigation page whose job is to list the apps the caller may open, so
15
+ * gating it on a grant that lists them would hide the page that leads to them.
16
+ */
17
+ export const ALWAYS_VISIBLE_ROUTES = ['/AppStream'];
18
+ /**
19
+ * The routes that accept a rule, before a host adds its own. A host with
20
+ * streamed applications extends this with `/AppStream/<app>` for each one, which
21
+ * is why the inventory is passed in rather than derived from a fixed list here.
22
+ */
23
+ export const STATIC_AUTHORIZED_ROUTES = [
24
+ '/SASjsApi/code/execute',
25
+ '/SASjsApi/stp/execute',
26
+ '/SASjsApi/drive/deploy',
27
+ '/SASjsApi/drive/deploy/upload',
28
+ '/SASjsApi/drive/file',
29
+ '/SASjsApi/drive/folder',
30
+ '/SASjsApi/drive/fileTree',
31
+ '/SASjsApi/drive/rename'
32
+ ];
33
+ /** The route inventory when the host states no additions of its own. */
34
+ export const DEFAULT_GRANTABLE_ROUTES = [
35
+ ...TOP_LEVEL_ROUTES,
36
+ ...STATIC_AUTHORIZED_ROUTES
37
+ ];
38
+ /**
39
+ * The top-level route that governs a request: `/SASjsApi/code/execute` is
40
+ * governed by `/SASjsApi`, and a rule on the top-level route applies to
41
+ * everything beneath it.
42
+ */
43
+ export const topLevelRouteFor = (routePath) => TOP_LEVEL_ROUTES.find((route) => routePath.toLowerCase().startsWith(route.toLowerCase())) ?? routePath;
44
+ const settingOn = (rules, path) => rules.find((rule) => rule.path === path)?.setting;
45
+ const GRANT = 'Grant';
46
+ const DENY = 'Deny';
47
+ /**
48
+ * Whether these rules grant access to `path`.
49
+ *
50
+ * The order is the documented one and the only implementation of it:
51
+ *
52
+ * 1. a rule addressed to the user on the path itself decides either way;
53
+ * 2. otherwise a rule addressed to the user on the top-level route decides;
54
+ * 3. otherwise a group grant on the path allows, and a group deny on the path
55
+ * blocks the top-level check - but a grant from another group still allows;
56
+ * 4. otherwise a group grant on the top-level route allows;
57
+ * 5. otherwise denied.
58
+ *
59
+ * A grant beats a deny at the same level, which is why step 3 looks at every
60
+ * group rule rather than stopping at the first deny.
61
+ */
62
+ export const isRouteGranted = (rules, path, topLevelRoute = topLevelRouteFor(path)) => {
63
+ const userSetting = settingOn(rules.user, path) ?? settingOn(rules.user, topLevelRoute);
64
+ if (userSetting)
65
+ return userSetting === GRANT;
66
+ const onPath = rules.group.filter((rule) => rule.path === path);
67
+ if (onPath.some((rule) => rule.setting === GRANT))
68
+ return true;
69
+ if (onPath.some((rule) => rule.setting === DENY))
70
+ return false;
71
+ return rules.group.some((rule) => rule.path === topLevelRoute && rule.setting === GRANT);
72
+ };
@@ -25,10 +25,11 @@ export type { RouteMatch } from './routes/index.js';
25
25
  export { OVERLAY, TAG_CAPABILITIES } from './routes/overlay.js';
26
26
  export type { RouteOverlay } from './routes/overlay.js';
27
27
  export { dispatch, HANDLED_ROUTES } from './host/dispatch.js';
28
+ export * from './identity/routeAccess.js';
28
29
  export type { ApiRequest, ApiResponse, Services } from './host/dispatch.js';
29
30
  export { createIdentityService } from './identity/service.js';
30
31
  export { createRecordService, sanitiseDisplayName } from './identity/records.js';
31
- export type { GroupDetailsResponse, GroupResponse, PermissionResponse, RecordService, UserDetailsResponse, UserResponse } from './identity/records.js';
32
+ export type { AuthorizedRoutesResponse, GroupDetailsResponse, GroupResponse, PermissionResponse, RecordServiceOptions, RecordService, UserDetailsResponse, UserResponse } from './identity/records.js';
32
33
  export type { IdentityService, LoginResult, SessionResult } from './identity/service.js';
33
34
  export { MemoryIdentityStore } from './identity/memory-store.js';
34
35
  export { DEFAULT_IDENTITY, IDENTITY_FORMAT, parseIdentity } from './identity/fixture.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EAClB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAC7D,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAChF,YAAY,EACV,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACb,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,OAAO,EACP,OAAO,EACP,SAAS,EACT,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EACL,aAAa,EACb,eAAe,EACf,gBAAgB,EACjB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EAClB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAG7D,cAAc,2BAA2B,CAAA;AACzC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAChF,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACb,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,OAAO,EACP,OAAO,EACP,SAAS,EACT,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EACL,aAAa,EACb,eAAe,EACf,gBAAgB,EACjB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
@@ -21,6 +21,8 @@ export { MemoryDriveStore } from './drive/memory-store.js';
21
21
  export { ROUTES, routeById, findRoute } from './routes/index.js';
22
22
  export { OVERLAY, TAG_CAPABILITIES } from './routes/overlay.js';
23
23
  export { dispatch, HANDLED_ROUTES } from './host/dispatch.js';
24
+ // a host gating its own middleware reads the same decision the inventory reports
25
+ export * from './identity/routeAccess.js';
24
26
  export { createIdentityService } from './identity/service.js';
25
27
  export { createRecordService, sanitiseDisplayName } from './identity/records.js';
26
28
  export { MemoryIdentityStore } from './identity/memory-store.js';
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AAiDD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAuNhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AA8CD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAmNhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
@@ -27,7 +27,6 @@ const EDITOR = 'Linting and formatting come from @sasjs/lint, which this package
27
27
  const 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.';
28
28
  const 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.";
29
29
  const HOST_CONFIG = "The body is the host's own configuration - ports, run times and providers - which a mock states for itself and this package cannot know.";
30
- const PERMISSIONS = "The route inventory is the host's permission model, which this package does not hold.";
31
30
  const HOST_OWNED = "The host owns this report: it describes the host's own process and files, which a browser build has no equivalent of.";
32
31
  const WEB_BUNDLE = 'The host serves its own web bundle; there is no API decision here for this package to answer.';
33
32
  const ARCHIVE = "Expanding the uploaded archive is the host's transport work - multer and a zip library on the server, `DecompressionStream` in a worker - and the host hands this package the tree it produces, which is the same tree `POST /SASjsApi/drive/deploy` takes.";
@@ -174,16 +173,12 @@ export const OVERLAY = {
174
173
  // Info - what the host is, and what this caller may use
175
174
  'GET /SASjsApi/info': { implemented: false, reason: HOST_CONFIG },
176
175
  'GET /SASjsApi/info/authorizedRoutes': {
177
- implemented: false,
178
- // the route inventory reports what the CALLER may use, so it needs an
179
- // identity even though the rest of the Info tag needs nothing
180
- capabilities: ['identity'],
181
- reason: PERMISSIONS
176
+ implemented: true,
177
+ capabilities: ['identity']
182
178
  },
183
179
  'GET /SASjsApi/info/myAuthorizedRoutes': {
184
- implemented: false,
185
- capabilities: ['identity'],
186
- reason: PERMISSIONS
180
+ implemented: true,
181
+ capabilities: ['identity']
187
182
  },
188
183
  // Permission - the rules the inventory above is derived from
189
184
  'GET /SASjsApi/permission': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sasjs/api",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "The SASjs Server API, described once - a generated route table, the services behind it, and the conformance corpus both hosts run",
5
5
  "license": "MIT",
6
6
  "author": "SASjs",