mirror of
https://github.com/sasjs/server.git
synced 2025-12-11 03:34:35 +00:00
Compare commits
278 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c44ec35b3d | ||
|
|
77fac663c5 | ||
| 7df9588e66 | |||
| 6a520f5b26 | |||
|
|
70c3834022 | ||
|
|
dbf6c7de08 | ||
|
|
d49ea47bd7 | ||
|
|
be4951d112 | ||
|
|
c116b263d9 | ||
|
|
b4436bad0d | ||
|
|
5e325522f4 | ||
|
|
e576fad8f4 | ||
| eda8e56bb0 | |||
|
|
bee4f215d2 | ||
|
|
100f138f98 | ||
| 6ffaa7e9e2 | |||
|
|
a433786011 | ||
|
|
1adff9a783 | ||
| 1435e380be | |||
| e099f2e678 | |||
| ddd155ba01 | |||
| 9936241815 | |||
| 570995e572 | |||
| 462829fd9a | |||
| c1c0554de2 | |||
| bd3aff9a7b | |||
| a1e255e0c7 | |||
| 0dae034f17 | |||
| 89048ce943 | |||
| a82cabb001 | |||
| c4066d32a0 | |||
|
|
6a44cd69d9 | ||
|
|
e607115995 | ||
| edab51c519 | |||
|
|
081cc3102c | ||
|
|
b19aa1eba4 | ||
| 2c31922f58 | |||
|
|
4d7a571a6e | ||
|
|
a373a4eb5f | ||
| 5e3ce8a98f | |||
|
|
737b34567e | ||
|
|
6373442f83 | ||
|
|
3de59ac4f8 | ||
|
|
941988cd7c | ||
| 158f044363 | |||
|
|
02ae041a81 | ||
|
|
c4c84b1537 | ||
| b3402ea80a | |||
|
|
abe942e697 | ||
|
|
faf2edb111 | ||
| 5bec453e89 | |||
| 7f2174dd2c | |||
| 2bae52e307 | |||
|
|
b243e62ece | ||
|
|
88c3056e97 | ||
| 203303b659 | |||
| 835709bd36 | |||
| 69f2576ee6 | |||
|
|
305077f36e | ||
|
|
96eca3a35d | ||
|
|
0f5c815c25 | ||
|
|
acccef1e99 | ||
| abc34ea047 | |||
| 71c429b093 | |||
|
|
c126f2d5d9 | ||
|
|
34dd95d16e | ||
| 1192583843 | |||
|
|
518815acf1 | ||
|
|
80b7e14ed5 | ||
| 23c997b3be | |||
| 39ba995355 | |||
|
|
0e081e024b | ||
|
|
6a84bd0387 | ||
| 98d177a691 | |||
| 4dcee4b3c3 | |||
|
|
4ffc1ec6a9 | ||
|
|
5a1d168e83 | ||
|
|
515c976685 | ||
| 112431a1b7 | |||
| c26485afec | |||
| 1d48f8856b | |||
| 68758aa616 | |||
| 8b8c43c21b | |||
| 4581f32534 | |||
| b47e74a7e1 | |||
| b27d684145 | |||
|
|
6b666d5554 | ||
|
|
b5f0911858 | ||
| b86ba5b8a3 | |||
| 200f6c596a | |||
|
|
1b7ccda6e9 | ||
|
|
532035d835 | ||
|
|
7ae862c5ce | ||
|
|
ab5858b8af | ||
|
|
a39f5dd9f1 | ||
|
|
3ea444756c | ||
|
|
96399ecbbe | ||
| bb054938c5 | |||
|
|
fb6a556630 | ||
|
|
9dbd8e16bd | ||
| fe07c41f5f | |||
| acc25cbd68 | |||
| 4ca61feda6 | |||
| abd5c64b4a | |||
| 2413c05fea | |||
|
|
4c874c2c39 | ||
|
|
d819d79bc9 | ||
| c51b50428f | |||
|
|
e10a0554f0 | ||
|
|
337e2eb2a0 | ||
| 66f8e7840b | |||
| 1c9d167f86 | |||
|
|
7e684b54a6 | ||
|
|
aafda2922b | ||
| 418bf41e38 | |||
| 81f0b03b09 | |||
| fe5ae44aab | |||
| 36be3a7d5e | |||
| 6434123401 | |||
|
|
0a6b972c65 | ||
|
|
be11707042 | ||
| 2412622367 | |||
|
|
de3a190a8d | ||
|
|
d5daafc6ed | ||
|
|
b1a2677b8c | ||
|
|
94072c3d24 | ||
|
|
b64c0c12da | ||
|
|
79bc7b0e28 | ||
|
|
fda0e0b57d | ||
|
|
14731e8824 | ||
|
|
258cc35f14 | ||
|
|
2295a518f0 | ||
|
|
1e5d621817 | ||
| 4d64420c45 | |||
|
|
799339de30 | ||
|
|
042ed41189 | ||
| 424f0fc1fa | |||
|
|
deafebde05 | ||
|
|
b66dc86b01 | ||
|
|
3bb05974d2 | ||
|
|
d1c1a59e91 | ||
|
|
668aff83fd | ||
| 3fc06b80fc | |||
| bbd7786c6c | |||
| 68f0c5c588 | |||
|
|
69ddf313b8 | ||
|
|
65e404cdbd | ||
| a14266077d | |||
|
|
fda6ad6356 | ||
|
|
fe3e5088f8 | ||
| f915c51b07 | |||
|
|
375f924f45 | ||
|
|
72329e30ed | ||
| 40f95f9072 | |||
|
|
58e8a869ef | ||
|
|
b558a3d01d | ||
| 249604384e | |||
|
|
056a436e10 | ||
|
|
06d59c618c | ||
|
|
a0e7875ae6 | ||
|
|
24966e695a | ||
|
|
5c40d8a342 | ||
| 6f5566dabb | |||
| d93470d183 | |||
| 330c020933 | |||
|
|
a810f6c7cf | ||
|
|
5d6c6086b4 | ||
|
|
0edcbdcefc | ||
|
|
ea0222f218 | ||
| edc2e2a302 | |||
|
|
efd2e1450e | ||
|
|
1092a73c10 | ||
| 9977c9d161 | |||
|
|
5c0eff5197 | ||
|
|
3bda991a58 | ||
| 0327f7c6ec | |||
| 92549402eb | |||
|
|
b88c911527 | ||
|
|
8b12f31060 | ||
|
|
e65cba9af0 | ||
| 0749d65173 | |||
|
|
a9c9b734f5 | ||
|
|
39da41c9f1 | ||
| 662b2ca36a | |||
| 16b7aa6abb | |||
| 4560ef942f | |||
| 06d3b17154 | |||
| d6651bbdbe | |||
| b9d032f148 | |||
|
|
70655e74d3 | ||
|
|
cb82fea0d8 | ||
| b9a596616d | |||
|
|
72a5393be3 | ||
|
|
769a840e9f | ||
| 730c7c52ac | |||
| ee2db276bb | |||
|
|
d0a24aacb6 | ||
|
|
57dfdf89a4 | ||
|
|
393b5eaf99 | ||
|
|
7477326b22 | ||
|
|
76bf84316e | ||
|
|
e355276e44 | ||
|
|
a3a9e3bd9f | ||
|
|
9f06080348 | ||
|
|
4bbf9cfdb3 | ||
|
|
e8e71fcde9 | ||
|
|
e63271a67a | ||
| 7633608318 | |||
|
|
e67d27d264 | ||
|
|
53033ccc96 | ||
|
|
6131ed1cbe | ||
|
|
5d624e3399 | ||
| ee17d37aa1 | |||
| 572fe22d50 | |||
| 091268bf58 | |||
| 71a4a48443 | |||
| 3b188cd724 | |||
| eeba2328c0 | |||
| 0a0ba2cca5 | |||
|
|
476f834a80 | ||
|
|
8b8739a873 | ||
| bce83cb6fb | |||
| 3a3c90d9e6 | |||
|
|
e63eaa5302 | ||
|
|
65de1bb175 | ||
|
|
a5ee2f2923 | ||
| 98ea2ac9b9 | |||
|
|
e94c56b23f | ||
|
|
64f80e958d | ||
| bd97363c13 | |||
| 02e88ae728 | |||
| 882bedd5d5 | |||
| 8780b800a3 | |||
| 4c11082796 | |||
| a9b25b8880 | |||
| b06993ab9e | |||
|
|
f736e67517 | ||
|
|
0f4a60c0c7 | ||
|
|
f8bb7327a8 | ||
|
|
abce135da2 | ||
|
|
a6c014946a | ||
| f27ac51fc4 | |||
|
|
cb5be1be21 | ||
|
|
d90fa9e5dd | ||
| d99fdd1ec7 | |||
|
|
399b5edad0 | ||
|
|
1dbc12e96b | ||
| e215958b8b | |||
| 9227cd449d | |||
| c67d3ee2f1 | |||
| 6ef40b954a | |||
|
|
0d913baff1 | ||
|
|
3671736c3d | ||
| 34cd84d8a9 | |||
|
|
f7fcc7741a | ||
|
|
18052fdbf6 | ||
|
|
5966016853 | ||
|
|
87c03c5f8d | ||
| 7a162eda8f | |||
| 754704bca8 | |||
|
|
77f8d30baf | ||
|
|
78bea7c154 | ||
|
|
9c3b155c12 | ||
|
|
98e501334f | ||
|
|
bbfd53e79e | ||
| 254bc07da7 | |||
| f978814ca7 | |||
| 68515f95a6 | |||
| d3a516c36e | |||
| c3e3befc17 | |||
|
|
275de9478e | ||
|
|
1a3ef62cb2 | ||
| 79b7827b7c | |||
| 37e1aa9b61 | |||
| 7e504008b7 | |||
| 5d5a9d3788 | |||
| b7dff341f0 | |||
| 8a3054e19a |
475
CHANGELOG.md
475
CHANGELOG.md
@@ -1,3 +1,478 @@
|
|||||||
|
## [0.33.3](https://github.com/sasjs/server/compare/v0.33.2...v0.33.3) (2023-04-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* use RateLimiterMemory instead of RateLimiterMongo ([6a520f5](https://github.com/sasjs/server/commit/6a520f5b26a3e2ed6345721b30ff4e3d9bfa903d))
|
||||||
|
|
||||||
|
## [0.33.2](https://github.com/sasjs/server/compare/v0.33.1...v0.33.2) (2023-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removing print redirection pending full [#274](https://github.com/sasjs/server/issues/274) fix ([d49ea47](https://github.com/sasjs/server/commit/d49ea47bd7a2add42bdb9a717082201f29e16597))
|
||||||
|
|
||||||
|
## [0.33.1](https://github.com/sasjs/server/compare/v0.33.0...v0.33.1) (2023-04-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* applying nologo only for sas.exe ([b4436ba](https://github.com/sasjs/server/commit/b4436bad0d24d5b5a402272632db1739b1018c90)), closes [#352](https://github.com/sasjs/server/issues/352)
|
||||||
|
|
||||||
|
# [0.33.0](https://github.com/sasjs/server/compare/v0.32.0...v0.33.0) (2023-04-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* option to reset admin password on startup ([eda8e56](https://github.com/sasjs/server/commit/eda8e56bb0ea20fdaacabbbe7dcf1e3ea7bd215a))
|
||||||
|
|
||||||
|
# [0.32.0](https://github.com/sasjs/server/compare/v0.31.0...v0.32.0) (2023-04-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add an api endpoint for admin to get list of client ids ([6ffaa7e](https://github.com/sasjs/server/commit/6ffaa7e9e2a62c083bb9fcc3398dcbed10cebdb1))
|
||||||
|
|
||||||
|
# [0.31.0](https://github.com/sasjs/server/compare/v0.30.3...v0.31.0) (2023-03-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* prevent brute force attack by rate limiting login endpoint ([a82cabb](https://github.com/sasjs/server/commit/a82cabb00134c79c5ee77afd1b1628a1f768e050))
|
||||||
|
|
||||||
|
## [0.30.3](https://github.com/sasjs/server/compare/v0.30.2...v0.30.3) (2023-03-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add location.pathname to location.origin conditionally ([edab51c](https://github.com/sasjs/server/commit/edab51c51997f17553e037dc7c2b5e5fa6ea8ffe))
|
||||||
|
|
||||||
|
## [0.30.2](https://github.com/sasjs/server/compare/v0.30.1...v0.30.2) (2023-03-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** add path to base in launch program url ([2c31922](https://github.com/sasjs/server/commit/2c31922f58a8aa20d7fa6bfc95b53a350f90c798))
|
||||||
|
|
||||||
|
## [0.30.1](https://github.com/sasjs/server/compare/v0.30.0...v0.30.1) (2023-03-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** add proper base url in axios.defaults ([5e3ce8a](https://github.com/sasjs/server/commit/5e3ce8a98f1825e14c1d26d8da0c9821beeff7b3))
|
||||||
|
|
||||||
|
# [0.30.0](https://github.com/sasjs/server/compare/v0.29.0...v0.30.0) (2023-02-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* lint + remove default settings ([3de59ac](https://github.com/sasjs/server/commit/3de59ac4f8e3d95cad31f09e6963bd04c4811f26))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add new env config DB_TYPE ([158f044](https://github.com/sasjs/server/commit/158f044363abf2576c8248f0ca9da4bc9cb7e9d8))
|
||||||
|
|
||||||
|
# [0.29.0](https://github.com/sasjs/server/compare/v0.28.7...v0.29.0) (2023-02-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add /SASjsApi endpoint in permissions ([b3402ea](https://github.com/sasjs/server/commit/b3402ea80afb8802eee8b8b6cbbbcc29903424bc))
|
||||||
|
|
||||||
|
## [0.28.7](https://github.com/sasjs/server/compare/v0.28.6...v0.28.7) (2023-02-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add user to all users group on user creation ([2bae52e](https://github.com/sasjs/server/commit/2bae52e307327d7ee4a94b19d843abdc0ccec9d1))
|
||||||
|
|
||||||
|
## [0.28.6](https://github.com/sasjs/server/compare/v0.28.5...v0.28.6) (2023-01-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* show loading spinner on login screen while request is in process ([69f2576](https://github.com/sasjs/server/commit/69f2576ee6d3d7b7f3325922a88656d511e3ac88))
|
||||||
|
|
||||||
|
## [0.28.5](https://github.com/sasjs/server/compare/v0.28.4...v0.28.5) (2023-01-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding NOPRNGETLIST system option for faster startup ([96eca3a](https://github.com/sasjs/server/commit/96eca3a35dce4521150257ee019beb4488c8a08f))
|
||||||
|
|
||||||
|
## [0.28.4](https://github.com/sasjs/server/compare/v0.28.3...v0.28.4) (2022-12-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* replace main class with container class ([71c429b](https://github.com/sasjs/server/commit/71c429b093b91e2444ae75d946579dccc2e48636))
|
||||||
|
|
||||||
|
## [0.28.3](https://github.com/sasjs/server/compare/v0.28.2...v0.28.3) (2022-12-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* stringify json file ([1192583](https://github.com/sasjs/server/commit/1192583843d7efd1a6ab6943207f394c3ae966be))
|
||||||
|
|
||||||
|
## [0.28.2](https://github.com/sasjs/server/compare/v0.28.1...v0.28.2) (2022-12-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* execute child process asyncronously ([23c997b](https://github.com/sasjs/server/commit/23c997b3beabeb6b733ae893031d2f1a48f28ad2))
|
||||||
|
* JS / Python / R session folders should be NEW folders, not existing SAS folders ([39ba995](https://github.com/sasjs/server/commit/39ba995355daa24bb7ab22720f8fc57d2dc85f40))
|
||||||
|
|
||||||
|
## [0.28.1](https://github.com/sasjs/server/compare/v0.28.0...v0.28.1) (2022-11-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update the content type header after the program has been executed ([4dcee4b](https://github.com/sasjs/server/commit/4dcee4b3c3950d402220b8f451c50ad98a317d83))
|
||||||
|
|
||||||
|
# [0.28.0](https://github.com/sasjs/server/compare/v0.27.0...v0.28.0) (2022-11-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update the response header of request to stp/execute routes ([112431a](https://github.com/sasjs/server/commit/112431a1b7461989c04100418d67d975a2a8f354))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** add the api endpoint for updating user password ([4581f32](https://github.com/sasjs/server/commit/4581f325344eb68c5df5a28492f132312f15bb5c))
|
||||||
|
* ask for updated password on first login ([1d48f88](https://github.com/sasjs/server/commit/1d48f8856b1fbbf3ef868914558333190e04981f))
|
||||||
|
* **web:** add the UI for updating user password ([8b8c43c](https://github.com/sasjs/server/commit/8b8c43c21bde5379825c5ec44ecd81a92425f605))
|
||||||
|
|
||||||
|
# [0.27.0](https://github.com/sasjs/server/compare/v0.26.2...v0.27.0) (2022-11-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* on startup add webout.sas file in sasautos folder ([200f6c5](https://github.com/sasjs/server/commit/200f6c596a6e732d799ed408f1f0fd92f216ba58))
|
||||||
|
|
||||||
|
## [0.26.2](https://github.com/sasjs/server/compare/v0.26.1...v0.26.2) (2022-11-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* comments ([7ae862c](https://github.com/sasjs/server/commit/7ae862c5ce720e9483d4728f4295dede4f849436))
|
||||||
|
|
||||||
|
## [0.26.1](https://github.com/sasjs/server/compare/v0.26.0...v0.26.1) (2022-11-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* change the expiration of access/refresh tokens from days to seconds ([bb05493](https://github.com/sasjs/server/commit/bb054938c5bd0535ae6b9da93ba0b14f9b80ddcd))
|
||||||
|
|
||||||
|
# [0.26.0](https://github.com/sasjs/server/compare/v0.25.1...v0.26.0) (2022-11-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** dispose monaco editor actions in return of useEffect ([acc25cb](https://github.com/sasjs/server/commit/acc25cbd686952d3f1c65e57aefcebe1cb859cc7))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* make access token duration configurable when creating client/secret ([2413c05](https://github.com/sasjs/server/commit/2413c05fea3960f7e5c3c8b7b2f85d61314f08db))
|
||||||
|
* make refresh token duration configurable ([abd5c64](https://github.com/sasjs/server/commit/abd5c64b4a726e3f17594a98111b6aa269b71fee))
|
||||||
|
|
||||||
|
## [0.25.1](https://github.com/sasjs/server/compare/v0.25.0...v0.25.1) (2022-11-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** use mui treeView instead of custom implementation ([c51b504](https://github.com/sasjs/server/commit/c51b50428f32608bc46438e9d7964429b2d595da))
|
||||||
|
|
||||||
|
# [0.25.0](https://github.com/sasjs/server/compare/v0.24.0...v0.25.0) (2022-11-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server ([1c9d167](https://github.com/sasjs/server/commit/1c9d167f86bbbb108b96e9bc30efaf8de65d82ff))
|
||||||
|
|
||||||
|
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* cli mock testing ([6434123](https://github.com/sasjs/server/commit/643412340162e854f31fba2f162d83b7ab1751d8))
|
||||||
|
* mocking sas9 responses with JS STP ([36be3a7](https://github.com/sasjs/server/commit/36be3a7d5e7df79f9a1f3f00c3661b925f462383))
|
||||||
|
|
||||||
|
## [0.23.4](https://github.com/sasjs/server/compare/v0.23.3...v0.23.4) (2022-10-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add action to editor ref for running code ([2412622](https://github.com/sasjs/server/commit/2412622367eb46c40f388e988ae4606a7ec239b2))
|
||||||
|
|
||||||
|
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added domain for session cookies ([94072c3](https://github.com/sasjs/server/commit/94072c3d24a4d0d4c97900dc31bfbf1c9d2559b7))
|
||||||
|
|
||||||
|
## [0.23.2](https://github.com/sasjs/server/compare/v0.23.1...v0.23.2) (2022-10-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bump in correct place ([14731e8](https://github.com/sasjs/server/commit/14731e8824fa9f3d1daf89fd62f9916d5e3fcae4))
|
||||||
|
* bumping sasjs/score ([258cc35](https://github.com/sasjs/server/commit/258cc35f14cf50f2160f607000c60de27593fd79))
|
||||||
|
* reverting commit ([fda0e0b](https://github.com/sasjs/server/commit/fda0e0b57d56e3b5231e626a8d933343ac0c5cdc))
|
||||||
|
|
||||||
|
## [0.23.1](https://github.com/sasjs/server/compare/v0.23.0...v0.23.1) (2022-10-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ldap issues ([4d64420](https://github.com/sasjs/server/commit/4d64420c45424134b4d2014a2d5dd6e846ed03b3))
|
||||||
|
|
||||||
|
# [0.23.0](https://github.com/sasjs/server/compare/v0.22.1...v0.23.0) (2022-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Enable SAS_PACKAGES in SASjs Server ([424f0fc](https://github.com/sasjs/server/commit/424f0fc1faec765eb7a14619584e649454105b70))
|
||||||
|
|
||||||
|
## [0.22.1](https://github.com/sasjs/server/compare/v0.22.0...v0.22.1) (2022-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* spelling issues ([3bb0597](https://github.com/sasjs/server/commit/3bb05974d216d69368f4498eb9f309bce7d97fd8))
|
||||||
|
|
||||||
|
# [0.22.0](https://github.com/sasjs/server/compare/v0.21.7...v0.22.0) (2022-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* do not throw error on deleting group when it is created by an external auth provider ([68f0c5c](https://github.com/sasjs/server/commit/68f0c5c5884431e7e8f586dccf98132abebb193e))
|
||||||
|
* no need to restrict api endpoints when ldap auth is applied ([a142660](https://github.com/sasjs/server/commit/a14266077d3541c7a33b7635efa4208335e73519))
|
||||||
|
* remove authProvider attribute from user and group payload interface ([bbd7786](https://github.com/sasjs/server/commit/bbd7786c6ce13b374d896a45c23255b8fa3e8bd2))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* implemented LDAP authentication ([f915c51](https://github.com/sasjs/server/commit/f915c51b077a2b8c4099727355ed914ecd6364bd))
|
||||||
|
|
||||||
|
## [0.21.7](https://github.com/sasjs/server/compare/v0.21.6...v0.21.7) (2022-09-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* csrf package is changed to pillarjs-csrf ([fe3e508](https://github.com/sasjs/server/commit/fe3e5088f8dfff50042ec8e8aac9ba5ba1394deb))
|
||||||
|
|
||||||
|
## [0.21.6](https://github.com/sasjs/server/compare/v0.21.5...v0.21.6) (2022-09-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* in getTokensFromDB handle the scenario when tokens are expired ([40f95f9](https://github.com/sasjs/server/commit/40f95f9072c8685910138d88fd2410f8704fc975))
|
||||||
|
|
||||||
|
## [0.21.5](https://github.com/sasjs/server/compare/v0.21.4...v0.21.5) (2022-09-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* made files extensions case insensitive ([2496043](https://github.com/sasjs/server/commit/249604384e42be4c12c88c70a7dff90fc1917a8f))
|
||||||
|
|
||||||
|
## [0.21.4](https://github.com/sasjs/server/compare/v0.21.3...v0.21.4) (2022-09-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removing single quotes from _program value ([a0e7875](https://github.com/sasjs/server/commit/a0e7875ae61cbb6e7d3995d2e36e7300b0daec86))
|
||||||
|
|
||||||
|
## [0.21.3](https://github.com/sasjs/server/compare/v0.21.2...v0.21.3) (2022-09-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* return same tokens if not expired ([330c020](https://github.com/sasjs/server/commit/330c020933f1080261b38f07d6b627f6d7c62446))
|
||||||
|
|
||||||
|
## [0.21.2](https://github.com/sasjs/server/compare/v0.21.1...v0.21.2) (2022-09-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* default content-type for sas programs should be text/plain ([9977c9d](https://github.com/sasjs/server/commit/9977c9d161947b11d45ab2513f99a5320a3f5a06))
|
||||||
|
* **studio:** inject program path to code before sending for execution ([edc2e2a](https://github.com/sasjs/server/commit/edc2e2a302ccea4985f3d6b83ef8c23620ab82b6))
|
||||||
|
|
||||||
|
## [0.21.1](https://github.com/sasjs/server/compare/v0.21.0...v0.21.1) (2022-09-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* SASJS_WEBOUT_HEADERS path for windows ([0749d65](https://github.com/sasjs/server/commit/0749d65173e8cfe9a93464711b7be1e123c289ff))
|
||||||
|
|
||||||
|
# [0.21.0](https://github.com/sasjs/server/compare/v0.20.0...v0.21.0) (2022-09-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* sas9 mocker improved - public access denied scenario ([06d3b17](https://github.com/sasjs/server/commit/06d3b1715432ea245ee755ae1dfd0579d3eb30e9))
|
||||||
|
|
||||||
|
# [0.20.0](https://github.com/sasjs/server/compare/v0.19.0...v0.20.0) (2022-09-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add support for R stored programs ([d6651bb](https://github.com/sasjs/server/commit/d6651bbdbeee5067f53c36e69a0eefa973c523b6))
|
||||||
|
|
||||||
|
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* added mocking endpoints ([0a0ba2c](https://github.com/sasjs/server/commit/0a0ba2cca5db867de46fb2486d856a84ec68d3b4))
|
||||||
|
|
||||||
|
# [0.18.0](https://github.com/sasjs/server/compare/v0.17.5...v0.18.0) (2022-09-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add option for program launch in context menu ([ee2db27](https://github.com/sasjs/server/commit/ee2db276bb0bbd522f758e0b66f7e7b2f4afd9d5))
|
||||||
|
|
||||||
|
## [0.17.5](https://github.com/sasjs/server/compare/v0.17.4...v0.17.5) (2022-09-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* SASINITIALFOLDER split over 2 params, closes [#271](https://github.com/sasjs/server/issues/271) ([393b5ea](https://github.com/sasjs/server/commit/393b5eaf990049c39eecf2b9e8dd21a001b6e298))
|
||||||
|
|
||||||
|
## [0.17.4](https://github.com/sasjs/server/compare/v0.17.3...v0.17.4) (2022-09-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* invalid JS logic ([9f06080](https://github.com/sasjs/server/commit/9f06080348aed076f8188a26fb4890d38a5a3510))
|
||||||
|
|
||||||
|
## [0.17.3](https://github.com/sasjs/server/compare/v0.17.2...v0.17.3) (2022-09-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* making SASINITIALFOLDER option windows only. Closes [#267](https://github.com/sasjs/server/issues/267) ([e63271a](https://github.com/sasjs/server/commit/e63271a67a0deb3059a5f2bec1854efee5a6e5a5))
|
||||||
|
|
||||||
|
## [0.17.2](https://github.com/sasjs/server/compare/v0.17.1...v0.17.2) (2022-08-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* addition of SASINITIALFOLDER startup option. Closes [#260](https://github.com/sasjs/server/issues/260) ([a5ee2f2](https://github.com/sasjs/server/commit/a5ee2f292384f90e9d95d003d652311c0d91a7a7))
|
||||||
|
|
||||||
|
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* typo mistake ([ee17d37](https://github.com/sasjs/server/commit/ee17d37aa188b0ca43cea0e89d6cd1a566b765cb))
|
||||||
|
|
||||||
|
# [0.17.0](https://github.com/sasjs/server/compare/v0.16.1...v0.17.0) (2022-08-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow underscores in file name ([bce83cb](https://github.com/sasjs/server/commit/bce83cb6fbc98f8198564c9399821f5829acc767))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add the functionality of saving file by ctrl + s in editor ([3a3c90d](https://github.com/sasjs/server/commit/3a3c90d9e690ac5267bf1acc834b5b5c5b4dadb6))
|
||||||
|
|
||||||
|
## [0.16.1](https://github.com/sasjs/server/compare/v0.16.0...v0.16.1) (2022-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update response of /SASjsApi/stp/execute and /SASjsApi/code/execute ([98ea2ac](https://github.com/sasjs/server/commit/98ea2ac9b98631605e39e5900e533727ea0e3d85))
|
||||||
|
|
||||||
|
# [0.16.0](https://github.com/sasjs/server/compare/v0.15.3...v0.16.0) (2022-08-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add a new variable _SASJS_WEBOUT_HEADERS to code.js and code.py ([882bedd](https://github.com/sasjs/server/commit/882bedd5d5da22de6ed45c03d0a261aadfb3a33c))
|
||||||
|
* update content for code.sas file ([02e88ae](https://github.com/sasjs/server/commit/02e88ae7280d020a753bc2c095a931c79ac392d1))
|
||||||
|
* update default content type for python and js runtimes ([8780b80](https://github.com/sasjs/server/commit/8780b800a34aa618631821e5d97e26e8b0f15806))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* implement the logic for running python stored programs ([b06993a](https://github.com/sasjs/server/commit/b06993ab9ea24b28d9e553763187387685aaa666))
|
||||||
|
|
||||||
|
## [0.15.3](https://github.com/sasjs/server/compare/v0.15.2...v0.15.3) (2022-08-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding proc printto in precode to enable print output in log. Closes [#253](https://github.com/sasjs/server/issues/253) ([f8bb732](https://github.com/sasjs/server/commit/f8bb7327a8a4649ac77bb6237e31cea075d46bb9))
|
||||||
|
|
||||||
|
## [0.15.2](https://github.com/sasjs/server/compare/v0.15.1...v0.15.2) (2022-08-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* remove vulnerabitities ([f27ac51](https://github.com/sasjs/server/commit/f27ac51fc4beb21070d0ab551cfdaec1f6ba39e0))
|
||||||
|
|
||||||
|
## [0.15.1](https://github.com/sasjs/server/compare/v0.15.0...v0.15.1) (2022-08-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** fix UI responsiveness ([d99fdd1](https://github.com/sasjs/server/commit/d99fdd1ec7991b94a0d98338d7a7a6216f46ce45))
|
||||||
|
|
||||||
|
# [0.15.0](https://github.com/sasjs/server/compare/v0.14.1...v0.15.0) (2022-08-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* after selecting file in sidebar collapse sidebar in mobile view ([e215958](https://github.com/sasjs/server/commit/e215958b8b05d7a8ce9d82395e0640b5b37fb40d))
|
||||||
|
* improve mobile view for studio page ([c67d3ee](https://github.com/sasjs/server/commit/c67d3ee2f102155e2e9781e13d5d33c1ab227cb4))
|
||||||
|
* improve responsiveness for mobile view ([6ef40b9](https://github.com/sasjs/server/commit/6ef40b954a87ebb0a2621119064f38d58ea85148))
|
||||||
|
* improve user experience for adding permissions ([7a162ed](https://github.com/sasjs/server/commit/7a162eda8fc60383ff647d93e6611799e2e6af7a))
|
||||||
|
* show logout button only when user is logged in ([9227cd4](https://github.com/sasjs/server/commit/9227cd449dc46fd960a488eb281804a9b9ffc284))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add multiple permission for same combination of type and principal at once ([754704b](https://github.com/sasjs/server/commit/754704bca89ecbdbcc3bd4ef04b94124c4f24167))
|
||||||
|
|
||||||
|
## [0.14.1](https://github.com/sasjs/server/compare/v0.14.0...v0.14.1) (2022-08-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **apps:** App Stream logo fix ([87c03c5](https://github.com/sasjs/server/commit/87c03c5f8dbdfc151d4ff3722ecbcd3f7e409aea))
|
||||||
|
* **cookie:** XSRF cookie is removed and passed token in head section ([77f8d30](https://github.com/sasjs/server/commit/77f8d30baf9b1077279c29f1c3e5ca02a5436bc0))
|
||||||
|
* **env:** check added for not providing WHITELIST ([5966016](https://github.com/sasjs/server/commit/5966016853369146b27ac5781808cb51d65c887f))
|
||||||
|
* **web:** show login on logged-out state ([f7fcc77](https://github.com/sasjs/server/commit/f7fcc7741aa2af93a4a2b1e651003704c9bbff0c))
|
||||||
|
|
||||||
|
# [0.14.0](https://github.com/sasjs/server/compare/v0.13.3...v0.14.0) (2022-08-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add restriction on add/remove user to public group ([d3a516c](https://github.com/sasjs/server/commit/d3a516c36e45aa1cc76c30c744e6a0e5bd553165))
|
||||||
|
* call jwt.verify in synchronous way ([254bc07](https://github.com/sasjs/server/commit/254bc07da744a9708109bfb792be70aa3f6284f4))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add public group to DB on seed ([c3e3bef](https://github.com/sasjs/server/commit/c3e3befc17102ee1754e1403193040b4f79fb2a7))
|
||||||
|
* bypass authentication when route is enabled for public group ([68515f9](https://github.com/sasjs/server/commit/68515f95a65d422e29c0ed6028f3ea0ae8d9b1bf))
|
||||||
|
|
||||||
|
## [0.13.3](https://github.com/sasjs/server/compare/v0.13.2...v0.13.3) (2022-08-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* show non-admin user his own permissions only ([8a3054e](https://github.com/sasjs/server/commit/8a3054e19ade82e2792cfb0f2a8af9e502c5eb52))
|
||||||
|
* update schema of Permission ([5d5a9d3](https://github.com/sasjs/server/commit/5d5a9d3788281d75c56f68f0dff231abc9c9c275))
|
||||||
|
|
||||||
## [0.13.2](https://github.com/sasjs/server/compare/v0.13.1...v0.13.2) (2022-08-01)
|
## [0.13.2](https://github.com/sasjs/server/compare/v0.13.1...v0.13.2) (2022-08-01)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
78
README.md
78
README.md
@@ -64,23 +64,53 @@ Example contents of a `.env` file:
|
|||||||
# Server mode is multi-user and suitable for intranet / internet use
|
# Server mode is multi-user and suitable for intranet / internet use
|
||||||
MODE=
|
MODE=
|
||||||
|
|
||||||
|
# A comma separated string that defines the available runTimes.
|
||||||
|
# Priority is given to the runtime that comes first in the string.
|
||||||
|
# Possible options at the moment are sas, js, py and r
|
||||||
|
|
||||||
|
# This string sets the priority of the available analytic runtimes
|
||||||
|
# Valid runtimes are SAS (sas), JavaScript (js), Python (py) and R (r)
|
||||||
|
# For each option provided, there should be a corresponding path,
|
||||||
|
# eg SAS_PATH, NODE_PATH, PYTHON_PATH or RSCRIPT_PATH
|
||||||
|
# Priority is given to runtimes earlier in the string
|
||||||
|
# Example options: [sas,js,py | js,py | sas | sas,js | r | sas,r]
|
||||||
|
RUN_TIMES=
|
||||||
|
|
||||||
# Path to SAS executable (sas.exe / sas.sh)
|
# Path to SAS executable (sas.exe / sas.sh)
|
||||||
SAS_PATH=/path/to/sas/executable.exe
|
SAS_PATH=/path/to/sas/executable.exe
|
||||||
|
|
||||||
# Path to Node.js executable
|
# Path to Node.js executable
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||||
|
|
||||||
|
# Path to Python executable
|
||||||
|
PYTHON_PATH=/usr/bin/python
|
||||||
|
|
||||||
|
# Path to R executable
|
||||||
|
R_PATH=/usr/bin/Rscript
|
||||||
|
|
||||||
# Path to working directory
|
# Path to working directory
|
||||||
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
|
|
||||||
|
# This location is for files, sasjs packages and appStreamConfig.json
|
||||||
|
DRIVE_LOCATION=./sasjs_root/drive
|
||||||
|
|
||||||
|
|
||||||
# options: [http|https] default: http
|
# options: [http|https] default: http
|
||||||
PROTOCOL=
|
PROTOCOL=
|
||||||
|
|
||||||
# default: 5000
|
# default: 5000
|
||||||
PORT=
|
PORT=
|
||||||
|
|
||||||
|
# options: [sas9|sasviya]
|
||||||
|
# If not present, mocking function is disabled
|
||||||
|
MOCK_SERVERTYPE=
|
||||||
|
|
||||||
|
# default: /api/mocks
|
||||||
|
# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs
|
||||||
|
# Server will automatically use subdirectory accordingly
|
||||||
|
STATIC_MOCK_LOCATION=
|
||||||
|
|
||||||
#
|
#
|
||||||
## Additional SAS Options
|
## Additional SAS Options
|
||||||
@@ -104,9 +134,22 @@ PRIVATE_KEY=privkey.pem (required)
|
|||||||
CERT_CHAIN=certificate.pem (required)
|
CERT_CHAIN=certificate.pem (required)
|
||||||
CA_ROOT=fullchain.pem (optional)
|
CA_ROOT=fullchain.pem (optional)
|
||||||
|
|
||||||
# ENV variables required for MODE: `server`
|
## ENV variables required for MODE: `server`
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
|
# options: [mongodb|cosmos_mongodb] default: mongodb
|
||||||
|
DB_TYPE=
|
||||||
|
|
||||||
|
# AUTH_PROVIDERS options: [ldap] default: ``
|
||||||
|
AUTH_PROVIDERS=
|
||||||
|
|
||||||
|
## ENV variables required for AUTH_MECHANISM: `ldap`
|
||||||
|
LDAP_URL= <LDAP_SERVER_URL>
|
||||||
|
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
|
||||||
|
LDAP_BIND_PASSWORD = <password>
|
||||||
|
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
||||||
|
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
||||||
|
|
||||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||||
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
||||||
CORS=
|
CORS=
|
||||||
@@ -132,6 +175,32 @@ HELMET_COEP=
|
|||||||
# }
|
# }
|
||||||
HELMET_CSP_CONFIG_PATH=./csp.config.json
|
HELMET_CSP_CONFIG_PATH=./csp.config.json
|
||||||
|
|
||||||
|
# To prevent brute force attack on login route we have implemented rate limiter
|
||||||
|
# Only valid for MODE: server
|
||||||
|
# Following are configurable env variable rate limiter
|
||||||
|
|
||||||
|
# After this, access is blocked for 1 day
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
|
||||||
|
|
||||||
|
|
||||||
|
# After this, access is blocked for an hour
|
||||||
|
# Store number for 24 days since first fail
|
||||||
|
# Once a successful login is attempted, it resets
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
|
||||||
|
|
||||||
|
# Name of the admin user that will be created on startup if not exists already
|
||||||
|
# Default is `secretuser`
|
||||||
|
ADMIN_USERNAME=secretuser
|
||||||
|
|
||||||
|
# Temporary password for the ADMIN_USERNAME, which is in place until the first login
|
||||||
|
# Default is `secretpassword`
|
||||||
|
ADMIN_PASSWORD_INITIAL=secretpassword
|
||||||
|
|
||||||
|
# Specify whether app has to reset the ADMIN_USERNAME's password or not
|
||||||
|
# Default is NO. Possible options are YES and NO
|
||||||
|
# If ADMIN_PASSWORD_RESET is YES then the ADMIN_USERNAME will be prompted to change the password from ADMIN_PASSWORD_INITIAL on their next login. This will repeat on every server restart, unless the option is removed / set to NO.
|
||||||
|
ADMIN_PASSWORD_RESET=NO
|
||||||
|
|
||||||
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
|
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
|
||||||
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
||||||
LOG_FORMAT_MORGAN=
|
LOG_FORMAT_MORGAN=
|
||||||
@@ -139,13 +208,6 @@ LOG_FORMAT_MORGAN=
|
|||||||
# This location is for server logs with classical UNIX logrotate behavior
|
# This location is for server logs with classical UNIX logrotate behavior
|
||||||
LOG_LOCATION=./sasjs_root/logs
|
LOG_LOCATION=./sasjs_root/logs
|
||||||
|
|
||||||
# A comma separated string that defines the available runTimes.
|
|
||||||
# Priority is given to the runtime that comes first in the string.
|
|
||||||
# Possible options at the moment are sas and js
|
|
||||||
|
|
||||||
# options: [sas,js|js,sas|sas|js] default:sas
|
|
||||||
RUN_TIMES=
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Persisting the Session
|
## Persisting the Session
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
MODE=[desktop|server] default considered as desktop
|
MODE=[desktop|server] default considered as desktop
|
||||||
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
|
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
|
||||||
|
ALLOWED_DOMAIN=<just domain e.g. example.com >
|
||||||
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
||||||
|
|
||||||
PROTOCOL=[http|https] default considered as http
|
PROTOCOL=[http|https] default considered as http
|
||||||
@@ -13,12 +14,34 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
|||||||
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
||||||
|
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
DB_TYPE=[mongodb|cosmos_mongodb] default considered as mongodb
|
||||||
|
|
||||||
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
|
AUTH_PROVIDERS=[ldap]
|
||||||
|
|
||||||
|
LDAP_URL= <LDAP_SERVER_URL>
|
||||||
|
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
|
||||||
|
LDAP_BIND_PASSWORD = <password>
|
||||||
|
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
||||||
|
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
||||||
|
|
||||||
|
#default value is 100
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
|
||||||
|
|
||||||
|
#default value is 10
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10
|
||||||
|
|
||||||
|
ADMIN_USERNAME=secretuser
|
||||||
|
ADMIN_PASSWORD_INITIAL=secretpassword
|
||||||
|
ADMIN_PASSWORD_RESET=NO
|
||||||
|
|
||||||
|
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||||
|
PYTHON_PATH=/usr/bin/python
|
||||||
|
R_PATH=/usr/bin/Rscript
|
||||||
|
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
|
DRIVE_LOCATION=./sasjs_root/drive
|
||||||
|
|
||||||
LOG_FORMAT_MORGAN=common
|
LOG_FORMAT_MORGAN=common
|
||||||
LOG_LOCATION=./sasjs_root/logs
|
LOG_LOCATION=./sasjs_root/logs
|
||||||
1
api/mocks/sas9/generic/logged-in
Normal file
1
api/mocks/sas9/generic/logged-in
Normal file
@@ -0,0 +1 @@
|
|||||||
|
You have signed in.
|
||||||
1
api/mocks/sas9/generic/logged-out
Normal file
1
api/mocks/sas9/generic/logged-out
Normal file
@@ -0,0 +1 @@
|
|||||||
|
You have signed out.
|
||||||
30
api/mocks/sas9/generic/login
Normal file
30
api/mocks/sas9/generic/login
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" dir="ltr" class="bg">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="initial-scale=1" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
|
||||||
|
<!--form container-->
|
||||||
|
<input type="hidden" name="lt" value="validtoken" aria-hidden="true" />
|
||||||
|
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
|
||||||
|
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
|
||||||
|
|
||||||
|
<span class="userid">
|
||||||
|
|
||||||
|
<input id="username" name="username" tabindex="3" aria-labelledby="username1 message1 message2 message3" name="username" placeholder="User ID" type="text" autofocus="true" value="" maxlength="500" autocomplete="off" />
|
||||||
|
</span>
|
||||||
|
<span class="password">
|
||||||
|
|
||||||
|
<input id="password" name="password" tabindex="4" name="password" placeholder="Password" type="password" value="" maxlength="500" autocomplete="off" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-submit" title="Sign In" tabindex="5" onClick="this.disabled=true;setSubmitUrl(this.form);this.form.submit();return false;">Sign In</button>
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</html>
|
||||||
1
api/mocks/sas9/generic/public-access-denied
Normal file
1
api/mocks/sas9/generic/public-access-denied
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Public access has been denied.
|
||||||
1
api/mocks/sas9/generic/sas-stored-process
Normal file
1
api/mocks/sas9/generic/sas-stored-process
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"title": "Log Off SAS Demo User"
|
||||||
4282
api/package-lock.json
generated
4282
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
"description": "Api of SASjs server",
|
"description": "Api of SASjs server",
|
||||||
"main": "./src/server.ts",
|
"main": "./src/server.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
|
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore && npm run downloadMacros",
|
||||||
"prestart": "npm run initial",
|
"prestart": "npm run initial",
|
||||||
"prebuild": "npm run initial",
|
"prebuild": "npm run initial",
|
||||||
"start": "NODE_ENV=development nodemon ./src/server.ts",
|
"start": "NODE_ENV=development nodemon ./src/server.ts",
|
||||||
@@ -17,20 +17,21 @@
|
|||||||
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"exe": "npm run build && pkg .",
|
"exe": "npm run build && pkg .",
|
||||||
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
|
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sas:copy && npm run web:copy",
|
||||||
"public:copy": "cp -r ./public/ ./build/public/",
|
"public:copy": "cp -r ./public/ ./build/public/",
|
||||||
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
||||||
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
|
"sas:copy": "cp -r ./sas/ ./build/sas/",
|
||||||
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
|
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
|
||||||
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
|
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
|
||||||
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
|
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts",
|
||||||
|
"downloadMacros": "ts-node ./scripts/downloadMacros.ts"
|
||||||
},
|
},
|
||||||
"bin": "./build/src/server.js",
|
"bin": "./build/src/server.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"assets": [
|
"assets": [
|
||||||
"./build/public/**/*",
|
"./build/public/**/*",
|
||||||
"./build/sasjsbuild/**/*",
|
"./build/sasjsbuild/**/*",
|
||||||
"./build/sasjscore/**/*",
|
"./build/sas/**/*",
|
||||||
"./web/build/**/*"
|
"./web/build/**/*"
|
||||||
],
|
],
|
||||||
"targets": [
|
"targets": [
|
||||||
@@ -47,22 +48,23 @@
|
|||||||
},
|
},
|
||||||
"author": "4GL Ltd",
|
"author": "4GL Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^4.31.3",
|
"@sasjs/core": "^4.40.1",
|
||||||
"@sasjs/utils": "2.42.1",
|
"@sasjs/utils": "3.2.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"connect-mongo": "^4.6.0",
|
"connect-mongo": "^4.6.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"csurf": "^1.11.0",
|
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-session": "^1.17.2",
|
"express-session": "^1.17.2",
|
||||||
"helmet": "^5.0.2",
|
"helmet": "^5.0.2",
|
||||||
"joi": "^17.4.2",
|
"joi": "^17.4.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"ldapjs": "2.3.3",
|
||||||
"mongoose": "^6.0.12",
|
"mongoose": "^6.0.12",
|
||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"rate-limiter-flexible": "2.4.1",
|
||||||
"rotating-file-stream": "^3.0.4",
|
"rotating-file-stream": "^3.0.4",
|
||||||
"swagger-ui-express": "4.3.0",
|
"swagger-ui-express": "4.3.0",
|
||||||
"unzipper": "^0.10.11",
|
"unzipper": "^0.10.11",
|
||||||
@@ -73,11 +75,11 @@
|
|||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/csurf": "^1.11.2",
|
|
||||||
"@types/express": "^4.17.12",
|
"@types/express": "^4.17.12",
|
||||||
"@types/express-session": "^1.17.4",
|
"@types/express-session": "^1.17.4",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/jsonwebtoken": "^8.5.5",
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
|
"@types/ldapjs": "^2.2.4",
|
||||||
"@types/mongoose-sequence": "^3.0.6",
|
"@types/mongoose-sequence": "^3.0.6",
|
||||||
"@types/morgan": "^1.9.3",
|
"@types/morgan": "^1.9.3",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
@@ -86,10 +88,13 @@
|
|||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
"@types/unzipper": "^0.10.5",
|
"@types/unzipper": "^0.10.5",
|
||||||
"adm-zip": "^0.5.9",
|
"adm-zip": "^0.5.9",
|
||||||
|
"axios": "0.27.2",
|
||||||
|
"csrf": "^3.1.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"http-headers-validation": "^0.0.1",
|
"http-headers-validation": "^0.0.1",
|
||||||
"jest": "^27.0.6",
|
"jest": "^27.0.6",
|
||||||
"mongodb-memory-server": "^8.0.0",
|
"mongodb-memory-server": "8.11.4",
|
||||||
|
"nodejs-file-downloader": "4.10.2",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"pkg": "5.6.0",
|
"pkg": "5.6.0",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
39
api/scripts/downloadMacros.ts
Normal file
39
api/scripts/downloadMacros.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import Downloader from 'nodejs-file-downloader'
|
||||||
|
import { createFile, listFilesInFolder } from '@sasjs/utils'
|
||||||
|
|
||||||
|
import { sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils/file'
|
||||||
|
|
||||||
|
export const downloadMacros = async () => {
|
||||||
|
const url =
|
||||||
|
'https://api.github.com/repos/yabwon/SAS_PACKAGES/contents/SPF/Macros'
|
||||||
|
|
||||||
|
console.info(`Downloading macros from ${url}`)
|
||||||
|
|
||||||
|
await axios
|
||||||
|
.get(url)
|
||||||
|
.then(async (res) => {
|
||||||
|
await downloadFiles(res.data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new Error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadFiles = async function (fileList: any) {
|
||||||
|
for (const file of fileList) {
|
||||||
|
const downloader = new Downloader({
|
||||||
|
url: file.download_url,
|
||||||
|
directory: sasJSCoreMacros,
|
||||||
|
fileName: file.path.replace(/^SPF\/Macros/, ''),
|
||||||
|
cloneFiles: false
|
||||||
|
})
|
||||||
|
await downloader.download()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileNames = await listFilesInFolder(sasJSCoreMacros)
|
||||||
|
|
||||||
|
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadMacros()
|
||||||
@@ -15,7 +15,7 @@ export const configureCors = (app: Express) => {
|
|||||||
whiteList.push(url.replace(/\/$/, ''))
|
whiteList.push(url.replace(/\/$/, ''))
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('All CORS Requests are enabled for:', whiteList)
|
process.logger.info('All CORS Requests are enabled for:', whiteList)
|
||||||
app.use(cors({ credentials: true, origin: whiteList }))
|
app.use(cors({ credentials: true, origin: whiteList }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,38 @@
|
|||||||
import { Express } from 'express'
|
import { Express, CookieOptions } from 'express'
|
||||||
import mongoose from 'mongoose'
|
import mongoose from 'mongoose'
|
||||||
import session from 'express-session'
|
import session from 'express-session'
|
||||||
import MongoStore from 'connect-mongo'
|
import MongoStore from 'connect-mongo'
|
||||||
|
|
||||||
import { ModeType } from '../utils'
|
import { DatabaseType, ModeType, ProtocolType } from '../utils'
|
||||||
import { cookieOptions } from '../app'
|
|
||||||
|
|
||||||
export const configureExpressSession = (app: Express) => {
|
export const configureExpressSession = (app: Express) => {
|
||||||
const { MODE } = process.env
|
const { MODE, DB_TYPE } = process.env
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
let store: MongoStore | undefined
|
let store: MongoStore | undefined
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
store = MongoStore.create({
|
if (DB_TYPE === DatabaseType.COSMOS_MONGODB) {
|
||||||
client: mongoose.connection!.getClient() as any,
|
// COSMOS DB requires specific connection options (compatibility mode)
|
||||||
collectionName: 'sessions'
|
// See: https://www.npmjs.com/package/connect-mongo#set-the-compatibility-mode
|
||||||
})
|
store = MongoStore.create({
|
||||||
|
client: mongoose.connection!.getClient() as any,
|
||||||
|
autoRemove: 'interval'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
store = MongoStore.create({
|
||||||
|
client: mongoose.connection!.getClient() as any
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { PROTOCOL, ALLOWED_DOMAIN } = process.env
|
||||||
|
const cookieOptions: CookieOptions = {
|
||||||
|
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
|
||||||
|
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||||
|
domain: ALLOWED_DOMAIN?.trim() || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const configureLogger = (app: Express) => {
|
|||||||
path: logsFolder
|
path: logsFolder
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Writing Logs to :', path.join(logsFolder, filename))
|
process.logger.info('Writing Logs to :', path.join(logsFolder, filename))
|
||||||
|
|
||||||
options = { stream: accessLogStream }
|
options = { stream: accessLogStream }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { ErrorRequestHandler } from 'express'
|
import express, { ErrorRequestHandler } from 'express'
|
||||||
import csrf from 'csurf'
|
|
||||||
import cookieParser from 'cookie-parser'
|
import cookieParser from 'cookie-parser'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
copySASjsCore,
|
copySASjsCore,
|
||||||
|
createWeboutSasFile,
|
||||||
|
getFilesFolder,
|
||||||
|
getPackagesFolder,
|
||||||
getWebBuildFolder,
|
getWebBuildFolder,
|
||||||
instantiateLogger,
|
instantiateLogger,
|
||||||
loadAppStreamConfig,
|
loadAppStreamConfig,
|
||||||
ProtocolType,
|
|
||||||
ReturnCode,
|
ReturnCode,
|
||||||
setProcessVariables,
|
setProcessVariables,
|
||||||
setupFolders,
|
setupFilesFolder,
|
||||||
|
setupPackagesFolder,
|
||||||
|
setupUserAutoExec,
|
||||||
verifyEnvVariables
|
verifyEnvVariables
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +24,7 @@ import {
|
|||||||
configureLogger,
|
configureLogger,
|
||||||
configureSecurity
|
configureSecurity
|
||||||
} from './app-modules'
|
} from './app-modules'
|
||||||
|
import { folderExists } from '@sasjs/utils'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
@@ -30,24 +34,8 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
|||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { PROTOCOL } = process.env
|
|
||||||
|
|
||||||
export const cookieOptions = {
|
|
||||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
|
||||||
httpOnly: true,
|
|
||||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* CSRF Protection *
|
|
||||||
***********************************/
|
|
||||||
export const csrfProtection = csrf({ cookie: cookieOptions })
|
|
||||||
|
|
||||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
if (err.code === 'EBADCSRFTOKEN')
|
process.logger.error(err.stack)
|
||||||
return res.status(400).send('Invalid CSRF token!')
|
|
||||||
|
|
||||||
console.error(err.stack)
|
|
||||||
res.status(500).send('Something broke!')
|
res.status(500).send('Something broke!')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +64,25 @@ export default setProcessVariables().then(async () => {
|
|||||||
app.use(express.json({ limit: '100mb' }))
|
app.use(express.json({ limit: '100mb' }))
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
await setupFolders()
|
// Body parser is used for decoding the formdata on POST request.
|
||||||
await copySASjsCore()
|
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
||||||
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
|
await setupUserAutoExec()
|
||||||
|
|
||||||
|
if (!(await folderExists(getFilesFolder()))) await setupFilesFolder()
|
||||||
|
|
||||||
|
if (!(await folderExists(getPackagesFolder()))) await setupPackagesFolder()
|
||||||
|
|
||||||
|
const sasautosPath = path.join(process.driveLoc, 'sas', 'sasautos')
|
||||||
|
if (await folderExists(sasautosPath)) {
|
||||||
|
process.logger.warn(
|
||||||
|
`SASAUTOS was not refreshed. To force a refresh, delete the ${sasautosPath} folder`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await copySASjsCore()
|
||||||
|
await createWeboutSasFile()
|
||||||
|
}
|
||||||
|
|
||||||
// loading these modules after setting up variables due to
|
// loading these modules after setting up variables due to
|
||||||
// multer's usage of process var process.driveLoc
|
// multer's usage of process var process.driveLoc
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
import express from 'express'
|
||||||
|
import {
|
||||||
|
Security,
|
||||||
|
Route,
|
||||||
|
Tags,
|
||||||
|
Example,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Request,
|
||||||
|
Body,
|
||||||
|
Query,
|
||||||
|
Hidden
|
||||||
|
} from 'tsoa'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
import {
|
import {
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
generateRefreshToken,
|
generateRefreshToken,
|
||||||
|
getTokensFromDB,
|
||||||
removeTokensInDB,
|
removeTokensInDB,
|
||||||
saveTokensInDB
|
saveTokensInDB
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
import Client from '../model/Client'
|
||||||
|
import User from '../model/User'
|
||||||
|
|
||||||
@Route('SASjsApi/auth')
|
@Route('SASjsApi/auth')
|
||||||
@Tags('Auth')
|
@Tags('Auth')
|
||||||
@@ -60,6 +75,18 @@ export class AuthController {
|
|||||||
public async logout(@Query() @Hidden() data?: InfoJWT) {
|
public async logout(@Query() @Hidden() data?: InfoJWT) {
|
||||||
return logout(data!)
|
return logout(data!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Update user's password.
|
||||||
|
*/
|
||||||
|
@Security('bearerAuth')
|
||||||
|
@Patch('updatePassword')
|
||||||
|
public async updatePassword(
|
||||||
|
@Request() req: express.Request,
|
||||||
|
@Body() body: UpdatePasswordPayload
|
||||||
|
) {
|
||||||
|
return updatePassword(req, body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = async (data: any): Promise<TokenResponse> => {
|
const token = async (data: any): Promise<TokenResponse> => {
|
||||||
@@ -73,8 +100,26 @@ const token = async (data: any): Promise<TokenResponse> => {
|
|||||||
|
|
||||||
AuthController.deleteCode(userInfo.userId, clientId)
|
AuthController.deleteCode(userInfo.userId, clientId)
|
||||||
|
|
||||||
const accessToken = generateAccessToken(userInfo)
|
// get tokens from DB
|
||||||
const refreshToken = generateRefreshToken(userInfo)
|
const existingTokens = await getTokensFromDB(userInfo.userId, clientId)
|
||||||
|
if (existingTokens) {
|
||||||
|
return {
|
||||||
|
accessToken: existingTokens.accessToken,
|
||||||
|
refreshToken: existingTokens.refreshToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await Client.findOne({ clientId })
|
||||||
|
if (!client) throw new Error('Invalid clientId.')
|
||||||
|
|
||||||
|
const accessToken = generateAccessToken(
|
||||||
|
userInfo,
|
||||||
|
client.accessTokenExpiration
|
||||||
|
)
|
||||||
|
const refreshToken = generateRefreshToken(
|
||||||
|
userInfo,
|
||||||
|
client.refreshTokenExpiration
|
||||||
|
)
|
||||||
|
|
||||||
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
|
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
|
||||||
|
|
||||||
@@ -82,8 +127,17 @@ const token = async (data: any): Promise<TokenResponse> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
|
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
|
||||||
const accessToken = generateAccessToken(userInfo)
|
const client = await Client.findOne({ clientId: userInfo.clientId })
|
||||||
const refreshToken = generateRefreshToken(userInfo)
|
if (!client) throw new Error('Invalid clientId.')
|
||||||
|
|
||||||
|
const accessToken = generateAccessToken(
|
||||||
|
userInfo,
|
||||||
|
client.accessTokenExpiration
|
||||||
|
)
|
||||||
|
const refreshToken = generateRefreshToken(
|
||||||
|
userInfo,
|
||||||
|
client.refreshTokenExpiration
|
||||||
|
)
|
||||||
|
|
||||||
await saveTokensInDB(
|
await saveTokensInDB(
|
||||||
userInfo.userId,
|
userInfo.userId,
|
||||||
@@ -99,6 +153,40 @@ const logout = async (userInfo: InfoJWT) => {
|
|||||||
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatePassword = async (
|
||||||
|
req: express.Request,
|
||||||
|
data: UpdatePasswordPayload
|
||||||
|
) => {
|
||||||
|
const { currentPassword, newPassword } = data
|
||||||
|
const userId = req.user?.userId
|
||||||
|
const dbUser = await User.findOne({ id: userId })
|
||||||
|
|
||||||
|
if (!dbUser)
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
message: `User not found!`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbUser?.authProvider) {
|
||||||
|
throw {
|
||||||
|
code: 405,
|
||||||
|
message:
|
||||||
|
'Can not update password of user that is created by an external auth provider.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validPass = dbUser.comparePassword(currentPassword)
|
||||||
|
if (!validPass)
|
||||||
|
throw {
|
||||||
|
code: 403,
|
||||||
|
message: `Invalid current password!`
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUser.password = User.hashPassword(newPassword)
|
||||||
|
dbUser.needsToUpdatePassword = false
|
||||||
|
await dbUser.save()
|
||||||
|
}
|
||||||
|
|
||||||
interface TokenPayload {
|
interface TokenPayload {
|
||||||
/**
|
/**
|
||||||
* Client ID
|
* Client ID
|
||||||
@@ -125,6 +213,19 @@ interface TokenResponse {
|
|||||||
refreshToken: string
|
refreshToken: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UpdatePasswordPayload {
|
||||||
|
/**
|
||||||
|
* Current Password
|
||||||
|
* @example "currentPasswordString"
|
||||||
|
*/
|
||||||
|
currentPassword: string
|
||||||
|
/**
|
||||||
|
* New Password
|
||||||
|
* @example "newPassword"
|
||||||
|
*/
|
||||||
|
newPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
const verifyAuthCode = async (
|
const verifyAuthCode = async (
|
||||||
clientId: string,
|
clientId: string,
|
||||||
code: string
|
code: string
|
||||||
|
|||||||
186
api/src/controllers/authConfig.ts
Normal file
186
api/src/controllers/authConfig.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { Security, Route, Tags, Get, Post, Example } from 'tsoa'
|
||||||
|
|
||||||
|
import { LDAPClient, LDAPUser, LDAPGroup, AuthProviderType } from '../utils'
|
||||||
|
import { randomBytes } from 'crypto'
|
||||||
|
import User from '../model/User'
|
||||||
|
import Group from '../model/Group'
|
||||||
|
import Permission from '../model/Permission'
|
||||||
|
|
||||||
|
@Security('bearerAuth')
|
||||||
|
@Route('SASjsApi/authConfig')
|
||||||
|
@Tags('Auth_Config')
|
||||||
|
export class AuthConfigController {
|
||||||
|
/**
|
||||||
|
* @summary Gives the detail of Auth Mechanism.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example({
|
||||||
|
ldap: {
|
||||||
|
LDAP_URL: 'ldaps://my.ldap.server:636',
|
||||||
|
LDAP_BIND_DN: 'cn=admin,ou=system,dc=cloudron',
|
||||||
|
LDAP_BIND_PASSWORD: 'secret',
|
||||||
|
LDAP_USERS_BASE_DN: 'ou=users,dc=cloudron',
|
||||||
|
LDAP_GROUPS_BASE_DN: 'ou=groups,dc=cloudron'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@Get('/')
|
||||||
|
public getDetail() {
|
||||||
|
return getAuthConfigDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Synchronises LDAP users and groups with internal DB and returns the count of imported users and groups.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example({
|
||||||
|
users: 5,
|
||||||
|
groups: 3
|
||||||
|
})
|
||||||
|
@Post('/synchroniseWithLDAP')
|
||||||
|
public async synchroniseWithLDAP() {
|
||||||
|
return synchroniseWithLDAP()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const synchroniseWithLDAP = async () => {
|
||||||
|
process.logger.info('Syncing LDAP with internal DB')
|
||||||
|
|
||||||
|
const permissions = await Permission.get({})
|
||||||
|
await Permission.deleteMany()
|
||||||
|
await User.deleteMany({ authProvider: AuthProviderType.LDAP })
|
||||||
|
await Group.deleteMany({ authProvider: AuthProviderType.LDAP })
|
||||||
|
|
||||||
|
const ldapClient = await LDAPClient.init()
|
||||||
|
|
||||||
|
process.logger.info('fetching LDAP users')
|
||||||
|
const users = await ldapClient.getAllLDAPUsers()
|
||||||
|
|
||||||
|
process.logger.info('inserting LDAP users to DB')
|
||||||
|
|
||||||
|
const existingUsers: string[] = []
|
||||||
|
const importedUsers: LDAPUser[] = []
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
const usernameExists = await User.findOne({ username: user.username })
|
||||||
|
if (usernameExists) {
|
||||||
|
existingUsers.push(user.username)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashPassword = User.hashPassword(randomBytes(64).toString('hex'))
|
||||||
|
|
||||||
|
await User.create({
|
||||||
|
displayName: user.displayName,
|
||||||
|
username: user.username,
|
||||||
|
password: hashPassword,
|
||||||
|
authProvider: AuthProviderType.LDAP,
|
||||||
|
needsToUpdatePassword: false
|
||||||
|
})
|
||||||
|
|
||||||
|
importedUsers.push(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingUsers.length > 0) {
|
||||||
|
process.logger.info(
|
||||||
|
'Failed to insert following users as they already exist in DB:'
|
||||||
|
)
|
||||||
|
existingUsers.forEach((user) => process.logger.log(`* ${user}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
process.logger.info('fetching LDAP groups')
|
||||||
|
const groups = await ldapClient.getAllLDAPGroups()
|
||||||
|
|
||||||
|
process.logger.info('inserting LDAP groups to DB')
|
||||||
|
|
||||||
|
const existingGroups: string[] = []
|
||||||
|
const importedGroups: LDAPGroup[] = []
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
const groupExists = await Group.findOne({ name: group.name })
|
||||||
|
if (groupExists) {
|
||||||
|
existingGroups.push(group.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await Group.create({
|
||||||
|
name: group.name,
|
||||||
|
authProvider: AuthProviderType.LDAP
|
||||||
|
})
|
||||||
|
|
||||||
|
importedGroups.push(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingGroups.length > 0) {
|
||||||
|
process.logger.info(
|
||||||
|
'Failed to insert following groups as they already exist in DB:'
|
||||||
|
)
|
||||||
|
existingGroups.forEach((group) => process.logger.log(`* ${group}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
process.logger.info('associating users and groups')
|
||||||
|
|
||||||
|
for (const group of importedGroups) {
|
||||||
|
const dbGroup = await Group.findOne({ name: group.name })
|
||||||
|
if (dbGroup) {
|
||||||
|
for (const member of group.members) {
|
||||||
|
const user = importedUsers.find((user) => user.uid === member)
|
||||||
|
if (user) {
|
||||||
|
const dbUser = await User.findOne({ username: user.username })
|
||||||
|
if (dbUser) await dbGroup.addUser(dbUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.logger.info('setting permissions')
|
||||||
|
|
||||||
|
for (const permission of permissions) {
|
||||||
|
const newPermission = new Permission({
|
||||||
|
path: permission.path,
|
||||||
|
type: permission.type,
|
||||||
|
setting: permission.setting
|
||||||
|
})
|
||||||
|
|
||||||
|
if (permission.user) {
|
||||||
|
const dbUser = await User.findOne({ username: permission.user.username })
|
||||||
|
if (dbUser) newPermission.user = dbUser._id
|
||||||
|
} else if (permission.group) {
|
||||||
|
const dbGroup = await Group.findOne({ name: permission.group.name })
|
||||||
|
if (dbGroup) newPermission.group = dbGroup._id
|
||||||
|
}
|
||||||
|
await newPermission.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
process.logger.info('LDAP synchronization completed!')
|
||||||
|
|
||||||
|
return {
|
||||||
|
userCount: importedUsers.length,
|
||||||
|
groupCount: importedGroups.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAuthConfigDetail = () => {
|
||||||
|
const { AUTH_PROVIDERS } = process.env
|
||||||
|
|
||||||
|
const returnObj: any = {}
|
||||||
|
|
||||||
|
if (AUTH_PROVIDERS === AuthProviderType.LDAP) {
|
||||||
|
const {
|
||||||
|
LDAP_URL,
|
||||||
|
LDAP_BIND_DN,
|
||||||
|
LDAP_BIND_PASSWORD,
|
||||||
|
LDAP_USERS_BASE_DN,
|
||||||
|
LDAP_GROUPS_BASE_DN
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
returnObj.ldap = {
|
||||||
|
LDAP_URL: LDAP_URL ?? '',
|
||||||
|
LDAP_BIND_DN: LDAP_BIND_DN ?? '',
|
||||||
|
LDAP_BIND_PASSWORD: LDAP_BIND_PASSWORD ?? '',
|
||||||
|
LDAP_USERS_BASE_DN: LDAP_USERS_BASE_DN ?? '',
|
||||||
|
LDAP_GROUPS_BASE_DN: LDAP_GROUPS_BASE_DN ?? ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnObj
|
||||||
|
}
|
||||||
@@ -1,18 +1,27 @@
|
|||||||
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
|
import { Security, Route, Tags, Example, Post, Body, Get } from 'tsoa'
|
||||||
|
|
||||||
import Client, { ClientPayload } from '../model/Client'
|
import Client, {
|
||||||
|
ClientPayload,
|
||||||
|
NUMBER_OF_SECONDS_IN_A_DAY
|
||||||
|
} from '../model/Client'
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/client')
|
@Route('SASjsApi/client')
|
||||||
@Tags('Client')
|
@Tags('Client')
|
||||||
export class ClientController {
|
export class ClientController {
|
||||||
/**
|
/**
|
||||||
* @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task.
|
* @summary Admin only task. Create client with the following attributes:
|
||||||
|
* ClientId,
|
||||||
|
* ClientSecret,
|
||||||
|
* accessTokenExpiration (optional),
|
||||||
|
* refreshTokenExpiration (optional)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<ClientPayload>({
|
@Example<ClientPayload>({
|
||||||
clientId: 'someFormattedClientID1234',
|
clientId: 'someFormattedClientID1234',
|
||||||
clientSecret: 'someRandomCryptoString'
|
clientSecret: 'someRandomCryptoString',
|
||||||
|
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
||||||
|
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
})
|
})
|
||||||
@Post('/')
|
@Post('/')
|
||||||
public async createClient(
|
public async createClient(
|
||||||
@@ -20,10 +29,37 @@ export class ClientController {
|
|||||||
): Promise<ClientPayload> {
|
): Promise<ClientPayload> {
|
||||||
return createClient(body)
|
return createClient(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Admin only task. Returns the list of all the clients
|
||||||
|
*/
|
||||||
|
@Example<ClientPayload[]>([
|
||||||
|
{
|
||||||
|
clientId: 'someClientID1234',
|
||||||
|
clientSecret: 'someRandomCryptoString',
|
||||||
|
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
||||||
|
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clientId: 'someOtherClientID',
|
||||||
|
clientSecret: 'someOtherRandomCryptoString',
|
||||||
|
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
||||||
|
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
|
}
|
||||||
|
])
|
||||||
|
@Get('/')
|
||||||
|
public async getAllClients(): Promise<ClientPayload[]> {
|
||||||
|
return getAllClients()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createClient = async (data: any): Promise<ClientPayload> => {
|
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
|
||||||
const { clientId, clientSecret } = data
|
const {
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
accessTokenExpiration,
|
||||||
|
refreshTokenExpiration
|
||||||
|
} = data
|
||||||
|
|
||||||
// Checking if client is already in the database
|
// Checking if client is already in the database
|
||||||
const clientExist = await Client.findOne({ clientId })
|
const clientExist = await Client.findOne({ clientId })
|
||||||
@@ -32,13 +68,27 @@ const createClient = async (data: any): Promise<ClientPayload> => {
|
|||||||
// Create a new client
|
// Create a new client
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret
|
clientSecret,
|
||||||
|
accessTokenExpiration,
|
||||||
|
refreshTokenExpiration
|
||||||
})
|
})
|
||||||
|
|
||||||
const savedClient = await client.save()
|
const savedClient = await client.save()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
clientId: savedClient.clientId,
|
clientId: savedClient.clientId,
|
||||||
clientSecret: savedClient.clientSecret
|
clientSecret: savedClient.clientSecret,
|
||||||
|
accessTokenExpiration: savedClient.accessTokenExpiration,
|
||||||
|
refreshTokenExpiration: savedClient.refreshTokenExpiration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAllClients = async (): Promise<ClientPayload[]> => {
|
||||||
|
return Client.find({}).select({
|
||||||
|
_id: 0,
|
||||||
|
clientId: 1,
|
||||||
|
clientSecret: 1,
|
||||||
|
accessTokenExpiration: 1,
|
||||||
|
refreshTokenExpiration: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||||
import { ExecuteReturnJson, ExecutionController } from './internal'
|
import { ExecutionController } from './internal'
|
||||||
import { ExecuteReturnJsonResponse } from '.'
|
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getPreProgramVariables,
|
||||||
getUserAutoExec,
|
getUserAutoExec,
|
||||||
@@ -25,17 +24,17 @@ interface ExecuteCodePayload {
|
|||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/code')
|
@Route('SASjsApi/code')
|
||||||
@Tags('CODE')
|
@Tags('Code')
|
||||||
export class CodeController {
|
export class CodeController {
|
||||||
/**
|
/**
|
||||||
* Execute SAS code.
|
* Execute Code on the Specified Runtime
|
||||||
* @summary Run SAS Code and returns log
|
* @summary Run Code and Return Webout Content and Log
|
||||||
*/
|
*/
|
||||||
@Post('/execute')
|
@Post('/execute')
|
||||||
public async executeCode(
|
public async executeCode(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body: ExecuteCodePayload
|
@Body() body: ExecuteCodePayload
|
||||||
): Promise<ExecuteReturnJsonResponse> {
|
): Promise<string | Buffer> {
|
||||||
return executeCode(request, body)
|
return executeCode(request, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,22 +50,15 @@ const executeCode = async (
|
|||||||
: await getUserAutoExec()
|
: await getUserAutoExec()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log, httpHeaders } =
|
const { result } = await new ExecutionController().executeProgram({
|
||||||
(await new ExecutionController().executeProgram({
|
program: code,
|
||||||
program: code,
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
vars: { ...req.query, _debug: 131 },
|
||||||
vars: { ...req.query, _debug: 131 },
|
otherArgs: { userAutoExec },
|
||||||
otherArgs: { userAutoExec },
|
runTime: runTime
|
||||||
returnJson: true,
|
})
|
||||||
runTime: runTime
|
|
||||||
})) as ExecuteReturnJson
|
|
||||||
|
|
||||||
return {
|
return result
|
||||||
status: 'success',
|
|
||||||
_webout: webout as string,
|
|
||||||
log: parseLogToArray(log),
|
|
||||||
httpHeaders
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import {
|
|||||||
Body
|
Body
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
|
|
||||||
import Group, { GroupPayload } from '../model/Group'
|
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
|
import { AuthProviderType } from '../utils'
|
||||||
import { UserResponse } from './user'
|
import { UserResponse } from './user'
|
||||||
|
|
||||||
export interface GroupResponse {
|
export interface GroupResponse {
|
||||||
@@ -147,12 +148,14 @@ export class GroupController {
|
|||||||
@Delete('{groupId}')
|
@Delete('{groupId}')
|
||||||
public async deleteGroup(@Path() groupId: number) {
|
public async deleteGroup(@Path() groupId: number) {
|
||||||
const group = await Group.findOne({ groupId })
|
const group = await Group.findOne({ groupId })
|
||||||
if (group) return await group.remove()
|
if (!group)
|
||||||
throw {
|
throw {
|
||||||
code: 404,
|
code: 404,
|
||||||
status: 'Not Found',
|
status: 'Not Found',
|
||||||
message: 'Group not found.'
|
message: 'Group not found.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await group.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +244,20 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'Group not found.'
|
message: 'Group not found.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (group.name === PUBLIC_GROUP_NAME)
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.authProvider)
|
||||||
|
throw {
|
||||||
|
code: 405,
|
||||||
|
status: 'Method Not Allowed',
|
||||||
|
message: `Can't add/remove user to group created by external auth provider.`
|
||||||
|
}
|
||||||
|
|
||||||
const user = await User.findOne({ id: userId })
|
const user = await User.findOne({ id: userId })
|
||||||
if (!user)
|
if (!user)
|
||||||
throw {
|
throw {
|
||||||
@@ -249,6 +266,13 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'User not found.'
|
message: 'User not found.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.authProvider)
|
||||||
|
throw {
|
||||||
|
code: 405,
|
||||||
|
status: 'Method Not Allowed',
|
||||||
|
message: `Can't add/remove user to group created by external auth provider.`
|
||||||
|
}
|
||||||
|
|
||||||
const updatedGroup =
|
const updatedGroup =
|
||||||
action === 'addUser'
|
action === 'addUser'
|
||||||
? await group.addUser(user)
|
? await group.addUser(user)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './auth'
|
export * from './auth'
|
||||||
|
export * from './authConfig'
|
||||||
export * from './client'
|
export * from './client'
|
||||||
export * from './code'
|
export * from './code'
|
||||||
export * from './drive'
|
export * from './drive'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Route, Tags, Example, Get } from 'tsoa'
|
import { Route, Tags, Example, Get } from 'tsoa'
|
||||||
import { getAuthorizedRoutes } from '../utils'
|
import { getAuthorizedRoutes } from '../utils'
|
||||||
export interface AuthorizedRoutesResponse {
|
export interface AuthorizedRoutesResponse {
|
||||||
URIs: string[]
|
paths: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InfoResponse {
|
export interface InfoResponse {
|
||||||
@@ -42,16 +42,16 @@ export class InfoController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Get authorized routes.
|
* @summary Get the list of available routes to which permissions can be applied. Used to populate the dialog in the URI Permissions feature.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<AuthorizedRoutesResponse>({
|
@Example<AuthorizedRoutesResponse>({
|
||||||
URIs: ['/AppStream', '/SASjsApi/stp/execute']
|
paths: ['/AppStream', '/SASjsApi/stp/execute']
|
||||||
})
|
})
|
||||||
@Get('/authorizedRoutes')
|
@Get('/authorizedRoutes')
|
||||||
public authorizedRoutes(): AuthorizedRoutesResponse {
|
public authorizedRoutes(): AuthorizedRoutesResponse {
|
||||||
const response = {
|
const response = {
|
||||||
URIs: getAuthorizedRoutes()
|
paths: getAuthorizedRoutes()
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ export interface ExecuteReturnRaw {
|
|||||||
result: string | Buffer
|
result: string | Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecuteReturnJson {
|
|
||||||
httpHeaders: HTTPHeaders
|
|
||||||
webout: string | Buffer
|
|
||||||
log?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExecuteFileParams {
|
interface ExecuteFileParams {
|
||||||
programPath: string
|
programPath: string
|
||||||
preProgramVariables: PreProgramVars
|
preProgramVariables: PreProgramVars
|
||||||
@@ -34,6 +28,7 @@ interface ExecuteFileParams {
|
|||||||
returnJson?: boolean
|
returnJson?: boolean
|
||||||
session?: Session
|
session?: Session
|
||||||
runTime: RunTimeType
|
runTime: RunTimeType
|
||||||
|
forceStringResult?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
||||||
@@ -48,7 +43,8 @@ export class ExecutionController {
|
|||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
returnJson,
|
||||||
session,
|
session,
|
||||||
runTime
|
runTime,
|
||||||
|
forceStringResult
|
||||||
}: ExecuteFileParams) {
|
}: ExecuteFileParams) {
|
||||||
const program = await readFile(programPath)
|
const program = await readFile(programPath)
|
||||||
|
|
||||||
@@ -59,7 +55,8 @@ export class ExecutionController {
|
|||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
returnJson,
|
||||||
session,
|
session,
|
||||||
runTime
|
runTime,
|
||||||
|
forceStringResult
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +65,10 @@ export class ExecutionController {
|
|||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
vars,
|
vars,
|
||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
|
||||||
session: sessionByFileUpload,
|
session: sessionByFileUpload,
|
||||||
runTime
|
runTime,
|
||||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
forceStringResult
|
||||||
|
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||||
const sessionController = getSessionController(runTime)
|
const sessionController = getSessionController(runTime)
|
||||||
|
|
||||||
const session =
|
const session =
|
||||||
@@ -81,6 +78,7 @@ export class ExecutionController {
|
|||||||
|
|
||||||
const logPath = path.join(session.path, 'log.log')
|
const logPath = path.join(session.path, 'log.log')
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
|
|
||||||
const weboutPath = path.join(session.path, 'webout.txt')
|
const weboutPath = path.join(session.path, 'webout.txt')
|
||||||
const tokenFile = path.join(session.path, 'reqHeaders.txt')
|
const tokenFile = path.join(session.path, 'reqHeaders.txt')
|
||||||
|
|
||||||
@@ -96,6 +94,7 @@ export class ExecutionController {
|
|||||||
vars,
|
vars,
|
||||||
session,
|
session,
|
||||||
weboutPath,
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
tokenFile,
|
tokenFile,
|
||||||
runTime,
|
runTime,
|
||||||
logPath,
|
logPath,
|
||||||
@@ -107,13 +106,15 @@ export class ExecutionController {
|
|||||||
? await readFile(headersPath)
|
? await readFile(headersPath)
|
||||||
: ''
|
: ''
|
||||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
const fileResponse: boolean =
|
|
||||||
httpHeaders.hasOwnProperty('content-type') &&
|
if (isDebugOn(vars)) {
|
||||||
!returnJson && // not a POST Request
|
httpHeaders['content-type'] = 'text/plain'
|
||||||
!isDebugOn(vars) // Debug is not enabled
|
}
|
||||||
|
|
||||||
|
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
||||||
|
|
||||||
const webout = (await fileExists(weboutPath))
|
const webout = (await fileExists(weboutPath))
|
||||||
? fileResponse
|
? fileResponse && !forceStringResult
|
||||||
? await readFileBinary(weboutPath)
|
? await readFileBinary(weboutPath)
|
||||||
: await readFile(weboutPath)
|
: await readFile(weboutPath)
|
||||||
: ''
|
: ''
|
||||||
@@ -121,19 +122,11 @@ export class ExecutionController {
|
|||||||
// it should be deleted by scheduleSessionDestroy
|
// it should be deleted by scheduleSessionDestroy
|
||||||
session.inUse = false
|
session.inUse = false
|
||||||
|
|
||||||
if (returnJson) {
|
|
||||||
return {
|
|
||||||
httpHeaders,
|
|
||||||
webout,
|
|
||||||
log: isDebugOn(vars) || session.crashed ? log : undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpHeaders,
|
httpHeaders,
|
||||||
result:
|
result:
|
||||||
isDebugOn(vars) || session.crashed
|
isDebugOn(vars) || session.crashed
|
||||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
? `${webout}\n${process.logsUUID}\n${log}`
|
||||||
: webout
|
: webout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Session } from '../../types'
|
|||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import {
|
import {
|
||||||
|
getPackagesFolder,
|
||||||
getSessionsFolder,
|
getSessionsFolder,
|
||||||
generateUniqueFileName,
|
generateUniqueFileName,
|
||||||
sysInitCompiledPath,
|
sysInitCompiledPath,
|
||||||
@@ -19,13 +20,41 @@ import {
|
|||||||
|
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
|
|
||||||
abstract class SessionController {
|
export class SessionController {
|
||||||
protected sessions: Session[] = []
|
protected sessions: Session[] = []
|
||||||
|
|
||||||
protected getReadySessions = (): Session[] =>
|
protected getReadySessions = (): Session[] =>
|
||||||
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
||||||
|
|
||||||
protected abstract createSession(): Promise<Session>
|
protected async createSession(): Promise<Session> {
|
||||||
|
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||||
|
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
||||||
|
|
||||||
|
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||||
|
// death time of session is 15 mins from creation
|
||||||
|
const deathTimeStamp = (
|
||||||
|
parseInt(creationTimeStamp) +
|
||||||
|
15 * 60 * 1000 -
|
||||||
|
1000
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
const session: Session = {
|
||||||
|
id: sessionId,
|
||||||
|
ready: true,
|
||||||
|
inUse: true,
|
||||||
|
consumed: false,
|
||||||
|
completed: false,
|
||||||
|
creationTimeStamp,
|
||||||
|
deathTimeStamp,
|
||||||
|
path: sessionFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
|
await createFile(headersPath, 'content-type: text/html; charset=utf-8')
|
||||||
|
|
||||||
|
this.sessions.push(session)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
public async getSession() {
|
public async getSession() {
|
||||||
const readySessions = this.getReadySessions()
|
const readySessions = this.getReadySessions()
|
||||||
@@ -64,6 +93,9 @@ export class SASSessionController extends SessionController {
|
|||||||
path: sessionFolder
|
path: sessionFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
|
await createFile(headersPath, 'content-type: text/html; charset=utf-8\n')
|
||||||
|
|
||||||
// we do not want to leave sessions running forever
|
// we do not want to leave sessions running forever
|
||||||
// we clean them up after a predefined period, if unused
|
// we clean them up after a predefined period, if unused
|
||||||
this.scheduleSessionDestroy(session)
|
this.scheduleSessionDestroy(session)
|
||||||
@@ -73,7 +105,8 @@ export class SASSessionController extends SessionController {
|
|||||||
|
|
||||||
// the autoexec file is executed on SAS startup
|
// the autoexec file is executed on SAS startup
|
||||||
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
||||||
const contentForAutoExec = `/* compiled systemInit */
|
const contentForAutoExec = `filename packages "${getPackagesFolder()}";
|
||||||
|
/* compiled systemInit */
|
||||||
${compiledSystemInitContent}
|
${compiledSystemInitContent}
|
||||||
/* autoexec */
|
/* autoexec */
|
||||||
${autoExecContent}`
|
${autoExecContent}`
|
||||||
@@ -101,21 +134,24 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? '-nologo' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
||||||
isWindows() ? '-nologo' : ''
|
process.sasLoc!.endsWith('sas.exe') ? '-NOPRNGETLIST' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
session.completed = true
|
session.completed = true
|
||||||
console.log('session completed', session)
|
process.logger.info('session completed', session)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
session.completed = true
|
session.completed = true
|
||||||
session.crashed = err.toString()
|
session.crashed = err.toString()
|
||||||
console.log('session crashed', session.id, session.crashed)
|
process.logger.error('session crashed', session.id, session.crashed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// we have a triggered session - add to array
|
// we have a triggered session - add to array
|
||||||
@@ -135,12 +171,15 @@ ${autoExecContent}`
|
|||||||
while ((await fileExists(codeFilePath)) && !session.crashed) {}
|
while ((await fileExists(codeFilePath)) && !session.crashed) {}
|
||||||
|
|
||||||
if (session.crashed)
|
if (session.crashed)
|
||||||
console.log('session crashed! while waiting to be ready', session.crashed)
|
process.logger.error(
|
||||||
|
'session crashed! while waiting to be ready',
|
||||||
|
session.crashed
|
||||||
|
)
|
||||||
|
|
||||||
session.ready = true
|
session.ready = true
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteSession(session: Session) {
|
private async deleteSession(session: Session) {
|
||||||
// remove the temporary files, to avoid buildup
|
// remove the temporary files, to avoid buildup
|
||||||
await deleteFolder(session.path)
|
await deleteFolder(session.path)
|
||||||
|
|
||||||
@@ -165,66 +204,20 @@ ${autoExecContent}`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JSSessionController extends SessionController {
|
|
||||||
protected async createSession(): Promise<Session> {
|
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
|
||||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
|
||||||
// death time of session is 15 mins from creation
|
|
||||||
const deathTimeStamp = (
|
|
||||||
parseInt(creationTimeStamp) +
|
|
||||||
15 * 60 * 1000 -
|
|
||||||
1000
|
|
||||||
).toString()
|
|
||||||
|
|
||||||
const session: Session = {
|
|
||||||
id: sessionId,
|
|
||||||
ready: true,
|
|
||||||
inUse: true,
|
|
||||||
consumed: false,
|
|
||||||
completed: false,
|
|
||||||
creationTimeStamp,
|
|
||||||
deathTimeStamp,
|
|
||||||
path: sessionFolder
|
|
||||||
}
|
|
||||||
|
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
|
||||||
await createFile(headersPath, 'Content-type: application/json')
|
|
||||||
|
|
||||||
this.sessions.push(session)
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSessionController = (
|
export const getSessionController = (
|
||||||
runTime: RunTimeType
|
runTime: RunTimeType
|
||||||
): SASSessionController | JSSessionController => {
|
): SessionController => {
|
||||||
if (runTime === RunTimeType.SAS) {
|
if (runTime === RunTimeType.SAS) {
|
||||||
return getSASSessionController()
|
process.sasSessionController =
|
||||||
|
process.sasSessionController || new SASSessionController()
|
||||||
|
|
||||||
|
return process.sasSessionController
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runTime === RunTimeType.JS) {
|
process.sessionController =
|
||||||
return getJSSessionController()
|
process.sessionController || new SessionController()
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('No Runtime is configured')
|
return process.sessionController
|
||||||
}
|
|
||||||
|
|
||||||
const getSASSessionController = (): SASSessionController => {
|
|
||||||
if (process.sasSessionController) return process.sasSessionController
|
|
||||||
|
|
||||||
process.sasSessionController = new SASSessionController()
|
|
||||||
|
|
||||||
return process.sasSessionController
|
|
||||||
}
|
|
||||||
|
|
||||||
const getJSSessionController = (): JSSessionController => {
|
|
||||||
if (process.jsSessionController) return process.jsSessionController
|
|
||||||
|
|
||||||
process.jsSessionController = new JSSessionController()
|
|
||||||
|
|
||||||
return process.jsSessionController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoExecContent = `
|
const autoExecContent = `
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isWindows } from '@sasjs/utils'
|
import { escapeWinSlashes } from '@sasjs/utils'
|
||||||
import { PreProgramVars, Session } from '../../types'
|
import { PreProgramVars, Session } from '../../types'
|
||||||
import { generateFileUploadJSCode } from '../../utils'
|
import { generateFileUploadJSCode } from '../../utils'
|
||||||
import { ExecutionVars } from './'
|
import { ExecutionVars } from './'
|
||||||
@@ -9,6 +9,7 @@ export const createJSProgram = async (
|
|||||||
vars: ExecutionVars,
|
vars: ExecutionVars,
|
||||||
session: Session,
|
session: Session,
|
||||||
weboutPath: string,
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
tokenFile: string,
|
tokenFile: string,
|
||||||
otherArgs?: any
|
otherArgs?: any
|
||||||
) => {
|
) => {
|
||||||
@@ -20,18 +21,15 @@ export const createJSProgram = async (
|
|||||||
|
|
||||||
const preProgramVarStatments = `
|
const preProgramVarStatments = `
|
||||||
let _webout = '';
|
let _webout = '';
|
||||||
const weboutPath = '${
|
const weboutPath = '${escapeWinSlashes(weboutPath)}';
|
||||||
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
const _SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
|
||||||
}';
|
const _SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
|
||||||
const _sasjs_tokenfile = '${
|
const _SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||||
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
const _SASJS_USERID = '${preProgramVariables?.userId}';
|
||||||
}';
|
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||||
const _sasjs_username = '${preProgramVariables?.username}';
|
const _METAPERSON = _SASJS_DISPLAYNAME;
|
||||||
const _sasjs_userid = '${preProgramVariables?.userId}';
|
const _METAUSER = _SASJS_USERNAME;
|
||||||
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
const SASJSPROCESSMODE = 'Stored Program';
|
||||||
const _metaperson = _sasjs_displayname;
|
|
||||||
const _metauser = _sasjs_username;
|
|
||||||
const sasjsprocessmode = 'Stored Program';
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const requiredModules = `const fs = require('fs')`
|
const requiredModules = `const fs = require('fs')`
|
||||||
@@ -55,14 +53,15 @@ if (_webout) {
|
|||||||
`
|
`
|
||||||
// if no files are uploaded filesNamesMap will be undefined
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
if (otherArgs?.filesNamesMap) {
|
if (otherArgs?.filesNamesMap) {
|
||||||
const uploadJSCode = await generateFileUploadJSCode(
|
const uploadJsCode = await generateFileUploadJSCode(
|
||||||
otherArgs.filesNamesMap,
|
otherArgs.filesNamesMap,
|
||||||
session.path
|
session.path
|
||||||
)
|
)
|
||||||
|
|
||||||
//If js code for the file is generated it will be appended to the top of jsCode
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
if (uploadJSCode.length > 0) {
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
program = `${uploadJSCode}\n` + program
|
if (uploadJsCode.length > 0) {
|
||||||
|
program = `${uploadJsCode}\n` + program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return requiredModules + program
|
return requiredModules + program
|
||||||
|
|||||||
64
api/src/controllers/internal/createPythonProgram.ts
Normal file
64
api/src/controllers/internal/createPythonProgram.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { escapeWinSlashes } from '@sasjs/utils'
|
||||||
|
import { PreProgramVars, Session } from '../../types'
|
||||||
|
import { generateFileUploadPythonCode } from '../../utils'
|
||||||
|
import { ExecutionVars } from './'
|
||||||
|
|
||||||
|
export const createPythonProgram = async (
|
||||||
|
program: string,
|
||||||
|
preProgramVariables: PreProgramVars,
|
||||||
|
vars: ExecutionVars,
|
||||||
|
session: Session,
|
||||||
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
|
tokenFile: string,
|
||||||
|
otherArgs?: any
|
||||||
|
) => {
|
||||||
|
const varStatments = Object.keys(vars).reduce(
|
||||||
|
(computed: string, key: string) => `${computed}${key} = '${vars[key]}';\n`,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
const preProgramVarStatments = `
|
||||||
|
_SASJS_SESSION_PATH = '${escapeWinSlashes(session.path)}';
|
||||||
|
_WEBOUT = '${escapeWinSlashes(weboutPath)}';
|
||||||
|
_SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
|
||||||
|
_SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
|
||||||
|
_SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||||
|
_SASJS_USERID = '${preProgramVariables?.userId}';
|
||||||
|
_SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||||
|
_METAPERSON = _SASJS_DISPLAYNAME;
|
||||||
|
_METAUSER = _SASJS_USERNAME;
|
||||||
|
SASJSPROCESSMODE = 'Stored Program';
|
||||||
|
`
|
||||||
|
|
||||||
|
const requiredModules = `import os`
|
||||||
|
|
||||||
|
program = `
|
||||||
|
# runtime vars
|
||||||
|
${varStatments}
|
||||||
|
|
||||||
|
# dynamic user-provided vars
|
||||||
|
${preProgramVarStatments}
|
||||||
|
|
||||||
|
# change working directory to session folder
|
||||||
|
os.chdir(_SASJS_SESSION_PATH)
|
||||||
|
|
||||||
|
# actual job code
|
||||||
|
${program}
|
||||||
|
|
||||||
|
`
|
||||||
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
|
if (otherArgs?.filesNamesMap) {
|
||||||
|
const uploadPythonCode = await generateFileUploadPythonCode(
|
||||||
|
otherArgs.filesNamesMap,
|
||||||
|
session.path
|
||||||
|
)
|
||||||
|
|
||||||
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
|
if (uploadPythonCode.length > 0) {
|
||||||
|
program = `${uploadPythonCode}\n` + program
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredModules + program
|
||||||
|
}
|
||||||
64
api/src/controllers/internal/createRProgram.ts
Normal file
64
api/src/controllers/internal/createRProgram.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { escapeWinSlashes } from '@sasjs/utils'
|
||||||
|
import { PreProgramVars, Session } from '../../types'
|
||||||
|
import { generateFileUploadRCode } from '../../utils'
|
||||||
|
import { ExecutionVars } from '.'
|
||||||
|
|
||||||
|
export const createRProgram = async (
|
||||||
|
program: string,
|
||||||
|
preProgramVariables: PreProgramVars,
|
||||||
|
vars: ExecutionVars,
|
||||||
|
session: Session,
|
||||||
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
|
tokenFile: string,
|
||||||
|
otherArgs?: any
|
||||||
|
) => {
|
||||||
|
const varStatments = Object.keys(vars).reduce(
|
||||||
|
(computed: string, key: string) => `${computed}.${key} <- '${vars[key]}'\n`,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
const preProgramVarStatments = `
|
||||||
|
._SASJS_SESSION_PATH <- '${escapeWinSlashes(session.path)}';
|
||||||
|
._WEBOUT <- '${escapeWinSlashes(weboutPath)}';
|
||||||
|
._SASJS_WEBOUT_HEADERS <- '${escapeWinSlashes(headersPath)}';
|
||||||
|
._SASJS_TOKENFILE <- '${escapeWinSlashes(tokenFile)}';
|
||||||
|
._SASJS_USERNAME <- '${preProgramVariables?.username}';
|
||||||
|
._SASJS_USERID <- '${preProgramVariables?.userId}';
|
||||||
|
._SASJS_DISPLAYNAME <- '${preProgramVariables?.displayName}';
|
||||||
|
._METAPERSON <- ._SASJS_DISPLAYNAME;
|
||||||
|
._METAUSER <- ._SASJS_USERNAME;
|
||||||
|
SASJSPROCESSMODE <- 'Stored Program';
|
||||||
|
`
|
||||||
|
|
||||||
|
const requiredModules = ``
|
||||||
|
|
||||||
|
program = `
|
||||||
|
# runtime vars
|
||||||
|
${varStatments}
|
||||||
|
|
||||||
|
# dynamic user-provided vars
|
||||||
|
${preProgramVarStatments}
|
||||||
|
|
||||||
|
# change working directory to session folder
|
||||||
|
setwd(._SASJS_SESSION_PATH)
|
||||||
|
|
||||||
|
# actual job code
|
||||||
|
${program}
|
||||||
|
|
||||||
|
`
|
||||||
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
|
if (otherArgs?.filesNamesMap) {
|
||||||
|
const uploadRCode = await generateFileUploadRCode(
|
||||||
|
otherArgs.filesNamesMap,
|
||||||
|
session.path
|
||||||
|
)
|
||||||
|
|
||||||
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
|
if (uploadRCode.length > 0) {
|
||||||
|
program = `${uploadRCode}\n` + program
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredModules + program
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ export const createSASProgram = async (
|
|||||||
vars: ExecutionVars,
|
vars: ExecutionVars,
|
||||||
session: Session,
|
session: Session,
|
||||||
weboutPath: string,
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
tokenFile: string,
|
tokenFile: string,
|
||||||
otherArgs?: any
|
otherArgs?: any
|
||||||
) => {
|
) => {
|
||||||
@@ -23,10 +24,14 @@ export const createSASProgram = async (
|
|||||||
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
||||||
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
||||||
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
||||||
|
%let _sasjs_webout_headers=${headersPath};
|
||||||
%let _metaperson=&_sasjs_displayname;
|
%let _metaperson=&_sasjs_displayname;
|
||||||
%let _metauser=&_sasjs_username;
|
%let _metauser=&_sasjs_username;
|
||||||
|
|
||||||
|
/* the below is here for compatibility and will be removed in a future release */
|
||||||
|
%let sasjs_stpsrv_header_loc=&_sasjs_webout_headers;
|
||||||
|
|
||||||
%let sasjsprocessmode=Stored Program;
|
%let sasjsprocessmode=Stored Program;
|
||||||
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
|
|
||||||
|
|
||||||
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
||||||
%macro _sasjs_server_init();
|
%macro _sasjs_server_init();
|
||||||
@@ -34,6 +39,7 @@ export const createSASProgram = async (
|
|||||||
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
||||||
%mend;
|
%mend;
|
||||||
%_sasjs_server_init()
|
%_sasjs_server_init()
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
@@ -60,7 +66,8 @@ ${program}`
|
|||||||
session.path
|
session.path
|
||||||
)
|
)
|
||||||
|
|
||||||
//If sas code for the file is generated it will be appended to the top of sasCode
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
if (uploadSasCode.length > 0) {
|
if (uploadSasCode.length > 0) {
|
||||||
program = `${uploadSasCode}` + program
|
program = `${uploadSasCode}` + program
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ export * from './Execution'
|
|||||||
export * from './FileUploadController'
|
export * from './FileUploadController'
|
||||||
export * from './createSASProgram'
|
export * from './createSASProgram'
|
||||||
export * from './createJSProgram'
|
export * from './createJSProgram'
|
||||||
|
export * from './createPythonProgram'
|
||||||
|
export * from './createRProgram'
|
||||||
export * from './processProgram'
|
export * from './processProgram'
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import { WriteStream, createWriteStream } from 'fs'
|
||||||
import { execFileSync } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import { once } from 'stream'
|
import { once } from 'stream'
|
||||||
import { createFile, moveFile } from '@sasjs/utils'
|
import { createFile, moveFile } from '@sasjs/utils'
|
||||||
import { PreProgramVars, Session } from '../../types'
|
import { PreProgramVars, Session } from '../../types'
|
||||||
import { RunTimeType } from '../../utils'
|
import { RunTimeType } from '../../utils'
|
||||||
import { ExecutionVars, createSASProgram, createJSProgram } from './'
|
import {
|
||||||
|
ExecutionVars,
|
||||||
|
createSASProgram,
|
||||||
|
createJSProgram,
|
||||||
|
createPythonProgram,
|
||||||
|
createRProgram
|
||||||
|
} from './'
|
||||||
|
|
||||||
export const processProgram = async (
|
export const processProgram = async (
|
||||||
program: string,
|
program: string,
|
||||||
@@ -13,54 +19,20 @@ export const processProgram = async (
|
|||||||
vars: ExecutionVars,
|
vars: ExecutionVars,
|
||||||
session: Session,
|
session: Session,
|
||||||
weboutPath: string,
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
tokenFile: string,
|
tokenFile: string,
|
||||||
runTime: RunTimeType,
|
runTime: RunTimeType,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
otherArgs?: any
|
otherArgs?: any
|
||||||
) => {
|
) => {
|
||||||
if (runTime === RunTimeType.JS) {
|
if (runTime === RunTimeType.SAS) {
|
||||||
program = await createJSProgram(
|
|
||||||
program,
|
|
||||||
preProgramVariables,
|
|
||||||
vars,
|
|
||||||
session,
|
|
||||||
weboutPath,
|
|
||||||
tokenFile,
|
|
||||||
otherArgs
|
|
||||||
)
|
|
||||||
|
|
||||||
const codePath = path.join(session.path, 'code.js')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createFile(codePath, program)
|
|
||||||
|
|
||||||
// create a stream that will write to console outputs to log file
|
|
||||||
const writeStream = fs.createWriteStream(logPath)
|
|
||||||
|
|
||||||
// waiting for the open event so that we can have underlying file descriptor
|
|
||||||
await once(writeStream, 'open')
|
|
||||||
|
|
||||||
execFileSync(process.nodeLoc!, [codePath], {
|
|
||||||
stdio: ['ignore', writeStream, writeStream]
|
|
||||||
})
|
|
||||||
|
|
||||||
// copy the code.js program to log and end write stream
|
|
||||||
writeStream.end(program)
|
|
||||||
|
|
||||||
session.completed = true
|
|
||||||
console.log('session completed', session)
|
|
||||||
} catch (err: any) {
|
|
||||||
session.completed = true
|
|
||||||
session.crashed = err.toString()
|
|
||||||
console.log('session crashed', session.id, session.crashed)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
program = await createSASProgram(
|
program = await createSASProgram(
|
||||||
program,
|
program,
|
||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
vars,
|
vars,
|
||||||
session,
|
session,
|
||||||
weboutPath,
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
tokenFile,
|
tokenFile,
|
||||||
otherArgs
|
otherArgs
|
||||||
)
|
)
|
||||||
@@ -80,7 +52,111 @@ export const processProgram = async (
|
|||||||
while (!session.completed) {
|
while (!session.completed) {
|
||||||
await delay(50)
|
await delay(50)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let codePath: string
|
||||||
|
let executablePath: string
|
||||||
|
switch (runTime) {
|
||||||
|
case RunTimeType.JS:
|
||||||
|
program = await createJSProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
session,
|
||||||
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
|
tokenFile,
|
||||||
|
otherArgs
|
||||||
|
)
|
||||||
|
codePath = path.join(session.path, 'code.js')
|
||||||
|
executablePath = process.nodeLoc!
|
||||||
|
|
||||||
|
break
|
||||||
|
case RunTimeType.PY:
|
||||||
|
program = await createPythonProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
session,
|
||||||
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
|
tokenFile,
|
||||||
|
otherArgs
|
||||||
|
)
|
||||||
|
codePath = path.join(session.path, 'code.py')
|
||||||
|
executablePath = process.pythonLoc!
|
||||||
|
|
||||||
|
break
|
||||||
|
case RunTimeType.R:
|
||||||
|
program = await createRProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
session,
|
||||||
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
|
tokenFile,
|
||||||
|
otherArgs
|
||||||
|
)
|
||||||
|
codePath = path.join(session.path, 'code.r')
|
||||||
|
executablePath = process.rLoc!
|
||||||
|
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid runtime!')
|
||||||
|
}
|
||||||
|
|
||||||
|
await createFile(codePath, program)
|
||||||
|
|
||||||
|
// create a stream that will write to console outputs to log file
|
||||||
|
const writeStream = createWriteStream(logPath)
|
||||||
|
// waiting for the open event so that we can have underlying file descriptor
|
||||||
|
await once(writeStream, 'open')
|
||||||
|
|
||||||
|
await execFilePromise(executablePath, [codePath], writeStream)
|
||||||
|
.then(() => {
|
||||||
|
session.completed = true
|
||||||
|
process.logger.info('session completed', session)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
session.completed = true
|
||||||
|
session.crashed = err.toString()
|
||||||
|
process.logger.error('session crashed', session.id, session.crashed)
|
||||||
|
})
|
||||||
|
|
||||||
|
// copy the code file to log and end write stream
|
||||||
|
writeStream.end(program)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promisified child_process.execFile
|
||||||
|
*
|
||||||
|
* @param file - The name or path of the executable file to run.
|
||||||
|
* @param args - List of string arguments.
|
||||||
|
* @param writeStream - Child process stdout and stderr will be piped to it.
|
||||||
|
*
|
||||||
|
* @returns {Promise<{ stdout: string, stderr: string }>}
|
||||||
|
*/
|
||||||
|
const execFilePromise = (
|
||||||
|
file: string,
|
||||||
|
args: string[],
|
||||||
|
writeStream: WriteStream
|
||||||
|
): Promise<{ stdout: string; stderr: string }> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = execFile(file, args, (err, stdout, stderr) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
|
||||||
|
resolve({ stdout, stderr })
|
||||||
|
})
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
writeStream.write(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
writeStream.write(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|||||||
283
api/src/controllers/mock-sas9.ts
Normal file
283
api/src/controllers/mock-sas9.ts
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import { readFile } from '@sasjs/utils'
|
||||||
|
import express from 'express'
|
||||||
|
import path from 'path'
|
||||||
|
import { Request, Post, Get } from 'tsoa'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { ExecutionController } from './internal'
|
||||||
|
import {
|
||||||
|
getPreProgramVariables,
|
||||||
|
getRunTimeAndFilePath,
|
||||||
|
makeFilesNamesMap
|
||||||
|
} from '../utils'
|
||||||
|
import { MulterFile } from '../types/Upload'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
export interface Sas9Response {
|
||||||
|
content: string
|
||||||
|
redirect?: string
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockFileRead {
|
||||||
|
content: string
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockSas9Controller {
|
||||||
|
private loggedIn: string | undefined
|
||||||
|
private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
||||||
|
|
||||||
|
@Get('/SASStoredProcess')
|
||||||
|
public async sasStoredProcess(
|
||||||
|
@Request() req: express.Request
|
||||||
|
): Promise<Sas9Response> {
|
||||||
|
const username = req.query._username?.toString() || undefined
|
||||||
|
const password = req.query._password?.toString() || undefined
|
||||||
|
|
||||||
|
if (username && password) this.loggedIn = req.body.username
|
||||||
|
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let program = req.query._program?.toString() || undefined
|
||||||
|
const filePath: string[] = program
|
||||||
|
? program.replace('/', '').split('/')
|
||||||
|
: ['generic', 'sas-stored-process']
|
||||||
|
|
||||||
|
if (program) {
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
this.mocksPath,
|
||||||
|
'sas9',
|
||||||
|
...filePath
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'sas9',
|
||||||
|
...filePath
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASStoredProcess/do')
|
||||||
|
public async sasStoredProcessDoGet(
|
||||||
|
@Request() req: express.Request
|
||||||
|
): Promise<Sas9Response> {
|
||||||
|
const username = req.query._username?.toString() || undefined
|
||||||
|
const password = req.query._password?.toString() || undefined
|
||||||
|
|
||||||
|
if (username && password) this.loggedIn = username
|
||||||
|
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = req.query._program ?? req.body?._program
|
||||||
|
const filePath: string[] = ['generic', 'sas-stored-process']
|
||||||
|
|
||||||
|
if (program) {
|
||||||
|
const vars = { ...req.query, ...req.body, _requestMethod: req.method }
|
||||||
|
const otherArgs = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { codePath, runTime } = await getRunTimeAndFilePath(
|
||||||
|
program + '.js'
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await new ExecutionController().executeFile({
|
||||||
|
programPath: codePath,
|
||||||
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
|
vars: vars,
|
||||||
|
otherArgs: otherArgs,
|
||||||
|
runTime,
|
||||||
|
forceStringResult: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: result.result as string
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
process.logger.error('err', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: 'No webout returned.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'sas9',
|
||||||
|
...filePath
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/SASStoredProcess/do/')
|
||||||
|
public async sasStoredProcessDoPost(
|
||||||
|
@Request() req: express.Request
|
||||||
|
): Promise<Sas9Response> {
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPublicAccount()) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/Login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = req.query._program ?? req.body?._program
|
||||||
|
const vars = {
|
||||||
|
...req.query,
|
||||||
|
...req.body,
|
||||||
|
_requestMethod: req.method,
|
||||||
|
_driveLoc: process.driveLoc
|
||||||
|
}
|
||||||
|
const filesNamesMap = req.files?.length
|
||||||
|
? makeFilesNamesMap(req.files as MulterFile[])
|
||||||
|
: null
|
||||||
|
const otherArgs = { filesNamesMap: filesNamesMap }
|
||||||
|
const { codePath, runTime } = await getRunTimeAndFilePath(program + '.js')
|
||||||
|
try {
|
||||||
|
const result = await new ExecutionController().executeFile({
|
||||||
|
programPath: codePath,
|
||||||
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
|
vars: vars,
|
||||||
|
otherArgs: otherArgs,
|
||||||
|
runTime,
|
||||||
|
session: req.sasjsSession,
|
||||||
|
forceStringResult: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: result.result as string
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
process.logger.error('err', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: 'No webout returned.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASLogon/login')
|
||||||
|
public async loginGet(): Promise<Sas9Response> {
|
||||||
|
if (this.loggedIn) {
|
||||||
|
if (this.isPublicAccount()) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASStoredProcess/Logoff?publicDenied=true'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'sas9',
|
||||||
|
'generic',
|
||||||
|
'logged-in'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'sas9',
|
||||||
|
'generic',
|
||||||
|
'login'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/SASLogon/login')
|
||||||
|
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
||||||
|
if (req.body.lt && req.body.lt !== 'validtoken')
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loggedIn = req.body.username
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'sas9',
|
||||||
|
'generic',
|
||||||
|
'logged-in'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASLogon/logout')
|
||||||
|
public async logout(req: express.Request): Promise<Sas9Response> {
|
||||||
|
this.loggedIn = undefined
|
||||||
|
|
||||||
|
if (req.query.publicDenied === 'true') {
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'sas9',
|
||||||
|
'generic',
|
||||||
|
'public-access-denied'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'sas9',
|
||||||
|
'generic',
|
||||||
|
'logged-out'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASStoredProcess/Logoff') //publicDenied=true
|
||||||
|
public async logoff(req: express.Request): Promise<Sas9Response> {
|
||||||
|
const params = req.query.publicDenied
|
||||||
|
? `?publicDenied=${req.query.publicDenied}`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/logout' + params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMockResponseFromFile = async (
|
||||||
|
filePath: string[]
|
||||||
|
): Promise<MockFileRead> => {
|
||||||
|
const filePathParsed = path.join(...filePath)
|
||||||
|
let error: boolean = false
|
||||||
|
|
||||||
|
let file = await readFile(filePathParsed).catch((err: any) => {
|
||||||
|
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
|
||||||
|
process.logger.error(errMsg)
|
||||||
|
|
||||||
|
error = true
|
||||||
|
|
||||||
|
return errMsg
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: file,
|
||||||
|
error: error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import express from 'express'
|
||||||
import {
|
import {
|
||||||
Security,
|
Security,
|
||||||
Route,
|
Route,
|
||||||
@@ -8,7 +9,8 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
Patch,
|
Patch,
|
||||||
Delete,
|
Delete,
|
||||||
Body
|
Body,
|
||||||
|
Request
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
|
|
||||||
import Permission from '../model/Permission'
|
import Permission from '../model/Permission'
|
||||||
@@ -17,12 +19,16 @@ import Group from '../model/Group'
|
|||||||
import { UserResponse } from './user'
|
import { UserResponse } from './user'
|
||||||
import { GroupDetailsResponse } from './group'
|
import { GroupDetailsResponse } from './group'
|
||||||
|
|
||||||
|
export enum PermissionType {
|
||||||
|
route = 'Route'
|
||||||
|
}
|
||||||
|
|
||||||
export enum PrincipalType {
|
export enum PrincipalType {
|
||||||
user = 'user',
|
user = 'user',
|
||||||
group = 'group'
|
group = 'group'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PermissionSetting {
|
export enum PermissionSettingForRoute {
|
||||||
grant = 'Grant',
|
grant = 'Grant',
|
||||||
deny = 'Deny'
|
deny = 'Deny'
|
||||||
}
|
}
|
||||||
@@ -32,12 +38,17 @@ interface RegisterPermissionPayload {
|
|||||||
* Name of affected resource
|
* Name of affected resource
|
||||||
* @example "/SASjsApi/code/execute"
|
* @example "/SASjsApi/code/execute"
|
||||||
*/
|
*/
|
||||||
uri: string
|
path: string
|
||||||
|
/**
|
||||||
|
* Type of affected resource
|
||||||
|
* @example "Route"
|
||||||
|
*/
|
||||||
|
type: PermissionType
|
||||||
/**
|
/**
|
||||||
* The indication of whether (and to what extent) access is provided
|
* The indication of whether (and to what extent) access is provided
|
||||||
* @example "Grant"
|
* @example "Grant"
|
||||||
*/
|
*/
|
||||||
setting: PermissionSetting
|
setting: PermissionSettingForRoute
|
||||||
/**
|
/**
|
||||||
* Indicates the type of principal
|
* Indicates the type of principal
|
||||||
* @example "user"
|
* @example "user"
|
||||||
@@ -55,12 +66,13 @@ interface UpdatePermissionPayload {
|
|||||||
* The indication of whether (and to what extent) access is provided
|
* The indication of whether (and to what extent) access is provided
|
||||||
* @example "Grant"
|
* @example "Grant"
|
||||||
*/
|
*/
|
||||||
setting: PermissionSetting
|
setting: PermissionSettingForRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PermissionDetailsResponse {
|
export interface PermissionDetailsResponse {
|
||||||
permissionId: number
|
permissionId: number
|
||||||
uri: string
|
path: string
|
||||||
|
type: string
|
||||||
setting: string
|
setting: string
|
||||||
user?: UserResponse
|
user?: UserResponse
|
||||||
group?: GroupDetailsResponse
|
group?: GroupDetailsResponse
|
||||||
@@ -71,13 +83,17 @@ export interface PermissionDetailsResponse {
|
|||||||
@Tags('Permission')
|
@Tags('Permission')
|
||||||
export class PermissionController {
|
export class PermissionController {
|
||||||
/**
|
/**
|
||||||
* @summary Get list of all permissions (uri, setting and userDetail).
|
* Get the list of permission rules applicable the authenticated user.
|
||||||
|
* If the user is an admin, all rules are returned.
|
||||||
|
*
|
||||||
|
* @summary Get the list of permission rules. If the user is admin, all rules are returned.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<PermissionDetailsResponse[]>([
|
@Example<PermissionDetailsResponse[]>([
|
||||||
{
|
{
|
||||||
permissionId: 123,
|
permissionId: 123,
|
||||||
uri: '/SASjsApi/code/execute',
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
setting: 'Grant',
|
setting: 'Grant',
|
||||||
user: {
|
user: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -88,7 +104,8 @@ export class PermissionController {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
permissionId: 124,
|
permissionId: 124,
|
||||||
uri: '/SASjsApi/code/execute',
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
setting: 'Grant',
|
setting: 'Grant',
|
||||||
group: {
|
group: {
|
||||||
groupId: 1,
|
groupId: 1,
|
||||||
@@ -100,8 +117,10 @@ export class PermissionController {
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
@Get('/')
|
@Get('/')
|
||||||
public async getAllPermissions(): Promise<PermissionDetailsResponse[]> {
|
public async getAllPermissions(
|
||||||
return getAllPermissions()
|
@Request() request: express.Request
|
||||||
|
): Promise<PermissionDetailsResponse[]> {
|
||||||
|
return getAllPermissions(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,7 +129,8 @@ export class PermissionController {
|
|||||||
*/
|
*/
|
||||||
@Example<PermissionDetailsResponse>({
|
@Example<PermissionDetailsResponse>({
|
||||||
permissionId: 123,
|
permissionId: 123,
|
||||||
uri: '/SASjsApi/code/execute',
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
setting: 'Grant',
|
setting: 'Grant',
|
||||||
user: {
|
user: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -133,7 +153,8 @@ export class PermissionController {
|
|||||||
*/
|
*/
|
||||||
@Example<PermissionDetailsResponse>({
|
@Example<PermissionDetailsResponse>({
|
||||||
permissionId: 123,
|
permissionId: 123,
|
||||||
uri: '/SASjsApi/code/execute',
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
setting: 'Grant',
|
setting: 'Grant',
|
||||||
user: {
|
user: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -161,33 +182,43 @@ export class PermissionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllPermissions = async (): Promise<PermissionDetailsResponse[]> =>
|
const getAllPermissions = async (
|
||||||
(await Permission.find({})
|
req: express.Request
|
||||||
.select({
|
): Promise<PermissionDetailsResponse[]> => {
|
||||||
_id: 0,
|
const { user } = req
|
||||||
permissionId: 1,
|
|
||||||
uri: 1,
|
if (user?.isAdmin) return await Permission.get({})
|
||||||
setting: 1
|
else {
|
||||||
})
|
const permissions: PermissionDetailsResponse[] = []
|
||||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
|
||||||
.populate({
|
const dbUser = await User.findOne({ id: user?.userId })
|
||||||
path: 'group',
|
if (!dbUser)
|
||||||
select: 'groupId name description -_id',
|
throw {
|
||||||
populate: {
|
code: 404,
|
||||||
path: 'users',
|
status: 'Not Found',
|
||||||
select: 'id username displayName isAdmin -_id',
|
message: 'User not found.'
|
||||||
options: { limit: 15 }
|
|
||||||
}
|
}
|
||||||
})) as unknown as PermissionDetailsResponse[]
|
|
||||||
|
permissions.push(...(await Permission.get({ user: dbUser._id })))
|
||||||
|
|
||||||
|
for (const group of dbUser.groups) {
|
||||||
|
permissions.push(...(await Permission.get({ group })))
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createPermission = async ({
|
const createPermission = async ({
|
||||||
uri,
|
path,
|
||||||
|
type,
|
||||||
setting,
|
setting,
|
||||||
principalType,
|
principalType,
|
||||||
principalId
|
principalId
|
||||||
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
|
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
|
||||||
const permission = new Permission({
|
const permission = new Permission({
|
||||||
uri,
|
path,
|
||||||
|
type,
|
||||||
setting
|
setting
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -212,7 +243,8 @@ const createPermission = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alreadyExists = await Permission.findOne({
|
const alreadyExists = await Permission.findOne({
|
||||||
uri,
|
path,
|
||||||
|
type,
|
||||||
user: userInDB._id
|
user: userInDB._id
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -220,7 +252,8 @@ const createPermission = async ({
|
|||||||
throw {
|
throw {
|
||||||
code: 409,
|
code: 409,
|
||||||
status: 'Conflict',
|
status: 'Conflict',
|
||||||
message: 'Permission already exists with provided URI and User.'
|
message:
|
||||||
|
'Permission already exists with provided Path, Type and User.'
|
||||||
}
|
}
|
||||||
|
|
||||||
permission.user = userInDB._id
|
permission.user = userInDB._id
|
||||||
@@ -243,14 +276,16 @@ const createPermission = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alreadyExists = await Permission.findOne({
|
const alreadyExists = await Permission.findOne({
|
||||||
uri,
|
path,
|
||||||
|
type,
|
||||||
group: groupInDB._id
|
group: groupInDB._id
|
||||||
})
|
})
|
||||||
if (alreadyExists)
|
if (alreadyExists)
|
||||||
throw {
|
throw {
|
||||||
code: 409,
|
code: 409,
|
||||||
status: 'Conflict',
|
status: 'Conflict',
|
||||||
message: 'Permission already exists with provided URI and Group.'
|
message:
|
||||||
|
'Permission already exists with provided Path, Type and Group.'
|
||||||
}
|
}
|
||||||
|
|
||||||
permission.group = groupInDB._id
|
permission.group = groupInDB._id
|
||||||
@@ -280,7 +315,8 @@ const createPermission = async ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
permissionId: savedPermission.permissionId,
|
permissionId: savedPermission.permissionId,
|
||||||
uri: savedPermission.uri,
|
path: savedPermission.path,
|
||||||
|
type: savedPermission.type,
|
||||||
setting: savedPermission.setting,
|
setting: savedPermission.setting,
|
||||||
user,
|
user,
|
||||||
group
|
group
|
||||||
@@ -301,7 +337,8 @@ const updatePermission = async (
|
|||||||
.select({
|
.select({
|
||||||
_id: 0,
|
_id: 0,
|
||||||
permissionId: 1,
|
permissionId: 1,
|
||||||
uri: 1,
|
path: 1,
|
||||||
|
type: 1,
|
||||||
setting: 1
|
setting: 1
|
||||||
})
|
})
|
||||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import express from 'express'
|
|||||||
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
||||||
import { UserResponse } from './user'
|
import { UserResponse } from './user'
|
||||||
|
|
||||||
|
interface SessionResponse extends UserResponse {
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/session')
|
@Route('SASjsApi/session')
|
||||||
@Tags('Session')
|
@Tags('Session')
|
||||||
@@ -19,7 +23,7 @@ export class SessionController {
|
|||||||
@Get('/')
|
@Get('/')
|
||||||
public async session(
|
public async session(
|
||||||
@Request() request: express.Request
|
@Request() request: express.Request
|
||||||
): Promise<UserResponse> {
|
): Promise<SessionResponse> {
|
||||||
return session(request)
|
return session(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,5 +32,6 @@ const session = (req: express.Request) => ({
|
|||||||
id: req.user!.userId,
|
id: req.user!.userId,
|
||||||
username: req.user!.username,
|
username: req.user!.username,
|
||||||
displayName: req.user!.displayName,
|
displayName: req.user!.displayName,
|
||||||
isAdmin: req.user!.isAdmin
|
isAdmin: req.user!.isAdmin,
|
||||||
|
needsToUpdatePassword: req.user!.needsToUpdatePassword
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,33 +1,16 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import {
|
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
|
||||||
Request,
|
import { ExecutionController, ExecutionVars } from './internal'
|
||||||
Security,
|
|
||||||
Route,
|
|
||||||
Tags,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
Get,
|
|
||||||
Query,
|
|
||||||
Example
|
|
||||||
} from 'tsoa'
|
|
||||||
import {
|
|
||||||
ExecuteReturnJson,
|
|
||||||
ExecuteReturnRaw,
|
|
||||||
ExecutionController,
|
|
||||||
ExecutionVars
|
|
||||||
} from './internal'
|
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getPreProgramVariables,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn,
|
|
||||||
LogLine,
|
LogLine,
|
||||||
makeFilesNamesMap,
|
makeFilesNamesMap,
|
||||||
parseLogToArray,
|
|
||||||
getRunTimeAndFilePath
|
getRunTimeAndFilePath
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { MulterFile } from '../types/Upload'
|
import { MulterFile } from '../types/Upload'
|
||||||
|
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecutePostRequestPayload {
|
||||||
/**
|
/**
|
||||||
* Location of SAS program
|
* Location of SAS program
|
||||||
* @example "/Public/somefolder/some.file"
|
* @example "/Public/somefolder/some.file"
|
||||||
@@ -35,102 +18,80 @@ interface ExecuteReturnJsonPayload {
|
|||||||
_program?: string
|
_program?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRecordOfAny {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
export interface ExecuteReturnJsonResponse {
|
|
||||||
status: string
|
|
||||||
_webout: string | IRecordOfAny
|
|
||||||
log: LogLine[]
|
|
||||||
message?: string
|
|
||||||
httpHeaders: HTTPHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/stp')
|
@Route('SASjsApi/stp')
|
||||||
@Tags('STP')
|
@Tags('STP')
|
||||||
export class STPController {
|
export class STPController {
|
||||||
/**
|
/**
|
||||||
* Trigger a SAS or JS program using the _program URL parameter.
|
* Trigger a Stored Program using the _program URL parameter.
|
||||||
*
|
*
|
||||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||||
*
|
*
|
||||||
* https://server.sasjs.io/storedprograms
|
* https://server.sasjs.io/storedprograms
|
||||||
*
|
*
|
||||||
* @summary Execute a Stored Program, returns raw _webout content.
|
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
||||||
* @param _program Location of SAS or JS code
|
* @param _program Location of code in SASjs Drive
|
||||||
* @example _program "/Projects/myApp/some/program"
|
* @example _program "/Projects/myApp/some/program"
|
||||||
*/
|
*/
|
||||||
@Get('/execute')
|
@Get('/execute')
|
||||||
public async executeReturnRaw(
|
public async executeGetRequest(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Query() _program: string
|
@Query() _program: string
|
||||||
): Promise<string | Buffer> {
|
): Promise<string | Buffer> {
|
||||||
return executeReturnRaw(request, _program)
|
const vars = request.query as ExecutionVars
|
||||||
|
return execute(request, _program, vars)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a SAS or JS program using the _program URL parameter.
|
* Trigger a Stored Program using the _program URL parameter.
|
||||||
*
|
*
|
||||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||||
*
|
*
|
||||||
* https://server.sasjs.io/storedprograms
|
* https://server.sasjs.io/storedprograms
|
||||||
*
|
*
|
||||||
* The response will be a JSON object with the following root attributes:
|
|
||||||
* log, webout, headers.
|
|
||||||
*
|
*
|
||||||
* The webout attribute will be nested JSON ONLY if the response-header
|
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
||||||
* contains a content-type of application/json AND it is valid JSON.
|
* @param _program Location of code in SASjs Drive
|
||||||
* Otherwise it will be a stringified version of the webout content.
|
|
||||||
*
|
|
||||||
* @summary Execute a Stored Program, return a JSON object
|
|
||||||
* @param _program Location of SAS or JS code
|
|
||||||
* @example _program "/Projects/myApp/some/program"
|
* @example _program "/Projects/myApp/some/program"
|
||||||
*/
|
*/
|
||||||
@Example<ExecuteReturnJsonResponse>({
|
|
||||||
status: 'success',
|
|
||||||
_webout: 'webout content',
|
|
||||||
log: [],
|
|
||||||
httpHeaders: {
|
|
||||||
'Content-type': 'application/zip',
|
|
||||||
'Cache-Control': 'public, max-age=1000'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@Post('/execute')
|
@Post('/execute')
|
||||||
public async executeReturnJson(
|
public async executePostRequest(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body?: ExecuteReturnJsonPayload,
|
@Body() body?: ExecutePostRequestPayload,
|
||||||
@Query() _program?: string
|
@Query() _program?: string
|
||||||
): Promise<ExecuteReturnJsonResponse> {
|
): Promise<string | Buffer> {
|
||||||
const program = _program ?? body?._program
|
const program = _program ?? body?._program
|
||||||
return executeReturnJson(request, program!)
|
const vars = { ...request.query, ...request.body }
|
||||||
|
const filesNamesMap = request.files?.length
|
||||||
|
? makeFilesNamesMap(request.files as MulterFile[])
|
||||||
|
: null
|
||||||
|
const otherArgs = { filesNamesMap: filesNamesMap }
|
||||||
|
|
||||||
|
return execute(request, program!, vars, otherArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnRaw = async (
|
const execute = async (
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
_program: string
|
_program: string,
|
||||||
|
vars: ExecutionVars,
|
||||||
|
otherArgs?: any
|
||||||
): Promise<string | Buffer> => {
|
): Promise<string | Buffer> => {
|
||||||
const query = req.query as ExecutionVars
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
||||||
|
|
||||||
const { result, httpHeaders } =
|
const { result, httpHeaders } = await new ExecutionController().executeFile(
|
||||||
(await new ExecutionController().executeFile({
|
{
|
||||||
programPath: codePath,
|
programPath: codePath,
|
||||||
|
runTime,
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
vars: query,
|
vars,
|
||||||
runTime
|
otherArgs,
|
||||||
})) as ExecuteReturnRaw
|
session: req.sasjsSession
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Should over-ride response header for debug
|
req.res?.header(httpHeaders)
|
||||||
// on GET request to see entire log rendering on browser.
|
|
||||||
if (isDebugOn(query)) {
|
|
||||||
httpHeaders['content-type'] = 'text/plain'
|
|
||||||
}
|
|
||||||
|
|
||||||
req.res?.set(httpHeaders)
|
|
||||||
|
|
||||||
if (result instanceof Buffer) {
|
if (result instanceof Buffer) {
|
||||||
;(req as any).sasHeaders = httpHeaders
|
;(req as any).sasHeaders = httpHeaders
|
||||||
@@ -146,48 +107,3 @@ const executeReturnRaw = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnJson = async (
|
|
||||||
req: express.Request,
|
|
||||||
_program: string
|
|
||||||
): Promise<ExecuteReturnJsonResponse> => {
|
|
||||||
const filesNamesMap = req.files?.length
|
|
||||||
? makeFilesNamesMap(req.files as MulterFile[])
|
|
||||||
: null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
|
||||||
|
|
||||||
const { webout, log, httpHeaders } =
|
|
||||||
(await new ExecutionController().executeFile({
|
|
||||||
programPath: codePath,
|
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
|
||||||
vars: { ...req.query, ...req.body },
|
|
||||||
otherArgs: { filesNamesMap: filesNamesMap },
|
|
||||||
returnJson: true,
|
|
||||||
session: req.sasjsSession,
|
|
||||||
runTime
|
|
||||||
})) as ExecuteReturnJson
|
|
||||||
|
|
||||||
let weboutRes: string | IRecordOfAny = webout
|
|
||||||
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
|
|
||||||
try {
|
|
||||||
weboutRes = JSON.parse(webout as string)
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
_webout: weboutRes,
|
|
||||||
log: parseLogToArray(log),
|
|
||||||
httpHeaders
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'Job execution failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ import {
|
|||||||
import { desktopUser } from '../middlewares'
|
import { desktopUser } from '../middlewares'
|
||||||
|
|
||||||
import User, { UserPayload } from '../model/User'
|
import User, { UserPayload } from '../model/User'
|
||||||
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
|
import {
|
||||||
import { GroupResponse } from './group'
|
getUserAutoExec,
|
||||||
|
updateUserAutoExec,
|
||||||
|
ModeType,
|
||||||
|
ALL_USERS_GROUP
|
||||||
|
} from '../utils'
|
||||||
|
import { GroupController, GroupResponse } from './group'
|
||||||
|
|
||||||
export interface UserResponse {
|
export interface UserResponse {
|
||||||
id: number
|
id: number
|
||||||
@@ -211,7 +216,11 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
|||||||
|
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
const usernameExist = await User.findOne({ username })
|
const usernameExist = await User.findOne({ username })
|
||||||
if (usernameExist) throw new Error('Username already exists.')
|
if (usernameExist)
|
||||||
|
throw {
|
||||||
|
code: 409,
|
||||||
|
message: 'Username already exists.'
|
||||||
|
}
|
||||||
|
|
||||||
// Hash passwords
|
// Hash passwords
|
||||||
const hashPassword = User.hashPassword(password)
|
const hashPassword = User.hashPassword(password)
|
||||||
@@ -228,6 +237,15 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
|||||||
|
|
||||||
const savedUser = await user.save()
|
const savedUser = await user.save()
|
||||||
|
|
||||||
|
const groupController = new GroupController()
|
||||||
|
const allUsersGroup = await groupController
|
||||||
|
.getGroupByGroupName(ALL_USERS_GROUP.name)
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
if (allUsersGroup) {
|
||||||
|
await groupController.addUserToGroup(allUsersGroup.groupId, savedUser.id)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: savedUser.id,
|
id: savedUser.id,
|
||||||
displayName: savedUser.displayName,
|
displayName: savedUser.displayName,
|
||||||
@@ -255,7 +273,11 @@ const getUser = async (
|
|||||||
'groupId name description -_id'
|
'groupId name description -_id'
|
||||||
)) as unknown as UserDetailsResponse
|
)) as unknown as UserDetailsResponse
|
||||||
|
|
||||||
if (!user) throw new Error('User is not found.')
|
if (!user)
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
message: 'User is not found.'
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -284,6 +306,24 @@ const updateUser = async (
|
|||||||
|
|
||||||
const params: any = { displayName, isAdmin, isActive, autoExec }
|
const params: any = { displayName, isAdmin, isActive, autoExec }
|
||||||
|
|
||||||
|
const user = await User.findOne(findBy)
|
||||||
|
|
||||||
|
if (username && username !== user?.username && user?.authProvider) {
|
||||||
|
throw {
|
||||||
|
code: 405,
|
||||||
|
message:
|
||||||
|
'Can not update username of user that is created by an external auth provider.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayName && displayName !== user?.displayName && user?.authProvider) {
|
||||||
|
throw {
|
||||||
|
code: 405,
|
||||||
|
message:
|
||||||
|
'Can not update display name of user that is created by an external auth provider.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
const usernameExist = await User.findOne({ username })
|
const usernameExist = await User.findOne({ username })
|
||||||
@@ -292,7 +332,10 @@ const updateUser = async (
|
|||||||
(findBy.id && usernameExist.id != findBy.id) ||
|
(findBy.id && usernameExist.id != findBy.id) ||
|
||||||
(findBy.username && usernameExist.username != findBy.username)
|
(findBy.username && usernameExist.username != findBy.username)
|
||||||
)
|
)
|
||||||
throw new Error('Username already exists.')
|
throw {
|
||||||
|
code: 409,
|
||||||
|
message: 'Username already exists.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
params.username = username
|
params.username = username
|
||||||
}
|
}
|
||||||
@@ -305,7 +348,10 @@ const updateUser = async (
|
|||||||
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
|
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
|
||||||
|
|
||||||
if (!updatedUser)
|
if (!updatedUser)
|
||||||
throw new Error(`Unable to find user with ${findBy.id || findBy.username}`)
|
throw {
|
||||||
|
code: 404,
|
||||||
|
message: `Unable to find user with ${findBy.id || findBy.username}`
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: updatedUser.id,
|
id: updatedUser.id,
|
||||||
@@ -332,11 +378,19 @@ const deleteUser = async (
|
|||||||
{ password }: { password?: string }
|
{ password }: { password?: string }
|
||||||
) => {
|
) => {
|
||||||
const user = await User.findOne(findBy)
|
const user = await User.findOne(findBy)
|
||||||
if (!user) throw new Error('User is not found.')
|
if (!user)
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
message: 'User is not found.'
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
const validPass = user.comparePassword(password!)
|
const validPass = user.comparePassword(password!)
|
||||||
if (!validPass) throw new Error('Invalid password.')
|
if (!validPass)
|
||||||
|
throw {
|
||||||
|
code: 401,
|
||||||
|
message: 'Invalid password.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await User.deleteOne(findBy)
|
await User.deleteOne(findBy)
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
|
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
|
||||||
import { readFile } from '@sasjs/utils'
|
import { readFile, convertSecondsToHms } from '@sasjs/utils'
|
||||||
|
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import Client from '../model/Client'
|
import Client from '../model/Client'
|
||||||
import { getWebBuildFolder, generateAuthCode } from '../utils'
|
import {
|
||||||
|
getWebBuildFolder,
|
||||||
|
generateAuthCode,
|
||||||
|
RateLimiter,
|
||||||
|
AuthProviderType,
|
||||||
|
LDAPClient
|
||||||
|
} from '../utils'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
import { AuthController } from './auth'
|
import { AuthController } from './auth'
|
||||||
|
|
||||||
@@ -78,10 +84,37 @@ const login = async (
|
|||||||
) => {
|
) => {
|
||||||
// Authenticate User
|
// Authenticate User
|
||||||
const user = await User.findOne({ username })
|
const user = await User.findOne({ username })
|
||||||
if (!user) throw new Error('Username is not found.')
|
|
||||||
|
|
||||||
const validPass = user.comparePassword(password)
|
let validPass = false
|
||||||
if (!validPass) throw new Error('Invalid password.')
|
|
||||||
|
if (user) {
|
||||||
|
if (
|
||||||
|
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
|
||||||
|
user.authProvider === AuthProviderType.LDAP
|
||||||
|
) {
|
||||||
|
const ldapClient = await LDAPClient.init()
|
||||||
|
validPass = await ldapClient
|
||||||
|
.verifyUser(username, password)
|
||||||
|
.catch(() => false)
|
||||||
|
} else {
|
||||||
|
validPass = user.comparePassword(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// code to prevent brute force attack
|
||||||
|
|
||||||
|
const rateLimiter = RateLimiter.getInstance()
|
||||||
|
|
||||||
|
if (!validPass) {
|
||||||
|
const retrySecs = await rateLimiter.consume(req.ip, user?.username)
|
||||||
|
if (retrySecs > 0) throw errors.tooManyRequests(retrySecs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) throw errors.userNotFound
|
||||||
|
if (!validPass) throw errors.invalidPassword
|
||||||
|
|
||||||
|
// Reset on successful authorization
|
||||||
|
rateLimiter.resetOnSuccess(req.ip, user.username)
|
||||||
|
|
||||||
req.session.loggedIn = true
|
req.session.loggedIn = true
|
||||||
req.session.user = {
|
req.session.user = {
|
||||||
@@ -91,7 +124,8 @@ const login = async (
|
|||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isActive: user.isActive,
|
isActive: user.isActive,
|
||||||
autoExec: user.autoExec
|
autoExec: user.autoExec,
|
||||||
|
needsToUpdatePassword: user.needsToUpdatePassword
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -100,7 +134,8 @@ const login = async (
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
isAdmin: user.isAdmin
|
isAdmin: user.isAdmin,
|
||||||
|
needsToUpdatePassword: user.needsToUpdatePassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,3 +192,18 @@ interface AuthorizeResponse {
|
|||||||
*/
|
*/
|
||||||
code: string
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errors = {
|
||||||
|
invalidPassword: {
|
||||||
|
code: 401,
|
||||||
|
message: 'Invalid Password.'
|
||||||
|
},
|
||||||
|
userNotFound: {
|
||||||
|
code: 401,
|
||||||
|
message: 'Username is not found.'
|
||||||
|
},
|
||||||
|
tooManyRequests: (seconds: number) => ({
|
||||||
|
code: 429,
|
||||||
|
message: `Too Many Requests! Retry after ${convertSecondsToHms(seconds)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { csrfProtection } from '../app'
|
import { csrfProtection } from './'
|
||||||
import {
|
import {
|
||||||
fetchLatestAutoExec,
|
fetchLatestAutoExec,
|
||||||
ModeType,
|
ModeType,
|
||||||
verifyTokenInDB,
|
verifyTokenInDB,
|
||||||
isAuthorizingRoute
|
isAuthorizingRoute,
|
||||||
|
isPublicRoute,
|
||||||
|
publicUser
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { desktopUser } from './desktop'
|
import { desktopUser } from './desktop'
|
||||||
import { authorize } from './authorize'
|
import { authorize } from './authorize'
|
||||||
@@ -41,7 +43,7 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticateToken(
|
await authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
nextFunction,
|
nextFunction,
|
||||||
@@ -50,8 +52,12 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
export const authenticateRefreshToken: RequestHandler = async (
|
||||||
authenticateToken(
|
req,
|
||||||
|
res,
|
||||||
|
next
|
||||||
|
) => {
|
||||||
|
await authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next,
|
next,
|
||||||
@@ -60,7 +66,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticateToken = (
|
const authenticateToken = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
@@ -75,7 +81,8 @@ const authenticateToken = (
|
|||||||
username: 'desktopModeUsername',
|
username: 'desktopModeUsername',
|
||||||
displayName: 'desktopModeDisplayName',
|
displayName: 'desktopModeDisplayName',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
needsToUpdatePassword: false
|
||||||
}
|
}
|
||||||
req.accessToken = 'desktopModeAccessToken'
|
req.accessToken = 'desktopModeAccessToken'
|
||||||
return next()
|
return next()
|
||||||
@@ -83,12 +90,12 @@ const authenticateToken = (
|
|||||||
|
|
||||||
const authHeader = req.headers['authorization']
|
const authHeader = req.headers['authorization']
|
||||||
const token = authHeader?.split(' ')[1]
|
const token = authHeader?.split(' ')[1]
|
||||||
if (!token) return res.sendStatus(401)
|
|
||||||
|
|
||||||
jwt.verify(token, key, async (err: any, data: any) => {
|
try {
|
||||||
if (err) return res.sendStatus(401)
|
if (!token) throw 'Unauthorized'
|
||||||
|
|
||||||
|
const data: any = jwt.verify(token, key)
|
||||||
|
|
||||||
// verify this valid token's entry in DB
|
|
||||||
const user = await verifyTokenInDB(
|
const user = await verifyTokenInDB(
|
||||||
data?.userId,
|
data?.userId,
|
||||||
data?.clientId,
|
data?.clientId,
|
||||||
@@ -101,8 +108,16 @@ const authenticateToken = (
|
|||||||
req.user = user
|
req.user = user
|
||||||
if (tokenType === 'accessToken') req.accessToken = token
|
if (tokenType === 'accessToken') req.accessToken = token
|
||||||
return next()
|
return next()
|
||||||
} else return res.sendStatus(401)
|
} else throw 'Unauthorized'
|
||||||
}
|
}
|
||||||
return res.sendStatus(401)
|
|
||||||
})
|
throw 'Unauthorized'
|
||||||
|
} catch (error) {
|
||||||
|
if (await isPublicRoute(req)) {
|
||||||
|
req.user = publicUser
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(401)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,87 @@
|
|||||||
import { RequestHandler } from 'express'
|
import { RequestHandler } from 'express'
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import Permission from '../model/Permission'
|
import Permission from '../model/Permission'
|
||||||
import { PermissionSetting } from '../controllers/permission'
|
import {
|
||||||
import { getUri } from '../utils'
|
PermissionSettingForRoute,
|
||||||
|
PermissionType
|
||||||
|
} from '../controllers/permission'
|
||||||
|
import { getPath, isPublicRoute, TopLevelRoutes } from '../utils'
|
||||||
|
|
||||||
export const authorize: RequestHandler = async (req, res, next) => {
|
export const authorize: RequestHandler = async (req, res, next) => {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
|
|
||||||
if (!user) {
|
if (!user) return res.sendStatus(401)
|
||||||
return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
// no need to check for permissions when user is admin
|
// no need to check for permissions when user is admin
|
||||||
if (user.isAdmin) return next()
|
if (user.isAdmin) return next()
|
||||||
|
|
||||||
|
// no need to check for permissions when route is Public
|
||||||
|
if (await isPublicRoute(req)) return next()
|
||||||
|
|
||||||
const dbUser = await User.findOne({ id: user.userId })
|
const dbUser = await User.findOne({ id: user.userId })
|
||||||
if (!dbUser) return res.sendStatus(401)
|
if (!dbUser) return res.sendStatus(401)
|
||||||
|
|
||||||
const uri = getUri(req)
|
const path = getPath(req)
|
||||||
|
const { baseUrl } = req
|
||||||
|
const topLevelRoute =
|
||||||
|
TopLevelRoutes.find((route) => baseUrl.startsWith(route)) || baseUrl
|
||||||
|
|
||||||
// find permission w.r.t user
|
// find permission w.r.t user
|
||||||
const permission = await Permission.findOne({ uri, user: dbUser._id })
|
const permission = await Permission.findOne({
|
||||||
|
path,
|
||||||
|
type: PermissionType.route,
|
||||||
|
user: dbUser._id
|
||||||
|
})
|
||||||
|
|
||||||
if (permission) {
|
if (permission) {
|
||||||
if (permission.setting === PermissionSetting.grant) return next()
|
if (permission.setting === PermissionSettingForRoute.grant) return next()
|
||||||
else return res.sendStatus(401)
|
else return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// find permission w.r.t user on top level
|
||||||
|
const topLevelPermission = await Permission.findOne({
|
||||||
|
path: topLevelRoute,
|
||||||
|
type: PermissionType.route,
|
||||||
|
user: dbUser._id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (topLevelPermission) {
|
||||||
|
if (topLevelPermission.setting === PermissionSettingForRoute.grant)
|
||||||
|
return next()
|
||||||
|
else return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isPermissionDenied = false
|
||||||
|
|
||||||
// find permission w.r.t user's groups
|
// find permission w.r.t user's groups
|
||||||
for (const group of dbUser.groups) {
|
for (const group of dbUser.groups) {
|
||||||
const groupPermission = await Permission.findOne({ uri, group })
|
const groupPermission = await Permission.findOne({
|
||||||
if (groupPermission?.setting === PermissionSetting.grant) return next()
|
path,
|
||||||
|
type: PermissionType.route,
|
||||||
|
group
|
||||||
|
})
|
||||||
|
|
||||||
|
if (groupPermission) {
|
||||||
|
if (groupPermission.setting === PermissionSettingForRoute.grant) {
|
||||||
|
return next()
|
||||||
|
} else {
|
||||||
|
isPermissionDenied = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isPermissionDenied) {
|
||||||
|
// find permission w.r.t user's groups on top level
|
||||||
|
for (const group of dbUser.groups) {
|
||||||
|
const groupPermission = await Permission.findOne({
|
||||||
|
path: topLevelRoute,
|
||||||
|
type: PermissionType.route,
|
||||||
|
group
|
||||||
|
})
|
||||||
|
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|||||||
22
api/src/middlewares/bruteForceProtection.ts
Normal file
22
api/src/middlewares/bruteForceProtection.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { RequestHandler } from 'express'
|
||||||
|
import { convertSecondsToHms } from '@sasjs/utils'
|
||||||
|
import { RateLimiter } from '../utils'
|
||||||
|
|
||||||
|
export const bruteForceProtection: RequestHandler = async (req, res, next) => {
|
||||||
|
const ip = req.ip
|
||||||
|
const username = req.body.username
|
||||||
|
|
||||||
|
const rateLimiter = RateLimiter.getInstance()
|
||||||
|
|
||||||
|
const retrySecs = await rateLimiter.check(ip, username)
|
||||||
|
|
||||||
|
if (retrySecs > 0) {
|
||||||
|
res
|
||||||
|
.status(429)
|
||||||
|
.send(`Too Many Requests! Retry after ${convertSecondsToHms(retrySecs)}`)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
32
api/src/middlewares/csrfProtection.ts
Normal file
32
api/src/middlewares/csrfProtection.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { RequestHandler } from 'express'
|
||||||
|
import csrf from 'csrf'
|
||||||
|
|
||||||
|
const csrfTokens = new csrf()
|
||||||
|
const secret = csrfTokens.secretSync()
|
||||||
|
|
||||||
|
export const generateCSRFToken = () => csrfTokens.create(secret)
|
||||||
|
|
||||||
|
export const csrfProtection: RequestHandler = (req, res, next) => {
|
||||||
|
if (req.method === 'GET') return next()
|
||||||
|
|
||||||
|
// Reads the token from the following locations, in order:
|
||||||
|
// req.body.csrf_token - typically generated by the body-parser module.
|
||||||
|
// req.query.csrf_token - a built-in from Express.js to read from the URL query string.
|
||||||
|
// req.headers['csrf-token'] - the CSRF-Token HTTP request header.
|
||||||
|
// req.headers['xsrf-token'] - the XSRF-Token HTTP request header.
|
||||||
|
// req.headers['x-csrf-token'] - the X-CSRF-Token HTTP request header.
|
||||||
|
// req.headers['x-xsrf-token'] - the X-XSRF-Token HTTP request header.
|
||||||
|
|
||||||
|
const token =
|
||||||
|
req.body?.csrf_token ||
|
||||||
|
req.query?.csrf_token ||
|
||||||
|
req.headers['csrf-token'] ||
|
||||||
|
req.headers['xsrf-token'] ||
|
||||||
|
req.headers['x-csrf-token'] ||
|
||||||
|
req.headers['x-xsrf-token']
|
||||||
|
|
||||||
|
if (!csrfTokens.verify(secret, token)) {
|
||||||
|
return res.status(400).send('Invalid CSRF token!')
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
}
|
||||||
@@ -33,5 +33,6 @@ export const desktopUser: RequestUser = {
|
|||||||
username: userInfo().username,
|
username: userInfo().username,
|
||||||
displayName: userInfo().username,
|
displayName: userInfo().username,
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
needsToUpdatePassword: false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export * from './authenticateToken'
|
export * from './authenticateToken'
|
||||||
|
export * from './authorize'
|
||||||
|
export * from './csrfProtection'
|
||||||
export * from './desktop'
|
export * from './desktop'
|
||||||
export * from './verifyAdmin'
|
export * from './verifyAdmin'
|
||||||
export * from './verifyAdminIfNeeded'
|
export * from './verifyAdminIfNeeded'
|
||||||
export * from './authorize'
|
export * from './bruteForceProtection'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import mongoose, { Schema } from 'mongoose'
|
import mongoose, { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
export const NUMBER_OF_SECONDS_IN_A_DAY = 86400
|
||||||
export interface ClientPayload {
|
export interface ClientPayload {
|
||||||
/**
|
/**
|
||||||
* Client ID
|
* Client ID
|
||||||
@@ -11,6 +12,16 @@ export interface ClientPayload {
|
|||||||
* @example "someRandomCryptoString"
|
* @example "someRandomCryptoString"
|
||||||
*/
|
*/
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
|
/**
|
||||||
|
* Number of seconds after which access token will expire. Default is 86400 (1 day)
|
||||||
|
* @example 86400
|
||||||
|
*/
|
||||||
|
accessTokenExpiration?: number
|
||||||
|
/**
|
||||||
|
* Number of seconds after which access token will expire. Default is 2592000 (30 days)
|
||||||
|
* @example 2592000
|
||||||
|
*/
|
||||||
|
refreshTokenExpiration?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClientSchema = new Schema<ClientPayload>({
|
const ClientSchema = new Schema<ClientPayload>({
|
||||||
@@ -21,6 +32,14 @@ const ClientSchema = new Schema<ClientPayload>({
|
|||||||
clientSecret: {
|
clientSecret: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
accessTokenExpiration: {
|
||||||
|
type: Number,
|
||||||
|
default: NUMBER_OF_SECONDS_IN_A_DAY
|
||||||
|
},
|
||||||
|
refreshTokenExpiration: {
|
||||||
|
type: Number,
|
||||||
|
default: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||||
import { GroupDetailsResponse } from '../controllers'
|
import { GroupDetailsResponse } from '../controllers'
|
||||||
import User, { IUser } from './User'
|
import User, { IUser } from './User'
|
||||||
|
import { AuthProviderType } from '../utils'
|
||||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||||
|
|
||||||
|
export const PUBLIC_GROUP_NAME = 'Public'
|
||||||
|
|
||||||
export interface GroupPayload {
|
export interface GroupPayload {
|
||||||
/**
|
/**
|
||||||
* Name of the group
|
* Name of the group
|
||||||
@@ -25,6 +28,7 @@ interface IGroupDocument extends GroupPayload, Document {
|
|||||||
groupId: number
|
groupId: number
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
users: Schema.Types.ObjectId[]
|
users: Schema.Types.ObjectId[]
|
||||||
|
authProvider?: AuthProviderType
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGroup extends IGroupDocument {
|
interface IGroup extends IGroupDocument {
|
||||||
@@ -44,6 +48,10 @@ const groupSchema = new Schema<IGroupDocument>({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'Group description.'
|
default: 'Group description.'
|
||||||
},
|
},
|
||||||
|
authProvider: {
|
||||||
|
type: String,
|
||||||
|
enum: AuthProviderType
|
||||||
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||||
|
import { PermissionDetailsResponse } from '../controllers'
|
||||||
|
|
||||||
|
interface GetPermissionBy {
|
||||||
|
user?: Schema.Types.ObjectId
|
||||||
|
group?: Schema.Types.ObjectId
|
||||||
|
}
|
||||||
|
|
||||||
interface IPermissionDocument extends Document {
|
interface IPermissionDocument extends Document {
|
||||||
uri: string
|
path: string
|
||||||
|
type: string
|
||||||
setting: string
|
setting: string
|
||||||
permissionId: number
|
permissionId: number
|
||||||
user: Schema.Types.ObjectId
|
user: Schema.Types.ObjectId
|
||||||
@@ -11,10 +18,16 @@ interface IPermissionDocument extends Document {
|
|||||||
|
|
||||||
interface IPermission extends IPermissionDocument {}
|
interface IPermission extends IPermissionDocument {}
|
||||||
|
|
||||||
interface IPermissionModel extends Model<IPermission> {}
|
interface IPermissionModel extends Model<IPermission> {
|
||||||
|
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
|
||||||
|
}
|
||||||
|
|
||||||
const permissionSchema = new Schema<IPermissionDocument>({
|
const permissionSchema = new Schema<IPermissionDocument>({
|
||||||
uri: {
|
path: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
@@ -28,6 +41,30 @@ const permissionSchema = new Schema<IPermissionDocument>({
|
|||||||
|
|
||||||
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
|
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
|
||||||
|
|
||||||
|
// Static Methods
|
||||||
|
permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
|
||||||
|
PermissionDetailsResponse[]
|
||||||
|
> {
|
||||||
|
return (await this.find(getBy)
|
||||||
|
.select({
|
||||||
|
_id: 0,
|
||||||
|
permissionId: 1,
|
||||||
|
path: 1,
|
||||||
|
type: 1,
|
||||||
|
setting: 1
|
||||||
|
})
|
||||||
|
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
||||||
|
.populate({
|
||||||
|
path: 'group',
|
||||||
|
select: 'groupId name description -_id',
|
||||||
|
populate: {
|
||||||
|
path: 'users',
|
||||||
|
select: 'id username displayName isAdmin -_id',
|
||||||
|
options: { limit: 15 }
|
||||||
|
}
|
||||||
|
})) as unknown as PermissionDetailsResponse[]
|
||||||
|
})
|
||||||
|
|
||||||
export const Permission: IPermissionModel = model<
|
export const Permission: IPermissionModel = model<
|
||||||
IPermission,
|
IPermission,
|
||||||
IPermissionModel
|
IPermissionModel
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
|
import { AuthProviderType } from '../utils'
|
||||||
|
|
||||||
export interface UserPayload {
|
export interface UserPayload {
|
||||||
/**
|
/**
|
||||||
@@ -39,9 +40,11 @@ interface IUserDocument extends UserPayload, Document {
|
|||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
autoExec: string
|
autoExec: string
|
||||||
groups: Schema.Types.ObjectId[]
|
groups: Schema.Types.ObjectId[]
|
||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
|
authProvider?: AuthProviderType
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUser extends IUserDocument {
|
export interface IUser extends IUserDocument {
|
||||||
@@ -67,6 +70,10 @@ const userSchema = new Schema<IUserDocument>({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
authProvider: {
|
||||||
|
type: String,
|
||||||
|
enum: AuthProviderType
|
||||||
|
},
|
||||||
isAdmin: {
|
isAdmin: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@@ -75,6 +82,10 @@ const userSchema = new Schema<IUserDocument>({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
needsToUpdatePassword: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
autoExec: {
|
autoExec: {
|
||||||
type: String
|
type: String
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,12 +7,28 @@ import {
|
|||||||
authenticateRefreshToken
|
authenticateRefreshToken
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
import { authorizeValidation, tokenValidation } from '../../utils'
|
import { tokenValidation, updatePasswordValidation } from '../../utils'
|
||||||
import { InfoJWT } from '../../types'
|
import { InfoJWT } from '../../types'
|
||||||
|
|
||||||
const authRouter = express.Router()
|
const authRouter = express.Router()
|
||||||
const controller = new AuthController()
|
const controller = new AuthController()
|
||||||
|
|
||||||
|
authRouter.patch(
|
||||||
|
'/updatePassword',
|
||||||
|
authenticateAccessToken,
|
||||||
|
async (req, res) => {
|
||||||
|
const { error, value: body } = updatePasswordValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await controller.updatePassword(req, body)
|
||||||
|
res.sendStatus(204)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(err.code).send(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
authRouter.post('/token', async (req, res) => {
|
authRouter.post('/token', async (req, res) => {
|
||||||
const { error, value: body } = tokenValidation(req.body)
|
const { error, value: body } = tokenValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|||||||
25
api/src/routes/api/authConfig.ts
Normal file
25
api/src/routes/api/authConfig.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { AuthConfigController } from '../../controllers'
|
||||||
|
const authConfigRouter = express.Router()
|
||||||
|
|
||||||
|
authConfigRouter.get('/', async (req, res) => {
|
||||||
|
const controller = new AuthConfigController()
|
||||||
|
try {
|
||||||
|
const response = controller.getDetail()
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
authConfigRouter.post('/synchroniseWithLDAP', async (req, res) => {
|
||||||
|
const controller = new AuthConfigController()
|
||||||
|
try {
|
||||||
|
const response = await controller.synchroniseWithLDAP()
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default authConfigRouter
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { ClientController } from '../../controllers'
|
import { ClientController } from '../../controllers'
|
||||||
import { registerClientValidation } from '../../utils'
|
import { registerClientValidation } from '../../utils'
|
||||||
|
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
|
||||||
|
|
||||||
const clientRouter = express.Router()
|
const clientRouter = express.Router()
|
||||||
|
|
||||||
@@ -17,4 +18,19 @@ clientRouter.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
clientRouter.get(
|
||||||
|
'/',
|
||||||
|
authenticateAccessToken,
|
||||||
|
verifyAdmin,
|
||||||
|
async (req, res) => {
|
||||||
|
const controller = new ClientController()
|
||||||
|
try {
|
||||||
|
const response = await controller.getAllClients()
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export default clientRouter
|
export default clientRouter
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ groupRouter.post(
|
|||||||
const response = await controller.createGroup(body)
|
const response = await controller.createGroup(body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(err.code).send(err.message)
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -33,11 +29,7 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
|
|||||||
const response = await controller.getAllGroups()
|
const response = await controller.getAllGroups()
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(err.code).send(err.message)
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -49,11 +41,7 @@ groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
|
|||||||
const response = await controller.getGroup(parseInt(groupId))
|
const response = await controller.getGroup(parseInt(groupId))
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(err.code).send(err.message)
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -71,11 +59,7 @@ groupRouter.get(
|
|||||||
const response = await controller.getGroupByGroupName(name)
|
const response = await controller.getGroupByGroupName(name)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(err.code).send(err.message)
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -95,11 +79,7 @@ groupRouter.post(
|
|||||||
)
|
)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(err.code).send(err.message)
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -119,11 +99,7 @@ groupRouter.delete(
|
|||||||
)
|
)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(err.code).send(err.message)
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -140,11 +116,7 @@ groupRouter.delete(
|
|||||||
await controller.deleteGroup(parseInt(groupId))
|
await controller.deleteGroup(parseInt(groupId))
|
||||||
res.status(200).send('Group Deleted!')
|
res.status(200).send('Group Deleted!')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(err.code).send(err.message)
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import clientRouter from './client'
|
|||||||
import authRouter from './auth'
|
import authRouter from './auth'
|
||||||
import sessionRouter from './session'
|
import sessionRouter from './session'
|
||||||
import permissionRouter from './permission'
|
import permissionRouter from './permission'
|
||||||
|
import authConfigRouter from './authConfig'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
@@ -43,6 +44,14 @@ router.use(
|
|||||||
permissionRouter
|
permissionRouter
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.use(
|
||||||
|
'/authConfig',
|
||||||
|
desktopRestrict,
|
||||||
|
authenticateAccessToken,
|
||||||
|
verifyAdmin,
|
||||||
|
authConfigRouter
|
||||||
|
)
|
||||||
|
|
||||||
router.use(
|
router.use(
|
||||||
'/',
|
'/',
|
||||||
swaggerUi.serve,
|
swaggerUi.serve,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const controller = new PermissionController()
|
|||||||
|
|
||||||
permissionRouter.get('/', async (req, res) => {
|
permissionRouter.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await controller.getAllPermissions()
|
const response = await controller.getAllPermissions(req)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
const statusCode = err.code
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import request from 'supertest'
|
|||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController, ClientController } from '../../../controllers/'
|
import { UserController, ClientController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../../../model/Client'
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
clientId: 'someclientID',
|
clientId: 'someclientID',
|
||||||
@@ -26,6 +27,7 @@ describe('client', () => {
|
|||||||
let app: Express
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
|
let adminAccessToken: string
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const clientController = new ClientController()
|
const clientController = new ClientController()
|
||||||
|
|
||||||
@@ -34,6 +36,18 @@ describe('client', () => {
|
|||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
|
const dbUser = await userController.createUser(adminUser)
|
||||||
|
adminAccessToken = generateAccessToken({
|
||||||
|
clientId: client.clientId,
|
||||||
|
userId: dbUser.id
|
||||||
|
})
|
||||||
|
await saveTokensInDB(
|
||||||
|
dbUser.id,
|
||||||
|
client.clientId,
|
||||||
|
adminAccessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -43,22 +57,6 @@ describe('client', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
let adminAccessToken: string
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const dbUser = await userController.createUser(adminUser)
|
|
||||||
adminAccessToken = generateAccessToken({
|
|
||||||
clientId: client.clientId,
|
|
||||||
userId: dbUser.id
|
|
||||||
})
|
|
||||||
await saveTokensInDB(
|
|
||||||
dbUser.id,
|
|
||||||
client.clientId,
|
|
||||||
adminAccessToken,
|
|
||||||
'refreshToken'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
const collections = mongoose.connection.collections
|
const collections = mongoose.connection.collections
|
||||||
const collection = collections['clients']
|
const collection = collections['clients']
|
||||||
@@ -157,4 +155,80 @@ describe('client', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
const collections = mongoose.connection.collections
|
||||||
|
const collection = collections['clients']
|
||||||
|
await collection.deleteMany({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with an array of all clients', async () => {
|
||||||
|
await clientController.createClient(newClient)
|
||||||
|
await clientController.createClient({
|
||||||
|
clientId: 'clientID',
|
||||||
|
clientSecret: 'clientSecret'
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/SASjsApi/client')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
clientId: 'newClientID',
|
||||||
|
clientSecret: 'newClientSecret',
|
||||||
|
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
||||||
|
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clientId: 'clientID',
|
||||||
|
clientSecret: 'clientSecret',
|
||||||
|
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
||||||
|
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(res.body).toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app).get('/SASjsApi/client').send().expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbideen if access token is not of an admin account', async () => {
|
||||||
|
const user = {
|
||||||
|
displayName: 'User 2',
|
||||||
|
username: 'username2',
|
||||||
|
password: '12345678',
|
||||||
|
isAdmin: false,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
const dbUser = await userController.createUser(user)
|
||||||
|
const accessToken = generateAccessToken({
|
||||||
|
clientId: client.clientId,
|
||||||
|
userId: dbUser.id
|
||||||
|
})
|
||||||
|
await saveTokensInDB(
|
||||||
|
dbUser.id,
|
||||||
|
client.clientId,
|
||||||
|
accessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/SASjsApi/client')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Admin account required')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ import appPromise from '../../../app'
|
|||||||
import {
|
import {
|
||||||
UserController,
|
UserController,
|
||||||
PermissionController,
|
PermissionController,
|
||||||
PermissionSetting,
|
PermissionType,
|
||||||
|
PermissionSettingForRoute,
|
||||||
PrincipalType
|
PrincipalType
|
||||||
} from '../../../controllers/'
|
} from '../../../controllers/'
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
@@ -48,6 +49,12 @@ const user = {
|
|||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const permission = {
|
||||||
|
type: PermissionType.route,
|
||||||
|
principalType: PrincipalType.user,
|
||||||
|
setting: PermissionSettingForRoute.grant
|
||||||
|
}
|
||||||
|
|
||||||
describe('drive', () => {
|
describe('drive', () => {
|
||||||
let app: Express
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
@@ -66,34 +73,29 @@ describe('drive', () => {
|
|||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
accessToken = await generateAndSaveToken(dbUser.id)
|
accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/drive/deploy',
|
...permission,
|
||||||
principalType: PrincipalType.user,
|
path: '/SASjsApi/drive/deploy',
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/drive/deploy/upload',
|
...permission,
|
||||||
principalType: PrincipalType.user,
|
path: '/SASjsApi/drive/deploy/upload',
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/drive/file',
|
...permission,
|
||||||
principalType: PrincipalType.user,
|
path: '/SASjsApi/drive/file',
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/drive/folder',
|
...permission,
|
||||||
principalType: PrincipalType.user,
|
path: '/SASjsApi/drive/folder',
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/drive/rename',
|
...permission,
|
||||||
principalType: PrincipalType.user,
|
path: '/SASjsApi/drive/rename',
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
|
|||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController, GroupController } from '../../../controllers/'
|
import { UserController, GroupController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import {
|
||||||
|
generateAccessToken,
|
||||||
|
saveTokensInDB,
|
||||||
|
AuthProviderType
|
||||||
|
} from '../../../utils'
|
||||||
|
import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group'
|
||||||
|
import User from '../../../model/User'
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
@@ -27,6 +33,12 @@ const group = {
|
|||||||
description: 'DC group for testing purposes.'
|
description: 'DC group for testing purposes.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PUBLIC_GROUP = {
|
||||||
|
name: PUBLIC_GROUP_NAME,
|
||||||
|
description:
|
||||||
|
'A special group that can be used to bypass authentication for particular routes.'
|
||||||
|
}
|
||||||
|
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const groupController = new GroupController()
|
const groupController = new GroupController()
|
||||||
|
|
||||||
@@ -535,6 +547,64 @@ describe('group', () => {
|
|||||||
expect(res.text).toEqual('User not found.')
|
expect(res.text).toEqual('User not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request when adding user to Public group', async () => {
|
||||||
|
const dbGroup = await groupController.createGroup(PUBLIC_GROUP)
|
||||||
|
const dbUser = await userController.createUser({
|
||||||
|
...user,
|
||||||
|
username: 'publicUser'
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
|
||||||
|
const dbGroup = await Group.create({
|
||||||
|
...group,
|
||||||
|
authProvider: AuthProviderType.LDAP
|
||||||
|
})
|
||||||
|
const dbUser = await userController.createUser({
|
||||||
|
...user,
|
||||||
|
username: 'ldapGroupUser'
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(405)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
`Can't add/remove user to group created by external auth provider.`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
|
||||||
|
const dbGroup = await groupController.createGroup(group)
|
||||||
|
const dbUser = await User.create({
|
||||||
|
...user,
|
||||||
|
username: 'ldapUser',
|
||||||
|
authProvider: AuthProviderType.LDAP
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(405)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
`Can't add/remove user to group created by external auth provider.`
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('RemoveUser', () => {
|
describe('RemoveUser', () => {
|
||||||
@@ -586,6 +656,46 @@ describe('group', () => {
|
|||||||
expect(res.body.groups).toEqual([])
|
expect(res.body.groups).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
|
||||||
|
const dbGroup = await Group.create({
|
||||||
|
...group,
|
||||||
|
authProvider: AuthProviderType.LDAP
|
||||||
|
})
|
||||||
|
const dbUser = await userController.createUser({
|
||||||
|
...user,
|
||||||
|
username: 'removeLdapGroupUser'
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(405)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
`Can't add/remove user to group created by external auth provider.`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
|
||||||
|
const dbGroup = await groupController.createGroup(group)
|
||||||
|
const dbUser = await User.create({
|
||||||
|
...user,
|
||||||
|
username: 'removeLdapUser',
|
||||||
|
authProvider: AuthProviderType.LDAP
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(405)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
`Can't add/remove user to group created by external auth provider.`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.delete('/SASjsApi/group/123/123')
|
.delete('/SASjsApi/group/123/123')
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import {
|
|||||||
DriveController,
|
DriveController,
|
||||||
UserController,
|
UserController,
|
||||||
GroupController,
|
GroupController,
|
||||||
ClientController,
|
|
||||||
PermissionController,
|
PermissionController,
|
||||||
PrincipalType,
|
PrincipalType,
|
||||||
PermissionSetting
|
PermissionType,
|
||||||
|
PermissionSettingForRoute
|
||||||
} from '../../../controllers/'
|
} from '../../../controllers/'
|
||||||
import {
|
import {
|
||||||
UserDetailsResponse,
|
UserDetailsResponse,
|
||||||
@@ -56,10 +56,10 @@ const user = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const permission = {
|
const permission = {
|
||||||
uri: '/SASjsApi/code/execute',
|
path: '/SASjsApi/code/execute',
|
||||||
setting: PermissionSetting.grant,
|
type: PermissionType.route,
|
||||||
principalType: PrincipalType.user,
|
setting: PermissionSettingForRoute.grant,
|
||||||
principalId: 123
|
principalType: PrincipalType.user
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = {
|
const group = {
|
||||||
@@ -69,7 +69,6 @@ const group = {
|
|||||||
|
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const groupController = new GroupController()
|
const groupController = new GroupController()
|
||||||
const clientController = new ClientController()
|
|
||||||
const permissionController = new PermissionController()
|
const permissionController = new PermissionController()
|
||||||
|
|
||||||
describe('permission', () => {
|
describe('permission', () => {
|
||||||
@@ -108,7 +107,8 @@ describe('permission', () => {
|
|||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.permissionId).toBeTruthy()
|
expect(res.body.permissionId).toBeTruthy()
|
||||||
expect(res.body.uri).toEqual(permission.uri)
|
expect(res.body.path).toEqual(permission.path)
|
||||||
|
expect(res.body.type).toEqual(permission.type)
|
||||||
expect(res.body.setting).toEqual(permission.setting)
|
expect(res.body.setting).toEqual(permission.setting)
|
||||||
expect(res.body.user).toBeTruthy()
|
expect(res.body.user).toBeTruthy()
|
||||||
})
|
})
|
||||||
@@ -127,7 +127,8 @@ describe('permission', () => {
|
|||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.permissionId).toBeTruthy()
|
expect(res.body.permissionId).toBeTruthy()
|
||||||
expect(res.body.uri).toEqual(permission.uri)
|
expect(res.body.path).toEqual(permission.path)
|
||||||
|
expect(res.body.type).toEqual(permission.type)
|
||||||
expect(res.body.setting).toEqual(permission.setting)
|
expect(res.body.setting).toEqual(permission.setting)
|
||||||
expect(res.body.group).toBeTruthy()
|
expect(res.body.group).toBeTruthy()
|
||||||
})
|
})
|
||||||
@@ -142,53 +143,74 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not of an admin account even if user has permission', async () => {
|
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
await permissionController.createPermission({
|
|
||||||
uri: '/SASjsApi/permission',
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: dbUser.id,
|
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.send()
|
.send(permission)
|
||||||
.expect(401)
|
.expect(401)
|
||||||
|
|
||||||
expect(res.text).toEqual('Admin account required')
|
expect(res.text).toEqual('Admin account required')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if uri is missing', async () => {
|
it('should respond with Bad Request if path is missing', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
...permission,
|
...permission,
|
||||||
uri: undefined
|
path: undefined
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual(`"uri" is required`)
|
expect(res.text).toEqual(`"path" is required`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if uri is not valid', async () => {
|
it('should respond with Bad Request if path is not valid', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
...permission,
|
...permission,
|
||||||
uri: '/some/random/api/endpoint'
|
path: '/some/random/api/endpoint'
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if type is not valid', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
type: 'invalid'
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('"type" must be [Route]')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if type is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
type: undefined
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"type" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is missing', async () => {
|
it('should respond with Bad Request if setting is missing', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
@@ -203,6 +225,20 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if setting is not valid', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
setting: 'invalid'
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if principalType is missing', async () => {
|
it('should respond with Bad Request if principalType is missing', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
@@ -217,20 +253,6 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if principalId is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalId: undefined
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"principalId" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if principal type is not valid', async () => {
|
it('should respond with Bad Request if principal type is not valid', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
@@ -245,17 +267,17 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is not valid', async () => {
|
it('should respond with Bad Request if principalId is missing', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
...permission,
|
...permission,
|
||||||
setting: 'invalid'
|
principalId: undefined
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
|
expect(res.text).toEqual(`"principalId" is required`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -313,7 +335,8 @@ describe('permission', () => {
|
|||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
...permission,
|
...permission,
|
||||||
principalType: 'group'
|
principalType: 'group',
|
||||||
|
principalId: 123
|
||||||
})
|
})
|
||||||
.expect(404)
|
.expect(404)
|
||||||
|
|
||||||
@@ -334,7 +357,7 @@ describe('permission', () => {
|
|||||||
.expect(409)
|
.expect(409)
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
expect(res.text).toEqual(
|
||||||
'Permission already exists with provided URI and User.'
|
'Permission already exists with provided Path, Type and User.'
|
||||||
)
|
)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
@@ -357,7 +380,7 @@ describe('permission', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({ setting: 'Deny' })
|
.send({ setting: PermissionSettingForRoute.deny })
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.setting).toEqual('Deny')
|
expect(res.body.setting).toEqual('Deny')
|
||||||
@@ -366,7 +389,7 @@ describe('permission', () => {
|
|||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
.send(permission)
|
.send()
|
||||||
.expect(401)
|
.expect(401)
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
expect(res.text).toEqual('Unauthorized')
|
||||||
@@ -400,12 +423,11 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is not valid', async () => {
|
it('should respond with Bad Request if setting is invalid', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
...permission,
|
|
||||||
setting: 'invalid'
|
setting: 'invalid'
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
@@ -414,12 +436,12 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with not found (404) if permission with provided id does not exists', async () => {
|
it('should respond with not found (404) if permission with provided id does not exist', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch('/SASjsApi/permission/123')
|
.patch('/SASjsApi/permission/123')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
setting: PermissionSetting.deny
|
setting: PermissionSettingForRoute.deny
|
||||||
})
|
})
|
||||||
.expect(404)
|
.expect(404)
|
||||||
|
|
||||||
@@ -458,12 +480,12 @@ describe('permission', () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
...permission,
|
...permission,
|
||||||
uri: '/test-1',
|
path: '/test-1',
|
||||||
principalId: dbUser.id
|
principalId: dbUser.id
|
||||||
})
|
})
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
...permission,
|
...permission,
|
||||||
uri: '/test-2',
|
path: '/test-2',
|
||||||
principalId: dbUser.id
|
principalId: dbUser.id
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -478,34 +500,37 @@ describe('permission', () => {
|
|||||||
expect(res.body).toHaveLength(2)
|
expect(res.body).toHaveLength(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should give a list of all permissions when user is not admin', async () => {
|
it(`should give a list of user's own permissions when user is not admin`, async () => {
|
||||||
const dbUser = await userController.createUser({
|
const nonAdminUser = await userController.createUser({
|
||||||
...user,
|
...user,
|
||||||
username: 'get' + user.username
|
username: 'get' + user.username
|
||||||
})
|
})
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
const accessToken = await generateAndSaveToken(nonAdminUser.id)
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/permission',
|
path: '/test-1',
|
||||||
|
type: PermissionType.route,
|
||||||
principalType: PrincipalType.user,
|
principalType: PrincipalType.user,
|
||||||
principalId: dbUser.id,
|
principalId: nonAdminUser.id,
|
||||||
setting: PermissionSetting.grant
|
setting: PermissionSettingForRoute.grant
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const permissionCount = 1
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/permission/')
|
.get('/SASjsApi/permission/')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body).toHaveLength(3)
|
expect(res.body).toHaveLength(permissionCount)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.only('verify', () => {
|
describe('verify', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
...permission,
|
...permission,
|
||||||
uri: '/SASjsApi/drive/deploy',
|
path: '/SASjsApi/drive/deploy',
|
||||||
principalId: dbUser.id
|
principalId: dbUser.id
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import appPromise from '../../../app'
|
|||||||
import {
|
import {
|
||||||
UserController,
|
UserController,
|
||||||
PermissionController,
|
PermissionController,
|
||||||
PermissionSetting,
|
PermissionType,
|
||||||
|
PermissionSettingForRoute,
|
||||||
PrincipalType
|
PrincipalType
|
||||||
} from '../../../controllers/'
|
} from '../../../controllers/'
|
||||||
import {
|
import {
|
||||||
@@ -20,8 +21,8 @@ import {
|
|||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
||||||
import {
|
import {
|
||||||
SASSessionController,
|
SessionController,
|
||||||
JSSessionController
|
SASSessionController
|
||||||
} from '../../../controllers/internal'
|
} from '../../../controllers/internal'
|
||||||
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
||||||
import { Session } from '../../../types'
|
import { Session } from '../../../types'
|
||||||
@@ -38,14 +39,17 @@ const user = {
|
|||||||
|
|
||||||
const sampleSasProgram = '%put hello world!;'
|
const sampleSasProgram = '%put hello world!;'
|
||||||
const sampleJsProgram = `console.log('hello world!/')`
|
const sampleJsProgram = `console.log('hello world!/')`
|
||||||
|
const samplePyProgram = `print('hello world!/')`
|
||||||
|
|
||||||
const filesFolder = getFilesFolder()
|
const filesFolder = getFilesFolder()
|
||||||
|
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||||
|
|
||||||
|
let app: Express
|
||||||
|
let accessToken: string
|
||||||
|
|
||||||
describe('stp', () => {
|
describe('stp', () => {
|
||||||
let app: Express
|
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
let accessToken: string
|
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const permissionController = new PermissionController()
|
const permissionController = new PermissionController()
|
||||||
|
|
||||||
@@ -56,10 +60,11 @@ describe('stp', () => {
|
|||||||
const dbUser = await userController.createUser(user)
|
const dbUser = await userController.createUser(user)
|
||||||
accessToken = await generateAndSaveToken(dbUser.id)
|
accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/stp/execute',
|
path: '/SASjsApi/stp/execute',
|
||||||
|
type: PermissionType.route,
|
||||||
principalType: PrincipalType.user,
|
principalType: PrincipalType.user,
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id,
|
||||||
setting: PermissionSetting.grant
|
setting: PermissionSettingForRoute.grant
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -70,8 +75,6 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('execute', () => {
|
describe('execute', () => {
|
||||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
|
||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
describe('with runtime js', () => {
|
describe('with runtime js', () => {
|
||||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||||
@@ -91,41 +94,45 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute js program when both js and sas program are present', async () => {
|
it('should execute js program when both js and sas program are present', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert(
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
[RunTimeType.JS, RunTimeType.SAS],
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
200,
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
RunTimeType.JS
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.JS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when js program is not present but sas program exists', async () => {
|
it('should throw error when js program is not present but sas program exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
})
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
})
|
||||||
|
|
||||||
await request(app)
|
describe('with runtime py', () => {
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
beforeAll(() => {
|
||||||
.expect(400)
|
process.runTimes = [RunTimeType.PY]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when python, js and sas programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.PY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when py program is not present but js or sas program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -145,41 +152,11 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute sas program when both sas and js programs are present', async () => {
|
it('should execute sas program when both sas and js programs are present', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.SAS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when sas program do not exit but js exists', async () => {
|
it('should throw error when sas program do not exit but js exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(400)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -199,63 +176,51 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute js program when both js and sas program are present', async () => {
|
it('should execute js program when both js and sas program are present', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert(
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
[RunTimeType.SAS, RunTimeType.JS],
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
200,
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
RunTimeType.JS
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.JS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should execute sas program when js program is not present but sas program exists', async () => {
|
it('should execute sas program when js program is not present but sas program exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.SAS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when both sas and js programs do not exist', async () => {
|
it('should throw error when both sas and js programs do not exist', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await request(app)
|
describe('with runtime py and sas', () => {
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
beforeAll(() => {
|
||||||
.auth(accessToken, { type: 'bearer' })
|
process.runTimes = [RunTimeType.PY, RunTimeType.SAS]
|
||||||
.send()
|
})
|
||||||
.expect(400)
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when both python and sas program are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.PY, RunTimeType.SAS],
|
||||||
|
200,
|
||||||
|
RunTimeType.PY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when python program is not present but sas program exists', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when both sas and js programs do not exist', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -275,76 +240,220 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute sas program when both sas and js programs exist', async () => {
|
it('should execute sas program when both sas and js programs exist', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert(
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
[RunTimeType.SAS, RunTimeType.JS],
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
200,
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
RunTimeType.SAS
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.SAS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should execute js program when sas program is not present but js program exists', async () => {
|
it('should execute js program when sas program is not present but js program exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.JS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when both sas and js programs do not exist', async () => {
|
it('should throw error when both sas and js programs do not exist', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await request(app)
|
describe('with runtime sas and py', () => {
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
beforeAll(() => {
|
||||||
.auth(accessToken, { type: 'bearer' })
|
process.runTimes = [RunTimeType.SAS, RunTimeType.PY]
|
||||||
.send()
|
})
|
||||||
.expect(400)
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when both sas and python programs exist', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when sas program is not present but python program exists', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when both sas and python programs do not exist', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with runtime sas, js and py', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
process.runTimes = [RunTimeType.SAS, RunTimeType.JS, RunTimeType.PY]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when it exists, no matter js and python programs exist or not', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.PY, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute js program when sas program is absent but js and python programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.JS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.JS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when both sas and js programs are not present', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when no program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with runtime js, sas and py', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
process.runTimes = [RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute js program when it exists, no matter sas and python programs exist or not', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.JS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when js program is absent but sas and python programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when both sas and js programs are not present', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when no program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with runtime py, sas and js', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
process.runTimes = [RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when it exists, no matter sas and js programs exist or not', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.PY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when python program is absent but sas and js programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute js program when both sas and python programs are not present', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when no program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const generateSaveTokenAndCreateUser = async (
|
const makeRequestAndAssert = async (
|
||||||
someUser: any
|
programTypes: RunTimeType[],
|
||||||
): Promise<string> => {
|
expectedStatusCode: number,
|
||||||
const userController = new UserController()
|
expectedRuntime?: RunTimeType
|
||||||
const dbUser = await userController.createUser(someUser)
|
) => {
|
||||||
|
const programPath = path.join(testFilesFolder, 'program')
|
||||||
|
for (const programType of programTypes) {
|
||||||
|
if (programType === RunTimeType.JS)
|
||||||
|
await createFile(
|
||||||
|
path.join(filesFolder, `${programPath}.js`),
|
||||||
|
sampleJsProgram
|
||||||
|
)
|
||||||
|
else if (programType === RunTimeType.PY)
|
||||||
|
await createFile(
|
||||||
|
path.join(filesFolder, `${programPath}.py`),
|
||||||
|
samplePyProgram
|
||||||
|
)
|
||||||
|
else if (programType === RunTimeType.SAS)
|
||||||
|
await createFile(
|
||||||
|
path.join(filesFolder, `${programPath}.sas`),
|
||||||
|
sampleSasProgram
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return generateAndSaveToken(dbUser.id)
|
await request(app)
|
||||||
|
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(expectedStatusCode)
|
||||||
|
|
||||||
|
if (expectedRuntime)
|
||||||
|
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expectedRuntime,
|
||||||
|
expect.anything(),
|
||||||
|
undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateAndSaveToken = async (userId: number) => {
|
const generateAndSaveToken = async (userId: number) => {
|
||||||
@@ -362,7 +471,7 @@ const setupMocks = async () => {
|
|||||||
.mockImplementation(mockedGetSession)
|
.mockImplementation(mockedGetSession)
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(JSSessionController.prototype, 'getSession')
|
.spyOn(SASSessionController.prototype, 'getSession')
|
||||||
.mockImplementation(mockedGetSession)
|
.mockImplementation(mockedGetSession)
|
||||||
|
|
||||||
jest
|
jest
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
|
|||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController, GroupController } from '../../../controllers/'
|
import { UserController, GroupController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import {
|
||||||
|
generateAccessToken,
|
||||||
|
saveTokensInDB,
|
||||||
|
AuthProviderType
|
||||||
|
} from '../../../utils'
|
||||||
|
import User from '../../../model/User'
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
@@ -110,16 +115,16 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if username is already present', async () => {
|
it('should respond with Conflict if username is already present', async () => {
|
||||||
await controller.createUser(user)
|
await controller.createUser(user)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/user')
|
.post('/SASjsApi/user')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send(user)
|
.send(user)
|
||||||
.expect(403)
|
.expect(409)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Username already exists.')
|
expect(res.text).toEqual('Username already exists.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -226,6 +231,36 @@ describe('user', () => {
|
|||||||
.expect(400)
|
.expect(400)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should respond with Method Not Allowed, when updating username of user created by an external auth provider', async () => {
|
||||||
|
const dbUser = await User.create({
|
||||||
|
...user,
|
||||||
|
authProvider: AuthProviderType.LDAP
|
||||||
|
})
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser!.id)
|
||||||
|
const newUsername = 'newUsername'
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/SASjsApi/user/${dbUser!.id}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ username: newUsername })
|
||||||
|
.expect(405)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Method Not Allowed, when updating displayName of user created by an external auth provider', async () => {
|
||||||
|
const dbUser = await User.create({
|
||||||
|
...user,
|
||||||
|
authProvider: AuthProviderType.LDAP
|
||||||
|
})
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser!.id)
|
||||||
|
const newDisplayName = 'My new display Name'
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/SASjsApi/user/${dbUser!.id}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ displayName: newDisplayName })
|
||||||
|
.expect(405)
|
||||||
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch('/SASjsApi/user/1234')
|
.patch('/SASjsApi/user/1234')
|
||||||
@@ -254,7 +289,7 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if username is already present', async () => {
|
it('should respond with Conflict if username is already present', async () => {
|
||||||
const dbUser1 = await controller.createUser(user)
|
const dbUser1 = await controller.createUser(user)
|
||||||
const dbUser2 = await controller.createUser({
|
const dbUser2 = await controller.createUser({
|
||||||
...user,
|
...user,
|
||||||
@@ -265,9 +300,9 @@ describe('user', () => {
|
|||||||
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({ username: dbUser2.username })
|
.send({ username: dbUser2.username })
|
||||||
.expect(403)
|
.expect(409)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Username already exists.')
|
expect(res.text).toEqual('Username already exists.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -349,7 +384,7 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if username is already present', async () => {
|
it('should respond with Conflict if username is already present', async () => {
|
||||||
const dbUser1 = await controller.createUser(user)
|
const dbUser1 = await controller.createUser(user)
|
||||||
const dbUser2 = await controller.createUser({
|
const dbUser2 = await controller.createUser({
|
||||||
...user,
|
...user,
|
||||||
@@ -360,9 +395,9 @@ describe('user', () => {
|
|||||||
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
|
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({ username: dbUser2.username })
|
.send({ username: dbUser2.username })
|
||||||
.expect(403)
|
.expect(409)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Username already exists.')
|
expect(res.text).toEqual('Username already exists.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -446,7 +481,7 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
|
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
|
||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
@@ -454,9 +489,9 @@ describe('user', () => {
|
|||||||
.delete(`/SASjsApi/user/${dbUser.id}`)
|
.delete(`/SASjsApi/user/${dbUser.id}`)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.send({ password: 'incorrectpassword' })
|
.send({ password: 'incorrectpassword' })
|
||||||
.expect(403)
|
.expect(401)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Invalid password.')
|
expect(res.text).toEqual('Invalid password.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -528,7 +563,7 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
|
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
|
||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
@@ -536,9 +571,9 @@ describe('user', () => {
|
|||||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.send({ password: 'incorrectpassword' })
|
.send({ password: 'incorrectpassword' })
|
||||||
.expect(403)
|
.expect(401)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Invalid password.')
|
expect(res.text).toEqual('Invalid password.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -652,16 +687,16 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if userId is incorrect', async () => {
|
it('should respond with Not Found if userId is incorrect', async () => {
|
||||||
await controller.createUser(user)
|
await controller.createUser(user)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/user/1234')
|
.get('/SASjsApi/user/1234')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(403)
|
.expect(404)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: User is not found.')
|
expect(res.text).toEqual('User is not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -731,16 +766,16 @@ describe('user', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if username is incorrect', async () => {
|
it('should respond with Not Found if username is incorrect', async () => {
|
||||||
await controller.createUser(user)
|
await controller.createUser(user)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/user/by/username/randomUsername')
|
.get('/SASjsApi/user/by/username/randomUsername')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(403)
|
.expect(404)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: User is not found.')
|
expect(res.text).toEqual('User is not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -39,21 +39,90 @@ describe('web', () => {
|
|||||||
|
|
||||||
describe('home', () => {
|
describe('home', () => {
|
||||||
it('should respond with CSRF Token', async () => {
|
it('should respond with CSRF Token', async () => {
|
||||||
await request(app)
|
const res = await request(app).get('/').expect(200)
|
||||||
.get('/')
|
|
||||||
.expect(
|
expect(res.text).toMatch(
|
||||||
'set-cookie',
|
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/
|
||||||
/_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\//
|
)
|
||||||
)
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SASLogon/authorize', () => {
|
||||||
|
let csrfToken: string
|
||||||
|
let authCookies: string
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
;({ csrfToken } = await getCSRF(app))
|
||||||
|
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
const credentials = {
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
}
|
||||||
|
|
||||||
|
;({ authCookies } = await performLogin(app, credentials, csrfToken))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const collections = mongoose.connection.collections
|
||||||
|
const collection = collections['users']
|
||||||
|
await collection.deleteMany({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with authorization code', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/authorize')
|
||||||
|
.set('Cookie', [authCookies].join('; '))
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({ clientId })
|
||||||
|
|
||||||
|
expect(res.body).toHaveProperty('code')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if CSRF Token is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/authorize')
|
||||||
|
.set('Cookie', [authCookies].join('; '))
|
||||||
|
.send({ clientId })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Invalid CSRF token!')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if clientId is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/authorize')
|
||||||
|
.set('Cookie', [authCookies].join('; '))
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"clientId" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if clientId is incorrect', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/authorize')
|
||||||
|
.set('Cookie', [authCookies].join('; '))
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
clientId: 'WrongClientID'
|
||||||
|
})
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Invalid clientId.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('SASLogon/login', () => {
|
describe('SASLogon/login', () => {
|
||||||
let csrfToken: string
|
let csrfToken: string
|
||||||
let cookies: string
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
;({ csrfToken, cookies } = await getCSRF(app))
|
;({ csrfToken } = await getCSRF(app))
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -67,7 +136,6 @@ describe('web', () => {
|
|||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASLogon/login')
|
.post('/SASLogon/login')
|
||||||
.set('Cookie', cookies)
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
.set('x-xsrf-token', csrfToken)
|
||||||
.send({
|
.send({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
@@ -80,73 +148,113 @@ describe('web', () => {
|
|||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
isAdmin: user.isAdmin
|
isAdmin: user.isAdmin,
|
||||||
|
needsToUpdatePassword: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
describe('SASLogon/authorize', () => {
|
|
||||||
let csrfToken: string
|
|
||||||
let cookies: string
|
|
||||||
let authCookies: string
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
;({ csrfToken, cookies } = await getCSRF(app))
|
|
||||||
|
|
||||||
|
it('should respond with too many requests when attempting with invalid password for a same user too many times', async () => {
|
||||||
await userController.createUser(user)
|
await userController.createUser(user)
|
||||||
|
|
||||||
const credentials = {
|
const promises: request.Test[] = []
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
}
|
|
||||||
|
|
||||||
;({ cookies: authCookies } = await performLogin(
|
const maxConsecutiveFailsByUsernameAndIp = Number(
|
||||||
app,
|
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
credentials,
|
)
|
||||||
cookies,
|
|
||||||
csrfToken
|
|
||||||
))
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
Array(maxConsecutiveFailsByUsernameAndIp + 1)
|
||||||
const collections = mongoose.connection.collections
|
.fill(0)
|
||||||
const collection = collections['users']
|
.map((_, i) => {
|
||||||
await collection.deleteMany({})
|
promises.push(
|
||||||
})
|
request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
username: user.username,
|
||||||
|
password: 'invalid-password'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
it('should respond with authorization code', async () => {
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASLogon/authorize')
|
.post('/SASLogon/login')
|
||||||
.set('Cookie', [authCookies, cookies].join('; '))
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
.set('x-xsrf-token', csrfToken)
|
||||||
.send({ clientId })
|
.send({
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
.expect(429)
|
||||||
|
|
||||||
expect(res.body).toHaveProperty('code')
|
expect(res.text).toContain('Too Many Requests!')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if clientId is missing', async () => {
|
it('should respond with too many requests when attempting with invalid credentials for different users but with same ip too many times', async () => {
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
const promises: request.Test[] = []
|
||||||
|
|
||||||
|
const maxWrongAttemptsByIpPerDay = Number(
|
||||||
|
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
|
||||||
|
)
|
||||||
|
|
||||||
|
Array(maxWrongAttemptsByIpPerDay + 1)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => {
|
||||||
|
promises.push(
|
||||||
|
request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
username: `user${i}`,
|
||||||
|
password: 'invalid-password'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASLogon/authorize')
|
.post('/SASLogon/login')
|
||||||
.set('Cookie', [authCookies, cookies].join('; '))
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
.set('x-xsrf-token', csrfToken)
|
||||||
.send({})
|
.send({
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
.expect(429)
|
||||||
|
|
||||||
|
expect(res.text).toContain('Too Many Requests!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if CSRF Token is not present', async () => {
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.send({
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual(`"clientId" is required`)
|
expect(res.text).toEqual('Invalid CSRF token!')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if clientId is incorrect', async () => {
|
it('should respond with Bad Request if CSRF Token is invalid', async () => {
|
||||||
const res = await request(app)
|
await userController.createUser(user)
|
||||||
.post('/SASLogon/authorize')
|
|
||||||
.set('Cookie', [authCookies, cookies].join('; '))
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
|
||||||
.send({
|
|
||||||
clientId: 'WrongClientID'
|
|
||||||
})
|
|
||||||
.expect(403)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Invalid clientId.')
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('x-xsrf-token', 'INVALID_CSRF_TOKEN')
|
||||||
|
.send({
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Invalid CSRF token!')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -154,30 +262,25 @@ describe('web', () => {
|
|||||||
|
|
||||||
const getCSRF = async (app: Express) => {
|
const getCSRF = async (app: Express) => {
|
||||||
// make request to get CSRF
|
// make request to get CSRF
|
||||||
const { header } = await request(app).get('/')
|
const { text } = await request(app).get('/')
|
||||||
const cookies = header['set-cookie'].join()
|
|
||||||
|
|
||||||
const csrfToken = extractCSRF(cookies)
|
return { csrfToken: extractCSRF(text) }
|
||||||
return { csrfToken, cookies }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const performLogin = async (
|
const performLogin = async (
|
||||||
app: Express,
|
app: Express,
|
||||||
credentials: { username: string; password: string },
|
credentials: { username: string; password: string },
|
||||||
cookies: string,
|
|
||||||
csrfToken: string
|
csrfToken: string
|
||||||
) => {
|
) => {
|
||||||
const { header } = await request(app)
|
const { header } = await request(app)
|
||||||
.post('/SASLogon/login')
|
.post('/SASLogon/login')
|
||||||
.set('Cookie', cookies)
|
|
||||||
.set('x-xsrf-token', csrfToken)
|
.set('x-xsrf-token', csrfToken)
|
||||||
.send(credentials)
|
.send(credentials)
|
||||||
|
|
||||||
const newCookies: string = header['set-cookie'].join()
|
return { authCookies: header['set-cookie'].join() }
|
||||||
return { cookies: newCookies }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractCSRF = (cookies: string) =>
|
const extractCSRF = (text: string) =>
|
||||||
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
|
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
|
||||||
cookies
|
text
|
||||||
)![2]
|
)![1]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ stpRouter.get('/execute', async (req, res) => {
|
|||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeReturnRaw(req, query._program)
|
const response = await controller.executeGetRequest(req, query._program)
|
||||||
|
|
||||||
if (response instanceof Buffer) {
|
if (response instanceof Buffer) {
|
||||||
res.writeHead(200, (req as any).sasHeaders)
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
@@ -42,7 +42,7 @@ stpRouter.post(
|
|||||||
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeReturnJson(
|
const response = await controller.executePostRequest(
|
||||||
req,
|
req,
|
||||||
req.body,
|
req.body,
|
||||||
req.query?._program as string
|
req.query?._program as string
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
|
|||||||
const response = await controller.createUser(body)
|
const response = await controller.createUser(body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(err.code).send(err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
|
|||||||
const response = await controller.getAllUsers()
|
const response = await controller.getAllUsers()
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(err.code).send(err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ userRouter.get(
|
|||||||
const response = await controller.getUserByUsername(req, username)
|
const response = await controller.getUserByUsername(req, username)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(err.code).send(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -64,7 +64,7 @@ userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
|
|||||||
const response = await controller.getUser(req, parseInt(userId))
|
const response = await controller.getUser(req, parseInt(userId))
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(err.code).send(err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ userRouter.patch(
|
|||||||
const response = await controller.updateUserByUsername(username, body)
|
const response = await controller.updateUserByUsername(username, body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(err.code).send(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -113,7 +113,7 @@ userRouter.patch(
|
|||||||
const response = await controller.updateUser(parseInt(userId), body)
|
const response = await controller.updateUser(parseInt(userId), body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(err.code).send(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -141,7 +141,7 @@ userRouter.delete(
|
|||||||
await controller.deleteUserByUsername(username, data, user!.isAdmin)
|
await controller.deleteUserByUsername(username, data, user!.isAdmin)
|
||||||
res.status(200).send('Account Deleted!')
|
res.status(200).send('Account Deleted!')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(err.code).send(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -163,7 +163,7 @@ userRouter.delete(
|
|||||||
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
|
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
|
||||||
res.status(200).send('Account Deleted!')
|
res.status(200).send('Account Deleted!')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(err.code).send(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { Request } from 'express'
|
import express, { Request } from 'express'
|
||||||
import { authenticateAccessToken } from '../../middlewares'
|
import { authenticateAccessToken, generateCSRFToken } from '../../middlewares'
|
||||||
import { folderExists } from '@sasjs/utils'
|
import { folderExists } from '@sasjs/utils'
|
||||||
|
|
||||||
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
||||||
@@ -13,7 +13,7 @@ const router = express.Router()
|
|||||||
router.get('/', authenticateAccessToken, async (req, res) => {
|
router.get('/', authenticateAccessToken, async (req, res) => {
|
||||||
const content = appStreamHtml(process.appStreamConfig)
|
const content = appStreamHtml(process.appStreamConfig)
|
||||||
|
|
||||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
res.cookie('XSRF-TOKEN', generateCSRFToken())
|
||||||
|
|
||||||
return res.send(content)
|
return res.send(content)
|
||||||
})
|
})
|
||||||
@@ -58,7 +58,7 @@ export const publishAppStream = async (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const sasJsPort = process.env.PORT || 5000
|
const sasJsPort = process.env.PORT || 5000
|
||||||
console.log(
|
process.logger.info(
|
||||||
'Serving Stream App: ',
|
'Serving Stream App: ',
|
||||||
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const style = `<style>
|
|||||||
}
|
}
|
||||||
.app-container .app img{
|
.app-container .app img{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: calc(100% - 30px);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import webRouter from './web'
|
|||||||
import apiRouter from './api'
|
import apiRouter from './api'
|
||||||
import appStreamRouter from './appStream'
|
import appStreamRouter from './appStream'
|
||||||
|
|
||||||
import { csrfProtection } from '../app'
|
import { csrfProtection } from '../middlewares'
|
||||||
|
|
||||||
export const setupRoutes = (app: Express) => {
|
export const setupRoutes = (app: Express) => {
|
||||||
app.use('/SASjsApi', apiRouter)
|
app.use('/SASjsApi', apiRouter)
|
||||||
@@ -15,5 +15,5 @@ export const setupRoutes = (app: Express) => {
|
|||||||
appStreamRouter(req, res, next)
|
appStreamRouter(req, res, next)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use('/', csrfProtection, webRouter)
|
app.use('/', webRouter)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import sas9WebRouter from './sas9-web'
|
||||||
|
import sasViyaWebRouter from './sasviya-web'
|
||||||
import webRouter from './web'
|
import webRouter from './web'
|
||||||
|
import { MOCK_SERVERTYPEType } from '../../utils'
|
||||||
|
import { csrfProtection } from '../../middlewares'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.use('/', webRouter)
|
const { MOCK_SERVERTYPE } = process.env
|
||||||
|
|
||||||
|
switch (MOCK_SERVERTYPE) {
|
||||||
|
case MOCK_SERVERTYPEType.SAS9: {
|
||||||
|
router.use('/', sas9WebRouter)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case MOCK_SERVERTYPEType.SASVIYA: {
|
||||||
|
router.use('/', sasViyaWebRouter)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
router.use('/', csrfProtection, webRouter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
152
api/src/routes/web/sas9-web.ts
Normal file
152
api/src/routes/web/sas9-web.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { generateCSRFToken } from '../../middlewares'
|
||||||
|
import { WebController } from '../../controllers'
|
||||||
|
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
||||||
|
import multer from 'multer'
|
||||||
|
import path from 'path'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { FileUploadController } from '../../controllers/internal'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const sas9WebRouter = express.Router()
|
||||||
|
const webController = new WebController()
|
||||||
|
// Mock controller must be singleton because it keeps the states
|
||||||
|
// for example `isLoggedIn` and potentially more in future mocks
|
||||||
|
const controller = new MockSas9Controller()
|
||||||
|
const fileUploadController = new FileUploadController()
|
||||||
|
|
||||||
|
const mockPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
dest: path.join(process.cwd(), mockPath, 'sas9', 'files-received')
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.get('/', async (req, res) => {
|
||||||
|
let response
|
||||||
|
try {
|
||||||
|
response = await webController.home()
|
||||||
|
} catch (_) {
|
||||||
|
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||||
|
} finally {
|
||||||
|
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||||
|
const injectedContent = response?.replace(
|
||||||
|
'</head>',
|
||||||
|
`${codeToInject}</head>`
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.send(injectedContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
||||||
|
const response = await controller.sasStoredProcess(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
|
||||||
|
const response = await controller.sasStoredProcessDoGet(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.post(
|
||||||
|
'/SASStoredProcess/do/',
|
||||||
|
fileUploadController.preUploadMiddleware,
|
||||||
|
fileUploadController.getMulterUploadObject().any(),
|
||||||
|
async (req, res) => {
|
||||||
|
const response = await controller.sasStoredProcessDoPost(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
|
||||||
|
const response = await controller.loginGet()
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.post('/SASLogon/login', async (req, res) => {
|
||||||
|
const response = await controller.loginPost(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.get('/SASLogon/logout', async (req, res) => {
|
||||||
|
const response = await controller.logout(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.get('/SASStoredProcess/Logoff', async (req, res) => {
|
||||||
|
const response = await controller.logoff(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default sas9WebRouter
|
||||||
33
api/src/routes/web/sasviya-web.ts
Normal file
33
api/src/routes/web/sasviya-web.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { generateCSRFToken } from '../../middlewares'
|
||||||
|
import { WebController } from '../../controllers/web'
|
||||||
|
|
||||||
|
const sasViyaWebRouter = express.Router()
|
||||||
|
const controller = new WebController()
|
||||||
|
|
||||||
|
sasViyaWebRouter.get('/', async (req, res) => {
|
||||||
|
let response
|
||||||
|
try {
|
||||||
|
response = await controller.home()
|
||||||
|
} catch (_) {
|
||||||
|
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||||
|
} finally {
|
||||||
|
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||||
|
const injectedContent = response?.replace(
|
||||||
|
'</head>',
|
||||||
|
`${codeToInject}</head>`
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.send(injectedContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sasViyaWebRouter.post('/SASJobExecution/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
res.send({ test: 'test' })
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default sasViyaWebRouter
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import { generateCSRFToken } from '../../middlewares'
|
||||||
import { WebController } from '../../controllers/web'
|
import { WebController } from '../../controllers/web'
|
||||||
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
|
import {
|
||||||
|
authenticateAccessToken,
|
||||||
|
bruteForceProtection,
|
||||||
|
desktopRestrict
|
||||||
|
} from '../../middlewares'
|
||||||
import { authorizeValidation, loginWebValidation } from '../../utils'
|
import { authorizeValidation, loginWebValidation } from '../../utils'
|
||||||
|
|
||||||
const webRouter = express.Router()
|
const webRouter = express.Router()
|
||||||
@@ -11,25 +16,41 @@ webRouter.get('/', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
response = await controller.home()
|
response = await controller.home()
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
response = 'Web Build is not present'
|
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||||
} finally {
|
} finally {
|
||||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
const { ALLOWED_DOMAIN } = process.env
|
||||||
|
const allowedDomain = ALLOWED_DOMAIN?.trim()
|
||||||
|
const domain = allowedDomain ? ` Domain=${allowedDomain};` : ''
|
||||||
|
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()};${domain} Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||||
|
const injectedContent = response?.replace(
|
||||||
|
'</head>',
|
||||||
|
`${codeToInject}</head>`
|
||||||
|
)
|
||||||
|
|
||||||
return res.send(response)
|
return res.send(injectedContent)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
|
webRouter.post(
|
||||||
const { error, value: body } = loginWebValidation(req.body)
|
'/SASLogon/login',
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
desktopRestrict,
|
||||||
|
bruteForceProtection,
|
||||||
|
async (req, res) => {
|
||||||
|
const { error, value: body } = loginWebValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.login(req, body)
|
const response = await controller.login(req, body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
if (err instanceof Error) {
|
||||||
|
res.status(500).send(err.toString())
|
||||||
|
} else {
|
||||||
|
res.status(err.code).send(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
webRouter.post(
|
webRouter.post(
|
||||||
'/SASLogon/authorize',
|
'/SASLogon/authorize',
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ appPromise.then(async (app) => {
|
|||||||
const protocol = process.env.PROTOCOL || 'http'
|
const protocol = process.env.PROTOCOL || 'http'
|
||||||
const sasJsPort = process.env.PORT || 5000
|
const sasJsPort = process.env.PORT || 5000
|
||||||
|
|
||||||
console.log('PROTOCOL: ', protocol)
|
process.logger.info('PROTOCOL: ', protocol)
|
||||||
|
|
||||||
if (protocol !== 'https') {
|
if (protocol !== 'https') {
|
||||||
app.listen(sasJsPort, () => {
|
app.listen(sasJsPort, () => {
|
||||||
console.log(
|
process.logger.info(
|
||||||
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
|
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -20,7 +20,7 @@ appPromise.then(async (app) => {
|
|||||||
|
|
||||||
const httpsServer = createServer({ key, cert, ca }, app)
|
const httpsServer = createServer({ key, cert, ca }, app)
|
||||||
httpsServer.listen(sasJsPort, () => {
|
httpsServer.listen(sasJsPort, () => {
|
||||||
console.log(
|
process.logger.info(
|
||||||
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
|
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ export interface RequestUser {
|
|||||||
displayName: string
|
displayName: string
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
needsToUpdatePassword: boolean
|
||||||
autoExec?: string
|
autoExec?: string
|
||||||
}
|
}
|
||||||
|
|||||||
6
api/src/types/system/process.d.ts
vendored
6
api/src/types/system/process.d.ts
vendored
@@ -2,10 +2,14 @@ declare namespace NodeJS {
|
|||||||
export interface Process {
|
export interface Process {
|
||||||
sasLoc?: string
|
sasLoc?: string
|
||||||
nodeLoc?: string
|
nodeLoc?: string
|
||||||
|
pythonLoc?: string
|
||||||
|
rLoc?: string
|
||||||
driveLoc: string
|
driveLoc: string
|
||||||
|
sasjsRoot: string
|
||||||
logsLoc: string
|
logsLoc: string
|
||||||
|
logsUUID: string
|
||||||
|
sessionController?: import('../../controllers/internal').SessionController
|
||||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
sasSessionController?: import('../../controllers/internal').SASSessionController
|
||||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
|
||||||
appStreamConfig: import('../').AppStreamConfig
|
appStreamConfig: import('../').AppStreamConfig
|
||||||
logger: import('@sasjs/utils/logger').Logger
|
logger: import('@sasjs/utils/logger').Logger
|
||||||
runTimes: import('../../utils').RunTimeType[]
|
runTimes: import('../../utils').RunTimeType[]
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const loadAppStreamConfig = async () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('App Stream Config loaded!')
|
process.logger.info('App Stream Config loaded!')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addEntryToAppStreamConfig = (
|
export const addEntryToAppStreamConfig = (
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export const connectDB = async () => {
|
|||||||
throw new Error('Unable to connect to DB!')
|
throw new Error('Unable to connect to DB!')
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Connected to DB!')
|
process.logger.success('Connected to DB!')
|
||||||
return seedDB()
|
return seedDB()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
|||||||
export const copySASjsCore = async () => {
|
export const copySASjsCore = async () => {
|
||||||
if (process.env.NODE_ENV === 'test') return
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
console.log('Copying Macros from container to drive(tmp).')
|
process.logger.log('Copying Macros from container to drive.')
|
||||||
|
|
||||||
const macrosDrivePath = getMacrosFolder()
|
const macrosDrivePath = getMacrosFolder()
|
||||||
|
|
||||||
@@ -30,5 +30,5 @@ export const copySASjsCore = async () => {
|
|||||||
await createFile(macroFileDestPath, macroContent)
|
await createFile(macroFileDestPath, macroContent)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Macros Drive Path:', macrosDrivePath)
|
process.logger.info('Macros Drive Path:', macrosDrivePath)
|
||||||
}
|
}
|
||||||
|
|||||||
18
api/src/utils/createWeboutSasFile.ts
Normal file
18
api/src/utils/createWeboutSasFile.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { createFile } from '@sasjs/utils'
|
||||||
|
import { getMacrosFolder } from './file'
|
||||||
|
|
||||||
|
const fileContent = `%macro webout(action,ds,dslabel=,fmt=,missing=NULL,showmeta=NO,maxobs=MAX);
|
||||||
|
%ms_webout(&action,ds=&ds,dslabel=&dslabel,fmt=&fmt
|
||||||
|
,missing=&missing
|
||||||
|
,showmeta=&showmeta
|
||||||
|
,maxobs=&maxobs
|
||||||
|
)
|
||||||
|
%mend;`
|
||||||
|
|
||||||
|
export const createWeboutSasFile = async () => {
|
||||||
|
const macrosDrivePath = getMacrosFolder()
|
||||||
|
process.logger.log(`Creating webout.sas at ${macrosDrivePath}`)
|
||||||
|
const filePath = path.join(macrosDrivePath, 'webout.sas')
|
||||||
|
await createFile(filePath, fileContent)
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ export const sysInitCompiledPath = path.join(
|
|||||||
'systemInitCompiled.sas'
|
'systemInitCompiled.sas'
|
||||||
)
|
)
|
||||||
|
|
||||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
export const sasJSCoreMacros = path.join(apiRoot, 'sas', 'sasautos')
|
||||||
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
||||||
|
|
||||||
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
|
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
|
||||||
@@ -20,19 +20,24 @@ export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
|
|||||||
export const getDesktopUserAutoExecPath = () =>
|
export const getDesktopUserAutoExecPath = () =>
|
||||||
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
||||||
|
|
||||||
export const getSasjsRootFolder = () => process.driveLoc
|
export const getSasjsRootFolder = () => process.sasjsRoot
|
||||||
|
|
||||||
|
export const getSasjsDriveFolder = () => process.driveLoc
|
||||||
|
|
||||||
export const getLogFolder = () => process.logsLoc
|
export const getLogFolder = () => process.logsLoc
|
||||||
|
|
||||||
export const getAppStreamConfigPath = () =>
|
export const getAppStreamConfigPath = () =>
|
||||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
path.join(getSasjsDriveFolder(), 'appStreamConfig.json')
|
||||||
|
|
||||||
export const getMacrosFolder = () =>
|
export const getMacrosFolder = () =>
|
||||||
path.join(getSasjsRootFolder(), 'sasjscore')
|
path.join(getSasjsDriveFolder(), 'sas', 'sasautos')
|
||||||
|
|
||||||
|
export const getPackagesFolder = () =>
|
||||||
|
path.join(getSasjsDriveFolder(), 'sas', 'sas_packages')
|
||||||
|
|
||||||
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
||||||
|
|
||||||
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
export const getFilesFolder = () => path.join(getSasjsDriveFolder(), 'files')
|
||||||
|
|
||||||
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
|
||||||
|
|
||||||
export const generateAccessToken = (data: InfoJWT) =>
|
export const generateAccessToken = (data: InfoJWT, expiry?: number) =>
|
||||||
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
|
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
|
||||||
expiresIn: '1day'
|
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
|
||||||
|
|
||||||
export const generateRefreshToken = (data: InfoJWT) =>
|
export const generateRefreshToken = (data: InfoJWT, expiry?: number) =>
|
||||||
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
|
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
|
||||||
expiresIn: '30 days'
|
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Request } from 'express'
|
import { Request } from 'express'
|
||||||
|
|
||||||
|
export const TopLevelRoutes = ['/AppStream', '/SASjsApi']
|
||||||
|
|
||||||
const StaticAuthorizedRoutes = [
|
const StaticAuthorizedRoutes = [
|
||||||
'/AppStream',
|
|
||||||
'/SASjsApi/code/execute',
|
'/SASjsApi/code/execute',
|
||||||
'/SASjsApi/stp/execute',
|
'/SASjsApi/stp/execute',
|
||||||
'/SASjsApi/drive/deploy',
|
'/SASjsApi/drive/deploy',
|
||||||
@@ -9,17 +10,16 @@ const StaticAuthorizedRoutes = [
|
|||||||
'/SASjsApi/drive/file',
|
'/SASjsApi/drive/file',
|
||||||
'/SASjsApi/drive/folder',
|
'/SASjsApi/drive/folder',
|
||||||
'/SASjsApi/drive/fileTree',
|
'/SASjsApi/drive/fileTree',
|
||||||
'/SASjsApi/drive/rename',
|
'/SASjsApi/drive/rename'
|
||||||
'/SASjsApi/permission'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const getAuthorizedRoutes = () => {
|
export const getAuthorizedRoutes = () => {
|
||||||
const streamingApps = Object.keys(process.appStreamConfig)
|
const streamingApps = Object.keys(process.appStreamConfig)
|
||||||
const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`)
|
const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`)
|
||||||
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
|
return [...TopLevelRoutes, ...StaticAuthorizedRoutes, ...streamingAppsRoutes]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUri = (req: Request) => {
|
export const getPath = (req: Request) => {
|
||||||
const { baseUrl, path: reqPath } = req
|
const { baseUrl, path: reqPath } = req
|
||||||
|
|
||||||
if (baseUrl === '/AppStream') {
|
if (baseUrl === '/AppStream') {
|
||||||
@@ -33,4 +33,4 @@ export const getUri = (req: Request) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isAuthorizingRoute = (req: Request): boolean =>
|
export const isAuthorizingRoute = (req: Request): boolean =>
|
||||||
getAuthorizedRoutes().includes(getUri(req))
|
getAuthorizedRoutes().includes(getPath(req))
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ export const getCertificates = async () => {
|
|||||||
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
|
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
|
||||||
const caPath = CA_ROOT
|
const caPath = CA_ROOT
|
||||||
|
|
||||||
console.log('keyPath: ', keyPath)
|
process.logger.info('keyPath: ', keyPath)
|
||||||
console.log('certPath: ', certPath)
|
process.logger.info('certPath: ', certPath)
|
||||||
if (caPath) console.log('caPath: ', caPath)
|
if (caPath) process.logger.info('caPath: ', caPath)
|
||||||
|
|
||||||
const key = await readFile(keyPath)
|
const key = await readFile(keyPath)
|
||||||
const cert = await readFile(certPath)
|
const cert = await readFile(certPath)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
|
|||||||
import { RunTimeType } from './verifyEnvVariables'
|
import { RunTimeType } from './verifyEnvVariables'
|
||||||
|
|
||||||
export const getDesktopFields = async () => {
|
export const getDesktopFields = async () => {
|
||||||
const { SAS_PATH, NODE_PATH } = process.env
|
const { SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH } = process.env
|
||||||
|
|
||||||
let sasLoc, nodeLoc
|
let sasLoc, nodeLoc, pythonLoc, rLoc
|
||||||
|
|
||||||
if (process.runTimes.includes(RunTimeType.SAS)) {
|
if (process.runTimes.includes(RunTimeType.SAS)) {
|
||||||
sasLoc = SAS_PATH ?? (await getSASLocation())
|
sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||||
@@ -16,7 +16,15 @@ export const getDesktopFields = async () => {
|
|||||||
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||||
}
|
}
|
||||||
|
|
||||||
return { sasLoc, nodeLoc }
|
if (process.runTimes.includes(RunTimeType.PY)) {
|
||||||
|
pythonLoc = PYTHON_PATH ?? (await getPythonLocation())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.runTimes.includes(RunTimeType.R)) {
|
||||||
|
rLoc = R_PATH ?? (await getRLocation())
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sasLoc, nodeLoc, pythonLoc, rLoc }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDriveLocation = async (): Promise<string> => {
|
const getDriveLocation = async (): Promise<string> => {
|
||||||
@@ -91,3 +99,47 @@ const getNodeLocation = async (): Promise<string> => {
|
|||||||
|
|
||||||
return targetName
|
return targetName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPythonLocation = async (): Promise<string> => {
|
||||||
|
const validator = async (filePath: string) => {
|
||||||
|
if (!filePath) return 'Path to Python executable is required.'
|
||||||
|
|
||||||
|
if (!(await fileExists(filePath))) {
|
||||||
|
return 'No file found at provided path.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLocation = isWindows() ? 'C:\\Python' : '/usr/bin/python'
|
||||||
|
|
||||||
|
const targetName = await getString(
|
||||||
|
'Please enter full path to a Python executable: ',
|
||||||
|
validator,
|
||||||
|
defaultLocation
|
||||||
|
)
|
||||||
|
|
||||||
|
return targetName
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRLocation = async (): Promise<string> => {
|
||||||
|
const validator = async (filePath: string) => {
|
||||||
|
if (!filePath) return 'Path to R executable is required.'
|
||||||
|
|
||||||
|
if (!(await fileExists(filePath))) {
|
||||||
|
return 'No file found at provided path.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLocation = isWindows() ? 'C:\\Rscript' : '/usr/bin/Rscript'
|
||||||
|
|
||||||
|
const targetName = await getString(
|
||||||
|
'Please enter full path to a R executable: ',
|
||||||
|
validator,
|
||||||
|
defaultLocation
|
||||||
|
)
|
||||||
|
|
||||||
|
return targetName
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
|||||||
const { user, accessToken } = req
|
const { user, accessToken } = req
|
||||||
const csrfToken = req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN']
|
const csrfToken = req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN']
|
||||||
const sessionId = req.cookies['connect.sid']
|
const sessionId = req.cookies['connect.sid']
|
||||||
const { _csrf } = req.cookies
|
|
||||||
|
|
||||||
const httpHeaders: string[] = []
|
const httpHeaders: string[] = []
|
||||||
|
|
||||||
@@ -16,14 +15,15 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
|||||||
|
|
||||||
const cookies: string[] = []
|
const cookies: string[] = []
|
||||||
if (sessionId) cookies.push(`connect.sid=${sessionId}`)
|
if (sessionId) cookies.push(`connect.sid=${sessionId}`)
|
||||||
if (_csrf) cookies.push(`_csrf=${_csrf}`)
|
|
||||||
|
|
||||||
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
||||||
|
|
||||||
|
//In desktop mode when mocking mode is enabled, user was undefined.
|
||||||
|
//So this is workaround.
|
||||||
return {
|
return {
|
||||||
username: user!.username,
|
username: user ? user.username : 'demo',
|
||||||
userId: user!.userId,
|
userId: user ? user.userId : 0,
|
||||||
displayName: user!.displayName,
|
displayName: user ? user.displayName : 'demo',
|
||||||
serverUrl: protocol + host,
|
serverUrl: protocol + host,
|
||||||
httpHeaders
|
httpHeaders
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { getFilesFolder } from './file'
|
|||||||
import { RunTimeType } from '.'
|
import { RunTimeType } from '.'
|
||||||
|
|
||||||
export const getRunTimeAndFilePath = async (programPath: string) => {
|
export const getRunTimeAndFilePath = async (programPath: string) => {
|
||||||
const ext = path.extname(programPath)
|
const ext = path.extname(programPath).toLowerCase()
|
||||||
// If programPath (_program) is provided with a ".sas" or ".js" extension
|
// If programPath (_program) is provided with a ".sas", ".js", ".py" or ".r" extension
|
||||||
// we should use that extension to determine the appropriate runTime
|
// we should use that extension to determine the appropriate runTime
|
||||||
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
||||||
const runTime = ext.slice(1)
|
const runTime = ext.slice(1)
|
||||||
|
|||||||
55
api/src/utils/getTokensFromDB.ts
Normal file
55
api/src/utils/getTokensFromDB.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import User from '../model/User'
|
||||||
|
|
||||||
|
const isValidToken = async (
|
||||||
|
token: string,
|
||||||
|
key: string,
|
||||||
|
userId: number,
|
||||||
|
clientId: string
|
||||||
|
) => {
|
||||||
|
const promise = new Promise<boolean>((resolve, reject) =>
|
||||||
|
jwt.verify(token, key, (err, decoded) => {
|
||||||
|
if (err) return reject(false)
|
||||||
|
|
||||||
|
if (decoded?.userId === userId && decoded?.clientId === clientId) {
|
||||||
|
return resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reject(false)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return await promise.then(() => true).catch(() => false)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTokensFromDB = async (userId: number, clientId: string) => {
|
||||||
|
const user = await User.findOne({ id: userId })
|
||||||
|
if (!user) return
|
||||||
|
|
||||||
|
const currentTokenObj = user.tokens.find(
|
||||||
|
(tokenObj: any) => tokenObj.clientId === clientId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (currentTokenObj) {
|
||||||
|
const accessToken = currentTokenObj.accessToken
|
||||||
|
const refreshToken = currentTokenObj.refreshToken
|
||||||
|
|
||||||
|
const isValidAccessToken = await isValidToken(
|
||||||
|
accessToken,
|
||||||
|
process.secrets.ACCESS_TOKEN_SECRET,
|
||||||
|
userId,
|
||||||
|
clientId
|
||||||
|
)
|
||||||
|
|
||||||
|
const isValidRefreshToken = await isValidToken(
|
||||||
|
refreshToken,
|
||||||
|
process.secrets.REFRESH_TOKEN_SECRET,
|
||||||
|
userId,
|
||||||
|
clientId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isValidAccessToken && isValidRefreshToken) {
|
||||||
|
return { accessToken, refreshToken }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './appStreamConfig'
|
export * from './appStreamConfig'
|
||||||
export * from './connectDB'
|
export * from './connectDB'
|
||||||
export * from './copySASjsCore'
|
export * from './copySASjsCore'
|
||||||
|
export * from './createWeboutSasFile'
|
||||||
export * from './desktopAutoExec'
|
export * from './desktopAutoExec'
|
||||||
export * from './extractHeaders'
|
export * from './extractHeaders'
|
||||||
export * from './extractName'
|
export * from './extractName'
|
||||||
@@ -14,16 +15,21 @@ export * from './getDesktopFields'
|
|||||||
export * from './getPreProgramVariables'
|
export * from './getPreProgramVariables'
|
||||||
export * from './getRunTimeAndFilePath'
|
export * from './getRunTimeAndFilePath'
|
||||||
export * from './getServerUrl'
|
export * from './getServerUrl'
|
||||||
|
export * from './getTokensFromDB'
|
||||||
export * from './instantiateLogger'
|
export * from './instantiateLogger'
|
||||||
export * from './isDebugOn'
|
export * from './isDebugOn'
|
||||||
export * from './zipped'
|
export * from './isPublicRoute'
|
||||||
|
export * from './ldapClient'
|
||||||
export * from './parseLogToArray'
|
export * from './parseLogToArray'
|
||||||
|
export * from './rateLimiter'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
export * from './saveTokensInDB'
|
export * from './saveTokensInDB'
|
||||||
export * from './seedDB'
|
export * from './seedDB'
|
||||||
export * from './setProcessVariables'
|
export * from './setProcessVariables'
|
||||||
export * from './setupFolders'
|
export * from './setupFolders'
|
||||||
|
export * from './setupUserAutoExec'
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './validation'
|
export * from './validation'
|
||||||
export * from './verifyEnvVariables'
|
export * from './verifyEnvVariables'
|
||||||
export * from './verifyTokenInDB'
|
export * from './verifyTokenInDB'
|
||||||
|
export * from './zipped'
|
||||||
|
|||||||
32
api/src/utils/isPublicRoute.ts
Normal file
32
api/src/utils/isPublicRoute.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Request } from 'express'
|
||||||
|
import { getPath } from './getAuthorizedRoutes'
|
||||||
|
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
||||||
|
import Permission from '../model/Permission'
|
||||||
|
import { PermissionSettingForRoute } from '../controllers'
|
||||||
|
import { RequestUser } from '../types'
|
||||||
|
|
||||||
|
export const isPublicRoute = async (req: Request): Promise<boolean> => {
|
||||||
|
const group = await Group.findOne({ name: PUBLIC_GROUP_NAME })
|
||||||
|
if (group) {
|
||||||
|
const path = getPath(req)
|
||||||
|
|
||||||
|
const groupPermission = await Permission.findOne({
|
||||||
|
path,
|
||||||
|
group: group?._id
|
||||||
|
})
|
||||||
|
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publicUser: RequestUser = {
|
||||||
|
userId: 0,
|
||||||
|
clientId: 'public_app',
|
||||||
|
username: 'publicUser',
|
||||||
|
displayName: 'Public User',
|
||||||
|
isAdmin: false,
|
||||||
|
isActive: true,
|
||||||
|
needsToUpdatePassword: false
|
||||||
|
}
|
||||||
163
api/src/utils/ldapClient.ts
Normal file
163
api/src/utils/ldapClient.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { createClient, Client } from 'ldapjs'
|
||||||
|
import { ReturnCode } from './verifyEnvVariables'
|
||||||
|
|
||||||
|
export interface LDAPUser {
|
||||||
|
uid: string
|
||||||
|
username: string
|
||||||
|
displayName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LDAPGroup {
|
||||||
|
name: string
|
||||||
|
members: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LDAPClient {
|
||||||
|
private ldapClient: Client
|
||||||
|
private static classInstance: LDAPClient | null
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
process.logger.info('creating LDAP client')
|
||||||
|
this.ldapClient = createClient({ url: process.env.LDAP_URL as string })
|
||||||
|
|
||||||
|
this.ldapClient.on('error', (error) => {
|
||||||
|
process.logger.error(error.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async init() {
|
||||||
|
if (!LDAPClient.classInstance) {
|
||||||
|
LDAPClient.classInstance = new LDAPClient()
|
||||||
|
|
||||||
|
process.logger.info('binding LDAP client')
|
||||||
|
await LDAPClient.classInstance.bind().catch((error) => {
|
||||||
|
LDAPClient.classInstance = null
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return LDAPClient.classInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
private async bind() {
|
||||||
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
|
const { LDAP_BIND_DN, LDAP_BIND_PASSWORD } = process.env
|
||||||
|
this.ldapClient.bind(LDAP_BIND_DN!, LDAP_BIND_PASSWORD!, (error) => {
|
||||||
|
if (error) reject(error)
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await promise.catch((error) => {
|
||||||
|
throw new Error(error.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllLDAPUsers() {
|
||||||
|
const promise = new Promise<LDAPUser[]>((resolve, reject) => {
|
||||||
|
const { LDAP_USERS_BASE_DN } = process.env
|
||||||
|
const filter = `(objectClass=*)`
|
||||||
|
|
||||||
|
this.ldapClient.search(
|
||||||
|
LDAP_USERS_BASE_DN!,
|
||||||
|
{ filter },
|
||||||
|
(error, result) => {
|
||||||
|
if (error) reject(error)
|
||||||
|
|
||||||
|
const users: LDAPUser[] = []
|
||||||
|
|
||||||
|
result.on('searchEntry', (entry) => {
|
||||||
|
users.push({
|
||||||
|
uid: entry.object.uid as string,
|
||||||
|
username: entry.object.username as string,
|
||||||
|
displayName: entry.object.displayname as string
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
result.on('end', (result) => {
|
||||||
|
resolve(users)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return await promise
|
||||||
|
.then((res) => res)
|
||||||
|
.catch((error) => {
|
||||||
|
throw new Error(error.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllLDAPGroups() {
|
||||||
|
const promise = new Promise<LDAPGroup[]>((resolve, reject) => {
|
||||||
|
const { LDAP_GROUPS_BASE_DN } = process.env
|
||||||
|
|
||||||
|
this.ldapClient.search(LDAP_GROUPS_BASE_DN!, {}, (error, result) => {
|
||||||
|
if (error) reject(error)
|
||||||
|
|
||||||
|
const groups: LDAPGroup[] = []
|
||||||
|
|
||||||
|
result.on('searchEntry', (entry) => {
|
||||||
|
const members =
|
||||||
|
typeof entry.object.memberuid === 'string'
|
||||||
|
? [entry.object.memberuid]
|
||||||
|
: entry.object.memberuid
|
||||||
|
groups.push({
|
||||||
|
name: entry.object.cn as string,
|
||||||
|
members
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
result.on('end', (result) => {
|
||||||
|
resolve(groups)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return await promise
|
||||||
|
.then((res) => res)
|
||||||
|
.catch((error) => {
|
||||||
|
throw new Error(error.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyUser(username: string, password: string) {
|
||||||
|
const promise = new Promise<boolean>((resolve, reject) => {
|
||||||
|
const { LDAP_USERS_BASE_DN } = process.env
|
||||||
|
const filter = `(username=${username})`
|
||||||
|
|
||||||
|
this.ldapClient.search(
|
||||||
|
LDAP_USERS_BASE_DN!,
|
||||||
|
{ filter },
|
||||||
|
(error, result) => {
|
||||||
|
if (error) reject(error)
|
||||||
|
|
||||||
|
const items: any = []
|
||||||
|
|
||||||
|
result.on('searchEntry', (entry) => {
|
||||||
|
items.push(entry.object)
|
||||||
|
})
|
||||||
|
|
||||||
|
result.on('end', (result) => {
|
||||||
|
if (result?.status !== 0 || items.length === 0) return reject()
|
||||||
|
|
||||||
|
// pick the first found
|
||||||
|
const user = items[0]
|
||||||
|
|
||||||
|
this.ldapClient.bind(user.dn, password, (error) => {
|
||||||
|
if (error) return reject(error)
|
||||||
|
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return await promise
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => {
|
||||||
|
throw new Error('Invalid password.')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,12 +22,12 @@ export const getEnvCSPDirectives = (
|
|||||||
try {
|
try {
|
||||||
cspConfigJson = JSON.parse(file)
|
cspConfigJson = JSON.parse(file)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
process.logger.error(
|
||||||
'Parsing Content Security Policy JSON config failed. Make sure it is valid json'
|
'Parsing Content Security Policy JSON config failed. Make sure it is valid json'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error reading HELMET CSP config file', e)
|
process.logger.error('Error reading HELMET CSP config file', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
123
api/src/utils/rateLimiter.ts
Normal file
123
api/src/utils/rateLimiter.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { RateLimiterMemory } from 'rate-limiter-flexible'
|
||||||
|
|
||||||
|
export class RateLimiter {
|
||||||
|
private static instance: RateLimiter
|
||||||
|
private limiterSlowBruteByIP: RateLimiterMemory
|
||||||
|
private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMemory
|
||||||
|
private maxWrongAttemptsByIpPerDay: number
|
||||||
|
private maxConsecutiveFailsByUsernameAndIp: number
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
const {
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
this.maxWrongAttemptsByIpPerDay = Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY)
|
||||||
|
this.maxConsecutiveFailsByUsernameAndIp = Number(
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
)
|
||||||
|
|
||||||
|
this.limiterSlowBruteByIP = new RateLimiterMemory({
|
||||||
|
keyPrefix: 'login_fail_ip_per_day',
|
||||||
|
points: this.maxWrongAttemptsByIpPerDay,
|
||||||
|
duration: 60 * 60 * 24,
|
||||||
|
blockDuration: 60 * 60 * 24 // Block for 1 day
|
||||||
|
})
|
||||||
|
|
||||||
|
this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMemory({
|
||||||
|
keyPrefix: 'login_fail_consecutive_username_and_ip',
|
||||||
|
points: this.maxConsecutiveFailsByUsernameAndIp,
|
||||||
|
duration: 60 * 60 * 24 * 24, // Store number for 24 days since first fail
|
||||||
|
blockDuration: 60 * 60 // Block for 1 hour
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance() {
|
||||||
|
if (!RateLimiter.instance) {
|
||||||
|
RateLimiter.instance = new RateLimiter()
|
||||||
|
}
|
||||||
|
return RateLimiter.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUsernameIPKey(ip: string, username: string) {
|
||||||
|
return `${username}_${ip}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks for brute force attack
|
||||||
|
* If attack is detected then returns the number of seconds after which user can make another request
|
||||||
|
* Else returns 0
|
||||||
|
*/
|
||||||
|
public async check(ip: string, username: string) {
|
||||||
|
const usernameIPkey = this.getUsernameIPKey(ip, username)
|
||||||
|
|
||||||
|
const [resSlowByIP, resUsernameAndIP] = await Promise.all([
|
||||||
|
this.limiterSlowBruteByIP.get(ip),
|
||||||
|
this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
|
||||||
|
])
|
||||||
|
|
||||||
|
// NOTE: To make use of blockDuration option, comparison in both following if statements should have greater than symbol
|
||||||
|
// otherwise, blockDuration option will not work
|
||||||
|
// For more info see: https://github.com/animir/node-rate-limiter-flexible/wiki/Options#blockduration
|
||||||
|
|
||||||
|
// Check if IP or Username + IP is already blocked
|
||||||
|
if (
|
||||||
|
resSlowByIP !== null &&
|
||||||
|
resSlowByIP.consumedPoints > this.maxWrongAttemptsByIpPerDay
|
||||||
|
) {
|
||||||
|
return Math.ceil(resSlowByIP.msBeforeNext / 1000)
|
||||||
|
} else if (
|
||||||
|
resUsernameAndIP !== null &&
|
||||||
|
resUsernameAndIP.consumedPoints > this.maxConsecutiveFailsByUsernameAndIp
|
||||||
|
) {
|
||||||
|
return Math.ceil(resUsernameAndIP.msBeforeNext / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume 1 point from limiters on wrong attempt and block if limits reached
|
||||||
|
* If limit is reached, return the number of seconds after which user can make another request
|
||||||
|
* Else return 0
|
||||||
|
*/
|
||||||
|
public async consume(ip: string, username?: string) {
|
||||||
|
try {
|
||||||
|
const promises = [this.limiterSlowBruteByIP.consume(ip)]
|
||||||
|
if (username) {
|
||||||
|
const usernameIPkey = this.getUsernameIPKey(ip, username)
|
||||||
|
|
||||||
|
// Count failed attempts by Username + IP only for registered users
|
||||||
|
promises.push(
|
||||||
|
this.limiterConsecutiveFailsByUsernameAndIP.consume(usernameIPkey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
} catch (rlRejected: any) {
|
||||||
|
if (rlRejected instanceof Error) {
|
||||||
|
throw rlRejected
|
||||||
|
} else {
|
||||||
|
// based upon the implementation of consume method of RateLimiterMemory
|
||||||
|
// we are sure that rlRejected will contain msBeforeNext
|
||||||
|
// for further reference,
|
||||||
|
// see https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#login-endpoint-protection
|
||||||
|
// or see https://github.com/animir/node-rate-limiter-flexible#ratelimiterres-object
|
||||||
|
return Math.ceil(rlRejected.msBeforeNext / 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resetOnSuccess(ip: string, username: string) {
|
||||||
|
const usernameIPkey = this.getUsernameIPKey(ip, username)
|
||||||
|
const resUsernameAndIP =
|
||||||
|
await this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
|
||||||
|
|
||||||
|
if (resUsernameAndIP !== null && resUsernameAndIP.consumedPoints > 0) {
|
||||||
|
await this.limiterConsecutiveFailsByUsernameAndIP.delete(usernameIPkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import bcrypt from 'bcryptjs'
|
||||||
import Client from '../model/Client'
|
import Client from '../model/Client'
|
||||||
import Group from '../model/Group'
|
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
||||||
import User from '../model/User'
|
import User, { IUser } from '../model/User'
|
||||||
import Configuration, { ConfigurationType } from '../model/Configuration'
|
import Configuration, { ConfigurationType } from '../model/Configuration'
|
||||||
|
import { ResetAdminPasswordType } from './verifyEnvVariables'
|
||||||
|
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
@@ -19,31 +21,46 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
const client = new Client(CLIENT)
|
const client = new Client(CLIENT)
|
||||||
await client.save()
|
await client.save()
|
||||||
|
|
||||||
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
process.logger.success(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checking if 'AllUsers' Group is already in the database
|
// Checking if 'AllUsers' Group is already in the database
|
||||||
let groupExist = await Group.findOne({ name: GROUP.name })
|
let groupExist = await Group.findOne({ name: ALL_USERS_GROUP.name })
|
||||||
if (!groupExist) {
|
if (!groupExist) {
|
||||||
const group = new Group(GROUP)
|
const group = new Group(ALL_USERS_GROUP)
|
||||||
groupExist = await group.save()
|
groupExist = await group.save()
|
||||||
|
|
||||||
console.log(`DB Seed - Group created: ${GROUP.name}`)
|
process.logger.success(`DB Seed - Group created: ${ALL_USERS_GROUP.name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checking if 'Public' Group is already in the database
|
||||||
|
const publicGroupExist = await Group.findOne({ name: PUBLIC_GROUP.name })
|
||||||
|
if (!publicGroupExist) {
|
||||||
|
const group = new Group(PUBLIC_GROUP)
|
||||||
|
await group.save()
|
||||||
|
|
||||||
|
process.logger.success(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ADMIN_USER = getAdminUser()
|
||||||
|
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||||
if (!usernameExist) {
|
if (usernameExist) {
|
||||||
|
usernameExist = await resetAdminPassword(usernameExist, ADMIN_USER.password)
|
||||||
|
} else {
|
||||||
const user = new User(ADMIN_USER)
|
const user = new User(ADMIN_USER)
|
||||||
usernameExist = await user.save()
|
usernameExist = await user.save()
|
||||||
|
|
||||||
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
process.logger.success(
|
||||||
|
`DB Seed - admin account created: ${ADMIN_USER.username}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupExist.hasUser(usernameExist)) {
|
if (usernameExist.isAdmin && !groupExist.hasUser(usernameExist)) {
|
||||||
groupExist.addUser(usernameExist)
|
groupExist.addUser(usernameExist)
|
||||||
console.log(
|
process.logger.success(
|
||||||
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'`
|
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${ALL_USERS_GROUP.name}'`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +70,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
const configuration = new Configuration(SECRETS)
|
const configuration = new Configuration(SECRETS)
|
||||||
configExist = await configuration.save()
|
configExist = await configuration.save()
|
||||||
|
|
||||||
console.log('DB Seed - configuration added')
|
process.logger.success('DB Seed - configuration added')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -64,19 +81,67 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const GROUP = {
|
export const ALL_USERS_GROUP = {
|
||||||
name: 'AllUsers',
|
name: 'AllUsers',
|
||||||
description: 'Group contains all users'
|
description: 'Group contains all users'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PUBLIC_GROUP = {
|
||||||
|
name: PUBLIC_GROUP_NAME,
|
||||||
|
description:
|
||||||
|
'A special group that can be used to bypass authentication for particular routes.'
|
||||||
|
}
|
||||||
|
|
||||||
const CLIENT = {
|
const CLIENT = {
|
||||||
clientId: 'clientID1',
|
clientId: 'clientID1',
|
||||||
clientSecret: 'clientSecret'
|
clientSecret: 'clientSecret'
|
||||||
}
|
}
|
||||||
const ADMIN_USER = {
|
|
||||||
id: 1,
|
const getAdminUser = () => {
|
||||||
displayName: 'Super Admin',
|
const { ADMIN_USERNAME, ADMIN_PASSWORD_INITIAL } = process.env
|
||||||
username: 'secretuser',
|
|
||||||
password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO',
|
const salt = bcrypt.genSaltSync(10)
|
||||||
isAdmin: true,
|
const hashedPassword = bcrypt.hashSync(ADMIN_PASSWORD_INITIAL as string, salt)
|
||||||
isActive: true
|
|
||||||
|
return {
|
||||||
|
displayName: 'Super Admin',
|
||||||
|
username: ADMIN_USERNAME,
|
||||||
|
password: hashedPassword,
|
||||||
|
isAdmin: true,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetAdminPassword = async (user: IUser, password: string) => {
|
||||||
|
const { ADMIN_PASSWORD_RESET } = process.env
|
||||||
|
|
||||||
|
if (ADMIN_PASSWORD_RESET === ResetAdminPasswordType.YES) {
|
||||||
|
if (!user.isAdmin) {
|
||||||
|
process.logger.error(
|
||||||
|
`Can not reset the password of non-admin user (${user.username}) on startup.`
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.authProvider) {
|
||||||
|
process.logger.error(
|
||||||
|
`Can not reset the password of admin (${user.username}) with ${user.authProvider} as authentication mechanism.`
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
process.logger.info(
|
||||||
|
`DB Seed - resetting password for admin user: ${user.username}`
|
||||||
|
)
|
||||||
|
|
||||||
|
user.password = password
|
||||||
|
user.needsToUpdatePassword = true
|
||||||
|
user = await user.save()
|
||||||
|
|
||||||
|
process.logger.success(`DB Seed - successfully reset the password`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export const setProcessVariables = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
process.sasjsRoot = path.join(process.cwd(), 'sasjs_root')
|
||||||
|
process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,28 +29,41 @@ export const setProcessVariables = async () => {
|
|||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
process.sasLoc = process.env.SAS_PATH
|
process.sasLoc = process.env.SAS_PATH
|
||||||
process.nodeLoc = process.env.NODE_PATH
|
process.nodeLoc = process.env.NODE_PATH
|
||||||
|
process.pythonLoc = process.env.PYTHON_PATH
|
||||||
|
process.rLoc = process.env.R_PATH
|
||||||
} else {
|
} else {
|
||||||
const { sasLoc, nodeLoc } = await getDesktopFields()
|
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
|
||||||
|
|
||||||
process.sasLoc = sasLoc
|
process.sasLoc = sasLoc
|
||||||
process.nodeLoc = nodeLoc
|
process.nodeLoc = nodeLoc
|
||||||
|
process.pythonLoc = pythonLoc
|
||||||
|
process.rLoc = rLoc
|
||||||
}
|
}
|
||||||
|
|
||||||
const { SASJS_ROOT } = process.env
|
const { SASJS_ROOT } = process.env
|
||||||
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
||||||
await createFolder(absPath)
|
await createFolder(absPath)
|
||||||
process.driveLoc = getRealPath(absPath)
|
process.sasjsRoot = getRealPath(absPath)
|
||||||
|
|
||||||
|
const { DRIVE_LOCATION } = process.env
|
||||||
|
const absDrivePath = getAbsolutePath(
|
||||||
|
DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'),
|
||||||
|
process.cwd()
|
||||||
|
)
|
||||||
|
await createFolder(absDrivePath)
|
||||||
|
process.driveLoc = getRealPath(absDrivePath)
|
||||||
|
|
||||||
const { LOG_LOCATION } = process.env
|
const { LOG_LOCATION } = process.env
|
||||||
const absLogsPath = getAbsolutePath(
|
const absLogsPath = getAbsolutePath(
|
||||||
LOG_LOCATION ?? `sasjs_root${path.sep}logs`,
|
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
|
||||||
process.cwd()
|
process.cwd()
|
||||||
)
|
)
|
||||||
await createFolder(absLogsPath)
|
await createFolder(absLogsPath)
|
||||||
process.logsLoc = getRealPath(absLogsPath)
|
process.logsLoc = getRealPath(absLogsPath)
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
|
||||||
console.log('sasLogs: ', process.logsLoc)
|
process.logger.info('sasLoc: ', process.sasLoc)
|
||||||
console.log('runTimes: ', process.runTimes)
|
process.logger.info('sasDrive: ', process.driveLoc)
|
||||||
|
process.logger.info('sasLogs: ', process.logsLoc)
|
||||||
|
process.logger.info('runTimes: ', process.runTimes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
import { createFolder } from '@sasjs/utils'
|
||||||
import { getDesktopUserAutoExecPath, getFilesFolder } from './file'
|
import { getFilesFolder, getPackagesFolder } from './file'
|
||||||
import { ModeType } from './verifyEnvVariables'
|
|
||||||
|
|
||||||
export const setupFolders = async () => {
|
export const setupFilesFolder = async () => await createFolder(getFilesFolder())
|
||||||
const drivePath = getFilesFolder()
|
|
||||||
await createFolder(drivePath)
|
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Desktop) {
|
export const setupPackagesFolder = async () =>
|
||||||
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
await createFolder(getPackagesFolder())
|
||||||
await createFile(getDesktopUserAutoExecPath(), '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
11
api/src/utils/setupUserAutoExec.ts
Normal file
11
api/src/utils/setupUserAutoExec.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createFile, fileExists } from '@sasjs/utils'
|
||||||
|
import { getDesktopUserAutoExecPath } from './file'
|
||||||
|
import { ModeType } from './verifyEnvVariables'
|
||||||
|
|
||||||
|
export const setupUserAutoExec = async () => {
|
||||||
|
if (process.env.MODE === ModeType.Desktop) {
|
||||||
|
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
||||||
|
await createFile(getDesktopUserAutoExecPath(), '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -126,9 +126,61 @@ export const generateFileUploadJSCode = async (
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (fileCount) {
|
uploadCode += `\nconst _WEBIN_FILE_COUNT = ${fileCount}`
|
||||||
uploadCode = `\nconst _WEBIN_FILE_COUNT = ${fileCount}` + uploadCode
|
|
||||||
}
|
return uploadCode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the python code that references uploaded files in the concurrent request
|
||||||
|
* @param filesNamesMap object that maps hashed file names and original file names
|
||||||
|
* @param sessionFolder name of the folder that is created for the purpose of files in concurrent request
|
||||||
|
* @returns generated python code
|
||||||
|
*/
|
||||||
|
export const generateFileUploadPythonCode = async (
|
||||||
|
filesNamesMap: FilenamesMap,
|
||||||
|
sessionFolder: string
|
||||||
|
) => {
|
||||||
|
let uploadCode = ''
|
||||||
|
let fileCount = 0
|
||||||
|
|
||||||
|
const sessionFolderList: string[] = await listFilesInFolder(sessionFolder)
|
||||||
|
sessionFolderList.forEach(async (fileName) => {
|
||||||
|
if (fileName.includes('req_file')) {
|
||||||
|
fileCount++
|
||||||
|
uploadCode += `\n_WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
||||||
|
uploadCode += `\n_WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadCode += `\n_WEBIN_FILE_COUNT = ${fileCount}`
|
||||||
|
|
||||||
|
return uploadCode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the R code that references uploaded files in the concurrent request
|
||||||
|
* @param filesNamesMap object that maps hashed file names and original file names
|
||||||
|
* @param sessionFolder name of the folder that is created for the purpose of files in concurrent request
|
||||||
|
* @returns generated python code
|
||||||
|
*/
|
||||||
|
export const generateFileUploadRCode = async (
|
||||||
|
filesNamesMap: FilenamesMap,
|
||||||
|
sessionFolder: string
|
||||||
|
) => {
|
||||||
|
let uploadCode = ''
|
||||||
|
let fileCount = 0
|
||||||
|
|
||||||
|
const sessionFolderList: string[] = await listFilesInFolder(sessionFolder)
|
||||||
|
sessionFolderList.forEach(async (fileName) => {
|
||||||
|
if (fileName.includes('req_file')) {
|
||||||
|
fileCount++
|
||||||
|
uploadCode += `\n._WEBIN_FILENAME${fileCount} <- '${filesNamesMap[fileName].originalName}'`
|
||||||
|
uploadCode += `\n._WEBIN_NAME${fileCount} <- '${filesNamesMap[fileName].fieldName}'`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadCode += `\n._WEBIN_FILE_COUNT <- ${fileCount}`
|
||||||
|
|
||||||
return uploadCode
|
return uploadCode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import Joi from 'joi'
|
import Joi from 'joi'
|
||||||
import { PermissionSetting, PrincipalType } from '../controllers/permission'
|
import {
|
||||||
|
PermissionType,
|
||||||
|
PermissionSettingForRoute,
|
||||||
|
PrincipalType
|
||||||
|
} from '../controllers/permission'
|
||||||
import { getAuthorizedRoutes } from './getAuthorizedRoutes'
|
import { getAuthorizedRoutes } from './getAuthorizedRoutes'
|
||||||
|
|
||||||
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||||
@@ -81,20 +85,31 @@ export const updateUserValidation = (
|
|||||||
return Joi.object(validationChecks).validate(data)
|
return Joi.object(validationChecks).validate(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updatePasswordValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
currentPassword: Joi.string().required(),
|
||||||
|
newPassword: passwordSchema.required()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
clientId: Joi.string().required(),
|
clientId: Joi.string().required(),
|
||||||
clientSecret: Joi.string().required()
|
clientSecret: Joi.string().required(),
|
||||||
|
accessTokenExpiration: Joi.number(),
|
||||||
|
refreshTokenExpiration: Joi.number()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
uri: Joi.string()
|
path: Joi.string()
|
||||||
.required()
|
.required()
|
||||||
.valid(...getAuthorizedRoutes()),
|
.valid(...getAuthorizedRoutes()),
|
||||||
|
type: Joi.string()
|
||||||
|
.required()
|
||||||
|
.valid(...Object.values(PermissionType)),
|
||||||
setting: Joi.string()
|
setting: Joi.string()
|
||||||
.required()
|
.required()
|
||||||
.valid(...Object.values(PermissionSetting)),
|
.valid(...Object.values(PermissionSettingForRoute)),
|
||||||
principalType: Joi.string()
|
principalType: Joi.string()
|
||||||
.required()
|
.required()
|
||||||
.valid(...Object.values(PrincipalType)),
|
.valid(...Object.values(PrincipalType)),
|
||||||
@@ -105,7 +120,7 @@ export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
|
|||||||
Joi.object({
|
Joi.object({
|
||||||
setting: Joi.string()
|
setting: Joi.string()
|
||||||
.required()
|
.required()
|
||||||
.valid(...Object.values(PermissionSetting))
|
.valid(...Object.values(PermissionSettingForRoute))
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const deployValidation = (data: any): Joi.ValidationResult =>
|
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
|
export enum MOCK_SERVERTYPEType {
|
||||||
|
SAS9 = 'sas9',
|
||||||
|
SASVIYA = 'sasviya'
|
||||||
|
}
|
||||||
|
|
||||||
export enum ModeType {
|
export enum ModeType {
|
||||||
Server = 'server',
|
Server = 'server',
|
||||||
Desktop = 'desktop'
|
Desktop = 'desktop'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AuthProviderType {
|
||||||
|
LDAP = 'ldap'
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProtocolType {
|
export enum ProtocolType {
|
||||||
HTTP = 'http',
|
HTTP = 'http',
|
||||||
HTTPS = 'https'
|
HTTPS = 'https'
|
||||||
@@ -28,7 +37,9 @@ export enum LOG_FORMAT_MORGANType {
|
|||||||
|
|
||||||
export enum RunTimeType {
|
export enum RunTimeType {
|
||||||
SAS = 'sas',
|
SAS = 'sas',
|
||||||
JS = 'js'
|
JS = 'js',
|
||||||
|
PY = 'py',
|
||||||
|
R = 'r'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReturnCode {
|
export enum ReturnCode {
|
||||||
@@ -36,9 +47,21 @@ export enum ReturnCode {
|
|||||||
InvalidEnv
|
InvalidEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DatabaseType {
|
||||||
|
MONGO = 'mongodb',
|
||||||
|
COSMOS_MONGODB = 'cosmos_mongodb'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ResetAdminPasswordType {
|
||||||
|
YES = 'YES',
|
||||||
|
NO = 'NO'
|
||||||
|
}
|
||||||
|
|
||||||
export const verifyEnvVariables = (): ReturnCode => {
|
export const verifyEnvVariables = (): ReturnCode => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
|
|
||||||
|
errors.push(...verifyMOCK_SERVERTYPE())
|
||||||
|
|
||||||
errors.push(...verifyMODE())
|
errors.push(...verifyMODE())
|
||||||
|
|
||||||
errors.push(...verifyPROTOCOL())
|
errors.push(...verifyPROTOCOL())
|
||||||
@@ -55,6 +78,14 @@ export const verifyEnvVariables = (): ReturnCode => {
|
|||||||
|
|
||||||
errors.push(...verifyExecutablePaths())
|
errors.push(...verifyExecutablePaths())
|
||||||
|
|
||||||
|
errors.push(...verifyLDAPVariables())
|
||||||
|
|
||||||
|
errors.push(...verifyDbType())
|
||||||
|
|
||||||
|
errors.push(...verifyRateLimiter())
|
||||||
|
|
||||||
|
errors.push(...verifyAdminUserConfig())
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
process.logger?.error(
|
process.logger?.error(
|
||||||
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
|
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
|
||||||
@@ -65,6 +96,23 @@ export const verifyEnvVariables = (): ReturnCode => {
|
|||||||
return ReturnCode.Success
|
return ReturnCode.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verifyMOCK_SERVERTYPE = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { MOCK_SERVERTYPE } = process.env
|
||||||
|
|
||||||
|
if (MOCK_SERVERTYPE) {
|
||||||
|
const modeTypes = Object.values(MOCK_SERVERTYPEType)
|
||||||
|
if (!modeTypes.includes(MOCK_SERVERTYPE as MOCK_SERVERTYPEType))
|
||||||
|
errors.push(
|
||||||
|
`- MOCK_SERVERTYPE '${MOCK_SERVERTYPE}'\n - valid options ${modeTypes}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
process.env.MOCK_SERVERTYPE = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
const verifyMODE = (): string[] => {
|
const verifyMODE = (): string[] => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
@@ -78,13 +126,22 @@ const verifyMODE = (): string[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Server) {
|
if (process.env.MODE === ModeType.Server) {
|
||||||
const { DB_CONNECT } = process.env
|
const { DB_CONNECT, AUTH_PROVIDERS } = process.env
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test')
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
if (!DB_CONNECT)
|
if (!DB_CONNECT)
|
||||||
errors.push(
|
errors.push(
|
||||||
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
|
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (AUTH_PROVIDERS) {
|
||||||
|
const authProvidersType = Object.values(AuthProviderType)
|
||||||
|
if (!authProvidersType.includes(AUTH_PROVIDERS as AuthProviderType))
|
||||||
|
errors.push(
|
||||||
|
`- AUTH_PROVIDERS '${AUTH_PROVIDERS}'\n - valid options ${authProvidersType}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
@@ -125,8 +182,27 @@ const verifyCORS = (): string[] => {
|
|||||||
|
|
||||||
if (CORS) {
|
if (CORS) {
|
||||||
const corsTypes = Object.values(CorsType)
|
const corsTypes = Object.values(CorsType)
|
||||||
|
|
||||||
if (!corsTypes.includes(CORS as CorsType))
|
if (!corsTypes.includes(CORS as CorsType))
|
||||||
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
|
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
|
||||||
|
|
||||||
|
if (CORS === CorsType.ENABLED) {
|
||||||
|
const { WHITELIST } = process.env
|
||||||
|
|
||||||
|
const urls = WHITELIST?.trim()
|
||||||
|
.split(' ')
|
||||||
|
.filter((url) => !!url)
|
||||||
|
if (urls?.length) {
|
||||||
|
urls.forEach((url) => {
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://'))
|
||||||
|
errors.push(
|
||||||
|
`- CORS '${CORS}'\n - provided WHITELIST ${url} is not valid`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
errors.push(`- CORS '${CORS}'\n - provide at least one WHITELIST URL`)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
process.env.CORS =
|
process.env.CORS =
|
||||||
@@ -207,9 +283,10 @@ const verifyRUN_TIMES = (): string[] => {
|
|||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
const verifyExecutablePaths = () => {
|
const verifyExecutablePaths = (): string[] => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
const { RUN_TIMES, SAS_PATH, NODE_PATH, MODE } = process.env
|
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } =
|
||||||
|
process.env
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
const runTimes = RUN_TIMES?.split(',')
|
const runTimes = RUN_TIMES?.split(',')
|
||||||
@@ -221,16 +298,171 @@ const verifyExecutablePaths = () => {
|
|||||||
if (runTimes?.includes(RunTimeType.JS) && !NODE_PATH) {
|
if (runTimes?.includes(RunTimeType.JS) && !NODE_PATH) {
|
||||||
errors.push(`- NODE_PATH is required for ${RunTimeType.JS} run time`)
|
errors.push(`- NODE_PATH is required for ${RunTimeType.JS} run time`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runTimes?.includes(RunTimeType.PY) && !PYTHON_PATH) {
|
||||||
|
errors.push(`- PYTHON_PATH is required for ${RunTimeType.PY} run time`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runTimes?.includes(RunTimeType.R) && !R_PATH) {
|
||||||
|
errors.push(`- R_PATH is required for ${RunTimeType.R} run time`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verifyLDAPVariables = () => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const {
|
||||||
|
LDAP_URL,
|
||||||
|
LDAP_BIND_DN,
|
||||||
|
LDAP_BIND_PASSWORD,
|
||||||
|
LDAP_USERS_BASE_DN,
|
||||||
|
LDAP_GROUPS_BASE_DN,
|
||||||
|
MODE,
|
||||||
|
AUTH_PROVIDERS
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Server && AUTH_PROVIDERS === AuthProviderType.LDAP) {
|
||||||
|
if (!LDAP_URL) {
|
||||||
|
errors.push(
|
||||||
|
`- LDAP_URL is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LDAP_BIND_DN) {
|
||||||
|
errors.push(
|
||||||
|
`- LDAP_BIND_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LDAP_BIND_PASSWORD) {
|
||||||
|
errors.push(
|
||||||
|
`- LDAP_BIND_PASSWORD is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LDAP_USERS_BASE_DN) {
|
||||||
|
errors.push(
|
||||||
|
`- LDAP_USERS_BASE_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LDAP_GROUPS_BASE_DN) {
|
||||||
|
errors.push(
|
||||||
|
`- LDAP_GROUPS_BASE_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyDbType = () => {
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
const { MODE, DB_TYPE } = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
if (DB_TYPE) {
|
||||||
|
const dbTypes = Object.values(DatabaseType)
|
||||||
|
if (!dbTypes.includes(DB_TYPE as DatabaseType))
|
||||||
|
errors.push(`- DB_TYPE '${DB_TYPE}'\n - valid options ${dbTypes}`)
|
||||||
|
} else {
|
||||||
|
process.env.DB_TYPE = DEFAULTS.DB_TYPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyRateLimiter = () => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const {
|
||||||
|
MODE,
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
} = process.env
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
if (MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) {
|
||||||
|
if (
|
||||||
|
!isNumeric(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) ||
|
||||||
|
Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) < 1
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`- Invalid value for 'MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY' - Only positive number is acceptable`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY =
|
||||||
|
DEFAULTS.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) {
|
||||||
|
if (
|
||||||
|
!isNumeric(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) ||
|
||||||
|
Number(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) < 1
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`- Invalid value for 'MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP' - Only positive number is acceptable`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP =
|
||||||
|
DEFAULTS.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyAdminUserConfig = () => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { MODE, ADMIN_USERNAME, ADMIN_PASSWORD_INITIAL, ADMIN_PASSWORD_RESET } =
|
||||||
|
process.env
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
if (ADMIN_USERNAME) {
|
||||||
|
process.env.ADMIN_USERNAME = ADMIN_USERNAME.toLowerCase()
|
||||||
|
} else {
|
||||||
|
process.env.ADMIN_USERNAME = DEFAULTS.ADMIN_USERNAME
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ADMIN_PASSWORD_INITIAL)
|
||||||
|
process.env.ADMIN_PASSWORD_INITIAL = DEFAULTS.ADMIN_PASSWORD_INITIAL
|
||||||
|
|
||||||
|
if (ADMIN_PASSWORD_RESET) {
|
||||||
|
const resetPasswordTypes = Object.values(ResetAdminPasswordType)
|
||||||
|
if (
|
||||||
|
!resetPasswordTypes.includes(
|
||||||
|
ADMIN_PASSWORD_RESET as ResetAdminPasswordType
|
||||||
|
)
|
||||||
|
)
|
||||||
|
errors.push(
|
||||||
|
`- ADMIN_PASSWORD_RESET '${ADMIN_PASSWORD_RESET}'\n - valid options ${resetPasswordTypes}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
process.env.ADMIN_PASSWORD_RESET = DEFAULTS.ADMIN_PASSWORD_RESET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumeric = (val: string): boolean => {
|
||||||
|
return !isNaN(Number(val))
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
MODE: ModeType.Desktop,
|
MODE: ModeType.Desktop,
|
||||||
PROTOCOL: ProtocolType.HTTP,
|
PROTOCOL: ProtocolType.HTTP,
|
||||||
PORT: '5000',
|
PORT: '5000',
|
||||||
HELMET_COEP: HelmetCoepType.TRUE,
|
HELMET_COEP: HelmetCoepType.TRUE,
|
||||||
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
||||||
RUN_TIMES: RunTimeType.SAS
|
RUN_TIMES: RunTimeType.SAS,
|
||||||
|
DB_TYPE: DatabaseType.MONGO,
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY: '100',
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP: '10',
|
||||||
|
ADMIN_USERNAME: 'secretuser',
|
||||||
|
ADMIN_PASSWORD_INITIAL: 'secretpassword',
|
||||||
|
ADMIN_PASSWORD_RESET: ResetAdminPasswordType.NO
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const fetchLatestAutoExec = async (
|
|||||||
displayName: dbUser.displayName,
|
displayName: dbUser.displayName,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isActive: dbUser.isActive,
|
isActive: dbUser.isActive,
|
||||||
|
needsToUpdatePassword: dbUser.needsToUpdatePassword,
|
||||||
autoExec: dbUser.autoExec
|
autoExec: dbUser.autoExec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ export const verifyTokenInDB = async (
|
|||||||
displayName: dbUser.displayName,
|
displayName: dbUser.displayName,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isActive: dbUser.isActive,
|
isActive: dbUser.isActive,
|
||||||
|
needsToUpdatePassword: dbUser.needsToUpdatePassword,
|
||||||
autoExec: dbUser.autoExec
|
autoExec: dbUser.autoExec
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|||||||
@@ -15,12 +15,16 @@
|
|||||||
"name": "Auth",
|
"name": "Auth",
|
||||||
"description": "Operations about auth"
|
"description": "Operations about auth"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Auth_Config",
|
||||||
|
"description": "Operations about external auth providers"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Client",
|
"name": "Client",
|
||||||
"description": "Operations about clients"
|
"description": "Operations about clients"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CODE",
|
"name": "Code",
|
||||||
"description": "Execution of code (various runtimes are supported)"
|
"description": "Execution of code (various runtimes are supported)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
1011
web/package-lock.json
generated
1011
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user