@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.
- package/build/browser.js +178 -111
- package/build/cjs/corpus/records.js +83 -81
- package/build/cjs/host/dispatch.js +12 -0
- package/build/cjs/identity/fixture.js +1 -1
- package/build/cjs/identity/records.js +46 -20
- package/build/cjs/identity/routeAccess.js +77 -0
- package/build/cjs/index.js +2 -0
- package/build/cjs/routes/overlay.js +4 -9
- package/build/esm/corpus/records.d.ts.map +1 -1
- package/build/esm/corpus/records.js +83 -81
- package/build/esm/host/dispatch.d.ts.map +1 -1
- package/build/esm/host/dispatch.js +12 -0
- package/build/esm/identity/fixture.js +1 -1
- package/build/esm/identity/records.d.ts +33 -3
- package/build/esm/identity/records.d.ts.map +1 -1
- package/build/esm/identity/records.js +46 -20
- package/build/esm/identity/routeAccess.d.ts +57 -0
- package/build/esm/identity/routeAccess.d.ts.map +1 -0
- package/build/esm/identity/routeAccess.js +72 -0
- package/build/esm/index.d.ts +2 -1
- package/build/esm/index.d.ts.map +1 -1
- package/build/esm/index.js +2 -0
- package/build/esm/routes/overlay.d.ts.map +1 -1
- package/build/esm/routes/overlay.js +4 -9
- package/package.json +1 -1
|
@@ -404,7 +404,7 @@ exports.RECORD_CORPUS = [
|
|
|
404
404
|
request: {
|
|
405
405
|
token: RESTRICTED,
|
|
406
406
|
params: { username: 'user2' },
|
|
407
|
-
body: { password: '
|
|
407
|
+
body: { password: 'wrong123' }
|
|
408
408
|
},
|
|
409
409
|
expect: { status: 401, error: 'Invalid password.' }
|
|
410
410
|
},
|
|
@@ -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',
|
|
@@ -713,55 +777,24 @@ exports.RECORD_CORPUS = [
|
|
|
713
777
|
body: [
|
|
714
778
|
{
|
|
715
779
|
uid: 'rule-routes',
|
|
716
|
-
path: '/SASjsApi/
|
|
780
|
+
path: '/SASjsApi/drive/file',
|
|
717
781
|
type: 'Route',
|
|
718
782
|
setting: 'Grant',
|
|
719
|
-
group:
|
|
720
|
-
uid: 'group-data-controller',
|
|
721
|
-
name: 'DCGroup',
|
|
722
|
-
description: 'Data Controller users',
|
|
723
|
-
isActive: true,
|
|
724
|
-
users: [
|
|
725
|
-
{
|
|
726
|
-
uid: RESTRICTED_UID,
|
|
727
|
-
username: 'user2',
|
|
728
|
-
displayName: 'Demo User',
|
|
729
|
-
isAdmin: false
|
|
730
|
-
}
|
|
731
|
-
]
|
|
732
|
-
}
|
|
783
|
+
group: 'group-data-controller'
|
|
733
784
|
},
|
|
734
785
|
{
|
|
735
786
|
uid: 'rule-execute',
|
|
736
787
|
path: '/SASjsApi/stp/execute',
|
|
737
788
|
type: 'Route',
|
|
738
789
|
setting: 'Grant',
|
|
739
|
-
group:
|
|
740
|
-
uid: 'group-data-controller',
|
|
741
|
-
name: 'DCGroup',
|
|
742
|
-
description: 'Data Controller users',
|
|
743
|
-
isActive: true,
|
|
744
|
-
users: [
|
|
745
|
-
{
|
|
746
|
-
uid: RESTRICTED_UID,
|
|
747
|
-
username: 'user2',
|
|
748
|
-
displayName: 'Demo User',
|
|
749
|
-
isAdmin: false
|
|
750
|
-
}
|
|
751
|
-
]
|
|
752
|
-
}
|
|
790
|
+
group: 'group-data-controller'
|
|
753
791
|
},
|
|
754
792
|
{
|
|
755
793
|
uid: 'rule-code',
|
|
756
794
|
path: '/SASjsApi/code/execute',
|
|
757
795
|
type: 'Route',
|
|
758
796
|
setting: 'Deny',
|
|
759
|
-
user:
|
|
760
|
-
uid: RESTRICTED_UID,
|
|
761
|
-
username: 'user2',
|
|
762
|
-
displayName: 'Demo User',
|
|
763
|
-
isAdmin: false
|
|
764
|
-
}
|
|
797
|
+
user: RESTRICTED_UID
|
|
765
798
|
}
|
|
766
799
|
]
|
|
767
800
|
}
|
|
@@ -777,55 +810,24 @@ exports.RECORD_CORPUS = [
|
|
|
777
810
|
body: [
|
|
778
811
|
{
|
|
779
812
|
uid: 'rule-routes',
|
|
780
|
-
path: '/SASjsApi/
|
|
813
|
+
path: '/SASjsApi/drive/file',
|
|
781
814
|
type: 'Route',
|
|
782
815
|
setting: 'Grant',
|
|
783
|
-
group:
|
|
784
|
-
uid: 'group-data-controller',
|
|
785
|
-
name: 'DCGroup',
|
|
786
|
-
description: 'Data Controller users',
|
|
787
|
-
isActive: true,
|
|
788
|
-
users: [
|
|
789
|
-
{
|
|
790
|
-
uid: RESTRICTED_UID,
|
|
791
|
-
username: 'user2',
|
|
792
|
-
displayName: 'Demo User',
|
|
793
|
-
isAdmin: false
|
|
794
|
-
}
|
|
795
|
-
]
|
|
796
|
-
}
|
|
816
|
+
group: 'group-data-controller'
|
|
797
817
|
},
|
|
798
818
|
{
|
|
799
819
|
uid: 'rule-execute',
|
|
800
820
|
path: '/SASjsApi/stp/execute',
|
|
801
821
|
type: 'Route',
|
|
802
822
|
setting: 'Grant',
|
|
803
|
-
group:
|
|
804
|
-
uid: 'group-data-controller',
|
|
805
|
-
name: 'DCGroup',
|
|
806
|
-
description: 'Data Controller users',
|
|
807
|
-
isActive: true,
|
|
808
|
-
users: [
|
|
809
|
-
{
|
|
810
|
-
uid: RESTRICTED_UID,
|
|
811
|
-
username: 'user2',
|
|
812
|
-
displayName: 'Demo User',
|
|
813
|
-
isAdmin: false
|
|
814
|
-
}
|
|
815
|
-
]
|
|
816
|
-
}
|
|
823
|
+
group: 'group-data-controller'
|
|
817
824
|
},
|
|
818
825
|
{
|
|
819
826
|
uid: 'rule-code',
|
|
820
827
|
path: '/SASjsApi/code/execute',
|
|
821
828
|
type: 'Route',
|
|
822
829
|
setting: 'Deny',
|
|
823
|
-
user:
|
|
824
|
-
uid: RESTRICTED_UID,
|
|
825
|
-
username: 'user2',
|
|
826
|
-
displayName: 'Demo User',
|
|
827
|
-
isAdmin: false
|
|
828
|
-
}
|
|
830
|
+
user: RESTRICTED_UID
|
|
829
831
|
}
|
|
830
832
|
]
|
|
831
833
|
}
|
|
@@ -846,7 +848,7 @@ exports.RECORD_CORPUS = [
|
|
|
846
848
|
request: {
|
|
847
849
|
token: ADMIN,
|
|
848
850
|
body: {
|
|
849
|
-
path: '/SASjsApi/
|
|
851
|
+
path: '/SASjsApi/drive/folder',
|
|
850
852
|
type: 'Route',
|
|
851
853
|
setting: 'Grant',
|
|
852
854
|
principalType: 'user',
|
|
@@ -855,7 +857,7 @@ exports.RECORD_CORPUS = [
|
|
|
855
857
|
},
|
|
856
858
|
expect: {
|
|
857
859
|
status: 200,
|
|
858
|
-
partial: { path: '/SASjsApi/
|
|
860
|
+
partial: { path: '/SASjsApi/drive/folder', setting: 'Grant' }
|
|
859
861
|
}
|
|
860
862
|
},
|
|
861
863
|
{
|
|
@@ -866,7 +868,7 @@ exports.RECORD_CORPUS = [
|
|
|
866
868
|
request: {
|
|
867
869
|
token: ADMIN,
|
|
868
870
|
body: {
|
|
869
|
-
path: '/SASjsApi/
|
|
871
|
+
path: '/SASjsApi/drive/rename',
|
|
870
872
|
type: 'Route',
|
|
871
873
|
setting: 'Grant',
|
|
872
874
|
principalType: 'group',
|
|
@@ -876,9 +878,9 @@ exports.RECORD_CORPUS = [
|
|
|
876
878
|
expect: {
|
|
877
879
|
status: 200,
|
|
878
880
|
partial: {
|
|
879
|
-
path: '/SASjsApi/
|
|
881
|
+
path: '/SASjsApi/drive/rename',
|
|
880
882
|
setting: 'Grant',
|
|
881
|
-
group:
|
|
883
|
+
group: 'group-data-controller'
|
|
882
884
|
}
|
|
883
885
|
}
|
|
884
886
|
},
|
|
@@ -890,7 +892,7 @@ exports.RECORD_CORPUS = [
|
|
|
890
892
|
request: {
|
|
891
893
|
token: ADMIN,
|
|
892
894
|
body: {
|
|
893
|
-
path: '/SASjsApi/
|
|
895
|
+
path: '/SASjsApi/drive/deploy',
|
|
894
896
|
type: 'Route',
|
|
895
897
|
setting: 'Grant',
|
|
896
898
|
principalType: 'role',
|
|
@@ -910,7 +912,7 @@ exports.RECORD_CORPUS = [
|
|
|
910
912
|
request: {
|
|
911
913
|
token: ADMIN,
|
|
912
914
|
body: {
|
|
913
|
-
path: '/SASjsApi/
|
|
915
|
+
path: '/SASjsApi/drive/folder',
|
|
914
916
|
type: 'Route',
|
|
915
917
|
setting: 'Grant',
|
|
916
918
|
principalType: 'user',
|
|
@@ -930,7 +932,7 @@ exports.RECORD_CORPUS = [
|
|
|
930
932
|
request: {
|
|
931
933
|
token: RESTRICTED,
|
|
932
934
|
body: {
|
|
933
|
-
path: '/SASjsApi/
|
|
935
|
+
path: '/SASjsApi/drive/folder',
|
|
934
936
|
type: 'Route',
|
|
935
937
|
setting: 'Grant',
|
|
936
938
|
principalType: 'user',
|
|
@@ -947,7 +949,7 @@ exports.RECORD_CORPUS = [
|
|
|
947
949
|
request: {
|
|
948
950
|
token: ADMIN,
|
|
949
951
|
body: {
|
|
950
|
-
path: '/SASjsApi/
|
|
952
|
+
path: '/SASjsApi/drive/rename',
|
|
951
953
|
type: 'Route',
|
|
952
954
|
setting: 'Grant',
|
|
953
955
|
principalType: 'group',
|
|
@@ -964,7 +966,7 @@ exports.RECORD_CORPUS = [
|
|
|
964
966
|
request: {
|
|
965
967
|
token: ADMIN,
|
|
966
968
|
body: {
|
|
967
|
-
path: '/SASjsApi/
|
|
969
|
+
path: '/SASjsApi/drive/file',
|
|
968
970
|
type: 'Route',
|
|
969
971
|
setting: 'Grant',
|
|
970
972
|
principalType: 'group',
|
|
@@ -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) {
|
|
@@ -113,25 +115,14 @@ const createRecordService = (store) => {
|
|
|
113
115
|
const findBy = async (by) => by.uid !== undefined
|
|
114
116
|
? store.userById(by.uid)
|
|
115
117
|
: store.userByName(by.username ?? '');
|
|
116
|
-
const permissionDetails = async (permission) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const user = await store.userById(permission.user);
|
|
125
|
-
if (user)
|
|
126
|
-
response.user = asUser(user);
|
|
127
|
-
}
|
|
128
|
-
if (permission.group) {
|
|
129
|
-
const group = await store.groupById(permission.group);
|
|
130
|
-
if (group)
|
|
131
|
-
response.group = await groupDetails(group);
|
|
132
|
-
}
|
|
133
|
-
return response;
|
|
134
|
-
};
|
|
118
|
+
const permissionDetails = async (permission) => ({
|
|
119
|
+
uid: permission.uid,
|
|
120
|
+
path: permission.path,
|
|
121
|
+
type: permission.type,
|
|
122
|
+
setting: permission.setting,
|
|
123
|
+
...(permission.user ? { user: permission.user } : {}),
|
|
124
|
+
...(permission.group ? { group: permission.group } : {})
|
|
125
|
+
});
|
|
135
126
|
return {
|
|
136
127
|
async listUsers(caller) {
|
|
137
128
|
await requireCaller(store, caller);
|
|
@@ -296,6 +287,41 @@ const createRecordService = (store) => {
|
|
|
296
287
|
throw new types_js_1.ApiError(400, 'Unable to update group.');
|
|
297
288
|
return groupDetails(updated);
|
|
298
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
|
+
},
|
|
299
325
|
async listPermissions(caller) {
|
|
300
326
|
const reader = await requireCaller(store, caller);
|
|
301
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;
|
package/build/cjs/index.js
CHANGED
|
@@ -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:
|
|
181
|
-
|
|
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:
|
|
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,
|
|
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"}
|