@sasjs/api 0.7.2 → 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.
@@ -401,7 +401,7 @@ export const RECORD_CORPUS = [
401
401
  request: {
402
402
  token: RESTRICTED,
403
403
  params: { username: 'user2' },
404
- body: { password: 'wrong' }
404
+ body: { password: 'wrong123' }
405
405
  },
406
406
  expect: { status: 401, error: 'Invalid password.' }
407
407
  },
@@ -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',
@@ -710,55 +774,24 @@ export const RECORD_CORPUS = [
710
774
  body: [
711
775
  {
712
776
  uid: 'rule-routes',
713
- path: '/SASjsApi/info',
777
+ path: '/SASjsApi/drive/file',
714
778
  type: 'Route',
715
779
  setting: 'Grant',
716
- group: {
717
- uid: 'group-data-controller',
718
- name: 'DCGroup',
719
- description: 'Data Controller users',
720
- isActive: true,
721
- users: [
722
- {
723
- uid: RESTRICTED_UID,
724
- username: 'user2',
725
- displayName: 'Demo User',
726
- isAdmin: false
727
- }
728
- ]
729
- }
780
+ group: 'group-data-controller'
730
781
  },
731
782
  {
732
783
  uid: 'rule-execute',
733
784
  path: '/SASjsApi/stp/execute',
734
785
  type: 'Route',
735
786
  setting: 'Grant',
736
- group: {
737
- uid: 'group-data-controller',
738
- name: 'DCGroup',
739
- description: 'Data Controller users',
740
- isActive: true,
741
- users: [
742
- {
743
- uid: RESTRICTED_UID,
744
- username: 'user2',
745
- displayName: 'Demo User',
746
- isAdmin: false
747
- }
748
- ]
749
- }
787
+ group: 'group-data-controller'
750
788
  },
751
789
  {
752
790
  uid: 'rule-code',
753
791
  path: '/SASjsApi/code/execute',
754
792
  type: 'Route',
755
793
  setting: 'Deny',
756
- user: {
757
- uid: RESTRICTED_UID,
758
- username: 'user2',
759
- displayName: 'Demo User',
760
- isAdmin: false
761
- }
794
+ user: RESTRICTED_UID
762
795
  }
763
796
  ]
764
797
  }
@@ -774,55 +807,24 @@ export const RECORD_CORPUS = [
774
807
  body: [
775
808
  {
776
809
  uid: 'rule-routes',
777
- path: '/SASjsApi/info',
810
+ path: '/SASjsApi/drive/file',
778
811
  type: 'Route',
779
812
  setting: 'Grant',
780
- group: {
781
- uid: 'group-data-controller',
782
- name: 'DCGroup',
783
- description: 'Data Controller users',
784
- isActive: true,
785
- users: [
786
- {
787
- uid: RESTRICTED_UID,
788
- username: 'user2',
789
- displayName: 'Demo User',
790
- isAdmin: false
791
- }
792
- ]
793
- }
813
+ group: 'group-data-controller'
794
814
  },
795
815
  {
796
816
  uid: 'rule-execute',
797
817
  path: '/SASjsApi/stp/execute',
798
818
  type: 'Route',
799
819
  setting: 'Grant',
800
- group: {
801
- uid: 'group-data-controller',
802
- name: 'DCGroup',
803
- description: 'Data Controller users',
804
- isActive: true,
805
- users: [
806
- {
807
- uid: RESTRICTED_UID,
808
- username: 'user2',
809
- displayName: 'Demo User',
810
- isAdmin: false
811
- }
812
- ]
813
- }
820
+ group: 'group-data-controller'
814
821
  },
815
822
  {
816
823
  uid: 'rule-code',
817
824
  path: '/SASjsApi/code/execute',
818
825
  type: 'Route',
819
826
  setting: 'Deny',
820
- user: {
821
- uid: RESTRICTED_UID,
822
- username: 'user2',
823
- displayName: 'Demo User',
824
- isAdmin: false
825
- }
827
+ user: RESTRICTED_UID
826
828
  }
827
829
  ]
828
830
  }
@@ -843,7 +845,7 @@ export const RECORD_CORPUS = [
843
845
  request: {
844
846
  token: ADMIN,
845
847
  body: {
846
- path: '/SASjsApi/code/lint',
848
+ path: '/SASjsApi/drive/folder',
847
849
  type: 'Route',
848
850
  setting: 'Grant',
849
851
  principalType: 'user',
@@ -852,7 +854,7 @@ export const RECORD_CORPUS = [
852
854
  },
853
855
  expect: {
854
856
  status: 200,
855
- partial: { path: '/SASjsApi/code/lint', setting: 'Grant' }
857
+ partial: { path: '/SASjsApi/drive/folder', setting: 'Grant' }
856
858
  }
857
859
  },
858
860
  {
@@ -863,7 +865,7 @@ export const RECORD_CORPUS = [
863
865
  request: {
864
866
  token: ADMIN,
865
867
  body: {
866
- path: '/SASjsApi/code/lint',
868
+ path: '/SASjsApi/drive/rename',
867
869
  type: 'Route',
868
870
  setting: 'Grant',
869
871
  principalType: 'group',
@@ -873,9 +875,9 @@ export const RECORD_CORPUS = [
873
875
  expect: {
874
876
  status: 200,
875
877
  partial: {
876
- path: '/SASjsApi/code/lint',
878
+ path: '/SASjsApi/drive/rename',
877
879
  setting: 'Grant',
878
- group: { uid: 'group-data-controller', name: 'DCGroup' }
880
+ group: 'group-data-controller'
879
881
  }
880
882
  }
881
883
  },
@@ -887,7 +889,7 @@ export const RECORD_CORPUS = [
887
889
  request: {
888
890
  token: ADMIN,
889
891
  body: {
890
- path: '/SASjsApi/code/lint',
892
+ path: '/SASjsApi/drive/deploy',
891
893
  type: 'Route',
892
894
  setting: 'Grant',
893
895
  principalType: 'role',
@@ -907,7 +909,7 @@ export const RECORD_CORPUS = [
907
909
  request: {
908
910
  token: ADMIN,
909
911
  body: {
910
- path: '/SASjsApi/code/lint',
912
+ path: '/SASjsApi/drive/folder',
911
913
  type: 'Route',
912
914
  setting: 'Grant',
913
915
  principalType: 'user',
@@ -927,7 +929,7 @@ export const RECORD_CORPUS = [
927
929
  request: {
928
930
  token: RESTRICTED,
929
931
  body: {
930
- path: '/SASjsApi/code/lint',
932
+ path: '/SASjsApi/drive/folder',
931
933
  type: 'Route',
932
934
  setting: 'Grant',
933
935
  principalType: 'user',
@@ -944,7 +946,7 @@ export const RECORD_CORPUS = [
944
946
  request: {
945
947
  token: ADMIN,
946
948
  body: {
947
- path: '/SASjsApi/code/lint',
949
+ path: '/SASjsApi/drive/rename',
948
950
  type: 'Route',
949
951
  setting: 'Grant',
950
952
  principalType: 'group',
@@ -961,7 +963,7 @@ export const RECORD_CORPUS = [
961
963
  request: {
962
964
  token: ADMIN,
963
965
  body: {
964
- path: '/SASjsApi/info',
966
+ path: '/SASjsApi/drive/file',
965
967
  type: 'Route',
966
968
  setting: 'Grant',
967
969
  principalType: 'group',
@@ -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',
@@ -54,7 +54,7 @@ export const DEFAULT_IDENTITY = {
54
54
  permissions: [
55
55
  {
56
56
  uid: 'rule-routes',
57
- path: '/SASjsApi/info',
57
+ path: '/SASjsApi/drive/file',
58
58
  type: 'Route',
59
59
  setting: 'Grant',
60
60
  group: 'group-data-controller'
@@ -25,13 +25,28 @@ export interface GroupDetailsResponse extends GroupResponse {
25
25
  isActive: boolean;
26
26
  users: UserResponse[];
27
27
  }
28
+ /**
29
+ * A permission rule as the routes report one.
30
+ *
31
+ * The principal is the identifier, not the whole account: a rule is a row in a
32
+ * table, and a host on any database can produce `user` or `group` as a uid
33
+ * without a join or a populate step. A client that wants the name asks for the
34
+ * account it names, which it can do whether the store is Mongo, a browser's
35
+ * IndexedDB or a file.
36
+ */
28
37
  export interface PermissionResponse {
29
38
  uid: string;
30
39
  path: string;
31
40
  type: string;
32
41
  setting: string;
33
- user?: UserResponse;
34
- group?: GroupDetailsResponse;
42
+ /** The uid of the account the rule is assigned to, when it is a user's. */
43
+ user?: string;
44
+ /** The uid of the group the rule is assigned to, when it is a group's. */
45
+ group?: string;
46
+ }
47
+ /** What `authorizedRoutes` and `myAuthorizedRoutes` answer. */
48
+ export interface AuthorizedRoutesResponse {
49
+ paths: string[];
35
50
  }
36
51
  export interface RecordService {
37
52
  listUsers(caller: Subject | undefined): Promise<UserResponse[]>;
@@ -77,6 +92,13 @@ export interface RecordService {
77
92
  deleteGroup(caller: Subject | undefined, uid: string): Promise<void>;
78
93
  addUserToGroup(caller: Subject | undefined, groupUid: string, userUid: string): Promise<GroupDetailsResponse>;
79
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>;
80
102
  listPermissions(caller: Subject | undefined): Promise<PermissionResponse[]>;
81
103
  createPermission(caller: Subject | undefined, body: {
82
104
  path: string;
@@ -98,5 +120,13 @@ export interface RecordService {
98
120
  }): Promise<void>;
99
121
  }
100
122
  export declare const sanitiseDisplayName: (value: unknown) => string;
101
- 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;
102
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) {
@@ -109,25 +111,14 @@ export const createRecordService = (store) => {
109
111
  const findBy = async (by) => by.uid !== undefined
110
112
  ? store.userById(by.uid)
111
113
  : store.userByName(by.username ?? '');
112
- const permissionDetails = async (permission) => {
113
- const response = {
114
- uid: permission.uid,
115
- path: permission.path,
116
- type: permission.type,
117
- setting: permission.setting
118
- };
119
- if (permission.user) {
120
- const user = await store.userById(permission.user);
121
- if (user)
122
- response.user = asUser(user);
123
- }
124
- if (permission.group) {
125
- const group = await store.groupById(permission.group);
126
- if (group)
127
- response.group = await groupDetails(group);
128
- }
129
- return response;
130
- };
114
+ const permissionDetails = async (permission) => ({
115
+ uid: permission.uid,
116
+ path: permission.path,
117
+ type: permission.type,
118
+ setting: permission.setting,
119
+ ...(permission.user ? { user: permission.user } : {}),
120
+ ...(permission.group ? { group: permission.group } : {})
121
+ });
131
122
  return {
132
123
  async listUsers(caller) {
133
124
  await requireCaller(store, caller);
@@ -292,6 +283,41 @@ export const createRecordService = (store) => {
292
283
  throw new ApiError(400, 'Unable to update group.');
293
284
  return groupDetails(updated);
294
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
+ },
295
321
  async listPermissions(caller) {
296
322
  const reader = await requireCaller(store, caller);
297
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"}