mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
303 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
9eb5f3ca4d | ||
|
|
916947dffa | ||
| 79b7827b7c | |||
| 37e1aa9b61 | |||
| 7e504008b7 | |||
| 5d5a9d3788 | |||
|
|
7c79d6479c | ||
|
|
3e635f422a | ||
|
|
77db14c690 | ||
| b7dff341f0 | |||
| 8a3054e19a | |||
|
|
a531de2adb | ||
|
|
c458d94493 | ||
| 706e228a8e | |||
| 7681722e5a | |||
| 8de032b543 | |||
|
|
998ef213e9 | ||
|
|
f8b0f98678 | ||
| 9640f65264 | |||
| c574b42235 | |||
| 468d1a929d | |||
| 7cdffe30e3 | |||
| 3b1fcb937d | |||
| 3c987c61dd | |||
| 0a780697da | |||
| 83d819df53 | |||
|
|
95df2b21d6 | ||
|
|
accdf914f1 | ||
| 15bdd2d7f0 | |||
| 2ce947d216 | |||
| ce2114e3f6 | |||
| 6c7550286b | |||
| 2360e104bd | |||
| 420a61a5a6 | |||
| 04e0f9efe3 | |||
| 99172cd9ed | |||
| 57daad0c26 | |||
| cc1e4543fc | |||
| 03cb89d14f | |||
| 72140d73c2 | |||
| efcefd2a42 | |||
| 06d7c91fc3 | |||
| 7010a6a120 | |||
| fdcaba9d56 | |||
| 48688a6547 | |||
| 0ce94a553e | |||
| 941917e508 | |||
|
|
5706371ffd | ||
|
|
ce5218a227 | ||
|
|
8b62755f39 | ||
|
|
cb84c3ebbb | ||
|
|
526402fd73 | ||
| 177675bc89 | |||
| 721165ff12 | |||
| 08e0c61e0f | |||
|
|
1b234eb2b1 | ||
|
|
ef25eec11f | ||
| 3e53f70928 | |||
| 0f19384999 | |||
| 63dd6813c0 | |||
| 299512135d | |||
| 6c35412d2f | |||
| 27410bc32b | |||
| 849b2dd468 | |||
|
|
a1a182698e | ||
|
|
4be692b24b | ||
|
|
d2ddd8aaca | ||
|
|
3a45e8f525 | ||
|
|
c0e2f55a7b | ||
|
|
aa027414ed | ||
|
|
8c4c52b1a9 | ||
|
|
ff420434ae | ||
|
|
65e6de9663 | ||
|
|
2e53d43e11 | ||
|
|
3795f748a7 | ||
|
|
e024a92f16 | ||
|
|
92fda183f3 | ||
|
|
6f2e6efd03 | ||
| 30d7a65358 | |||
| 5e930f14d2 | |||
| 9bc68b1cdc | |||
|
|
3b4e9d20d4 | ||
|
|
4a67d0c63a | ||
|
|
dea204e3c5 | ||
|
|
5f9e83759c | ||
|
|
fefe63deb1 | ||
| ddd179bbee | |||
| a10b87930c | |||
| 496247d0b9 | |||
| eeb63b330c | |||
|
|
1108d3dd7b | ||
|
|
7edb47a4cb | ||
|
|
451cb4f6dd | ||
|
|
0b759a5594 | ||
|
|
5338ffb211 | ||
| e42fdd3575 | |||
| b10e932605 | |||
| e54a09db19 | |||
| 4c35e04802 | |||
| b5f595a25c | |||
|
|
a131adbae7 | ||
|
|
a20c3b9719 | ||
|
|
eee3a7b084 | ||
|
|
9c3da56901 | ||
|
|
7e6524d7e4 | ||
|
|
0ea2690616 | ||
|
|
b369759f0f | ||
|
|
ac9a835c5a | ||
|
|
e290751c87 | ||
| e516b7716d | |||
| f3dfc7083f | |||
| 7d916ec3e9 | |||
| 70f279a49c | |||
| 66a3537271 | |||
| ca64c13909 | |||
| 0a73a35547 | |||
| a75edbaa32 | |||
| 4ddfec0403 | |||
| 35439d7d51 | |||
| 907aa485fd | |||
| 888627e1c8 | |||
| 9cb9e2dd33 | |||
| 54d4bf835d | |||
| 67fe298fd5 | |||
| 97ecfdc955 | |||
| 5b319f9ad1 | |||
| be8635ccc5 | |||
| f863b81a7d | |||
| bdf63df1d9 | |||
| 4c6b9c5e93 | |||
|
|
a2d1396057 | ||
|
|
b2f21eb3ac | ||
| fa63dc071b | |||
| e8c21a43b2 | |||
| 1413b18508 | |||
| dfbd155711 | |||
| 4fcc191ce9 | |||
| d000f7508f | |||
| 5652325452 | |||
| 0781ddd64e | |||
| 7be77cc38a | |||
| 98b8a75148 | |||
| 72a3197a06 | |||
| fce05d6959 | |||
| 1aec3abd28 | |||
| 9136c95013 | |||
|
|
89b32e70ff | ||
| 01713440a4 | |||
| 540f54fb77 | |||
| bf906aa544 | |||
| 797c2bcc39 | |||
| 1103ffe07b | |||
| e5200c1000 | |||
| 38a7db8514 | |||
| 39fc908de1 | |||
| be009d5b02 | |||
| 6bea1f7666 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [sasjs]
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,8 @@ node_modules/
|
|||||||
.env*
|
.env*
|
||||||
sas/
|
sas/
|
||||||
sasjs_root/
|
sasjs_root/
|
||||||
|
api/mocks/custom/*
|
||||||
|
!api/mocks/custom/.keep
|
||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
|
|||||||
419
CHANGELOG.md
419
CHANGELOG.md
@@ -1,3 +1,422 @@
|
|||||||
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding ls=max to reduce log size and improve readability ([916947d](https://github.com/sasjs/server/commit/916947dffacd902ff23ac3e899d1bf5ab6238b75))
|
||||||
|
|
||||||
|
## [0.13.1](https://github.com/sasjs/server/compare/v0.13.0...v0.13.1) (2022-07-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding options to prevent unwanted windows on windows. Closes [#244](https://github.com/sasjs/server/issues/244) ([77db14c](https://github.com/sasjs/server/commit/77db14c690e18145d733ac2b0d646ab0dbe4d521))
|
||||||
|
|
||||||
|
# [0.13.0](https://github.com/sasjs/server/compare/v0.12.1...v0.13.0) (2022-07-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* autofocus input field and submit on enter ([7681722](https://github.com/sasjs/server/commit/7681722e5afdc2df0c9eed201b05add3beda92a7))
|
||||||
|
* move api button to user menu ([8de032b](https://github.com/sasjs/server/commit/8de032b5431b47daabcf783c47ff078bf817247d))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add action and command to editor ([706e228](https://github.com/sasjs/server/commit/706e228a8e1924786fd9dc97de387974eda504b1))
|
||||||
|
|
||||||
|
## [0.12.1](https://github.com/sasjs/server/compare/v0.12.0...v0.12.1) (2022-07-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** disable launch icon button when file content is not saved ([c574b42](https://github.com/sasjs/server/commit/c574b4223591c4a6cd3ef5e146ce99cd8f7c9190))
|
||||||
|
* **web:** saveAs functionality fixed in studio page ([3c987c6](https://github.com/sasjs/server/commit/3c987c61ddc258f991e2bf38c1f16a0c4248d6ae))
|
||||||
|
* **web:** show original name as default name in rename file/folder modal ([9640f65](https://github.com/sasjs/server/commit/9640f6526496f3564664ccb1f834d0f659dcad4e))
|
||||||
|
* **web:** webout tab item fixed in studio page ([7cdffe3](https://github.com/sasjs/server/commit/7cdffe30e36e5cad0284f48ea97925958e12704c))
|
||||||
|
* **web:** when no file is selected save the editor content to local storage ([3b1fcb9](https://github.com/sasjs/server/commit/3b1fcb937d06d02ab99c9e8dbe307012d48a7a3a))
|
||||||
|
|
||||||
|
# [0.12.0](https://github.com/sasjs/server/compare/v0.11.5...v0.12.0) (2022-07-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fileTree api response to include an additional attribute isFolder ([0f19384](https://github.com/sasjs/server/commit/0f193849994f1ac8a071afa8f10af5b46f86663d))
|
||||||
|
* remove drive component ([06d7c91](https://github.com/sasjs/server/commit/06d7c91fc34620a954df1fd1c682eff370f79ca6))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add api end point for delete folder ([08e0c61](https://github.com/sasjs/server/commit/08e0c61e0fd7041d6cded6f4d71fbb410e5615ce))
|
||||||
|
* add sidebar(drive) to left of studio ([6c35412](https://github.com/sasjs/server/commit/6c35412d2f5180d4e49b12e616576d8b8dacb7d8))
|
||||||
|
* created api endpoint for adding empty folder in drive ([941917e](https://github.com/sasjs/server/commit/941917e508ece5009135f9dddf99775dd4002f78))
|
||||||
|
* implemented api for renaming file/folder ([fdcaba9](https://github.com/sasjs/server/commit/fdcaba9d56cddea5d56d7de5a172f1bb49be3db5))
|
||||||
|
* implemented delete file/folder functionality ([177675b](https://github.com/sasjs/server/commit/177675bc897416f7994dd849dc7bb11ba072efe9))
|
||||||
|
* implemented functionality for adding file/folder from sidebar context menu ([0ce94a5](https://github.com/sasjs/server/commit/0ce94a553e53bfcdbd6273b26b322095a080a341))
|
||||||
|
* implemented the functionality for renaming file/folder from context menu ([7010a6a](https://github.com/sasjs/server/commit/7010a6a1201720d0eb4093267a344fb828b90a2f))
|
||||||
|
* prevent user from leaving studio page when there are unsaved changes ([6c75502](https://github.com/sasjs/server/commit/6c7550286b5f505e9dfe8ca63c62fa1db1b60b2e))
|
||||||
|
* **web:** add difference view editor in studio ([420a61a](https://github.com/sasjs/server/commit/420a61a5a6b11dcb5eb0a652ea9cecea5c3bee5f))
|
||||||
|
|
||||||
|
## [0.11.5](https://github.com/sasjs/server/compare/v0.11.4...v0.11.5) (2022-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Revert "fix(security): missing cookie flags are added" ([ce5218a](https://github.com/sasjs/server/commit/ce5218a2278cc750f2b1032024685dc6cd72f796))
|
||||||
|
|
||||||
|
## [0.11.4](https://github.com/sasjs/server/compare/v0.11.3...v0.11.4) (2022-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **security:** missing cookie flags are added ([526402f](https://github.com/sasjs/server/commit/526402fd73407ee4fa2d31092111a7e6a1741487))
|
||||||
|
|
||||||
|
## [0.11.3](https://github.com/sasjs/server/compare/v0.11.2...v0.11.3) (2022-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* filePath fix in code.js file for windows ([2995121](https://github.com/sasjs/server/commit/299512135d77c2ac9e34853cf35aee6f2e1d4da4))
|
||||||
|
|
||||||
|
## [0.11.2](https://github.com/sasjs/server/compare/v0.11.1...v0.11.2) (2022-07-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* apply icon option only for sas.exe ([d2ddd8a](https://github.com/sasjs/server/commit/d2ddd8aacadfdd143026881f2c6ae8c6b277610a))
|
||||||
|
|
||||||
|
## [0.11.1](https://github.com/sasjs/server/compare/v0.11.0...v0.11.1) (2022-07-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bank operator ([aa02741](https://github.com/sasjs/server/commit/aa027414ed3ce51f1014ef36c4191e064b2e963d))
|
||||||
|
* ensuring nosplash option only applies for sas.exe ([65e6de9](https://github.com/sasjs/server/commit/65e6de966383fe49a919b1f901d77c7f1e402c9b)), closes [#229](https://github.com/sasjs/server/issues/229)
|
||||||
|
|
||||||
|
# [0.11.0](https://github.com/sasjs/server/compare/v0.10.0...v0.11.0) (2022-07-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **logs:** logs location is configurable ([e024a92](https://github.com/sasjs/server/commit/e024a92f165990e08db8aa26ee326dbcb30e2e46))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **logs:** logs to file with rotating + code split into files ([92fda18](https://github.com/sasjs/server/commit/92fda183f3f0f3956b7c791669eb8dd52c389d1b))
|
||||||
|
|
||||||
|
# [0.10.0](https://github.com/sasjs/server/compare/v0.9.0...v0.10.0) (2022-07-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add authorize middleware for appStreams ([e54a09d](https://github.com/sasjs/server/commit/e54a09db19ec8690e54a40760531a4e06d250974))
|
||||||
|
* add isAdmin attribute to return response of get session and login requests ([bdf63df](https://github.com/sasjs/server/commit/bdf63df1d915892486005ec904807749786b1c0c))
|
||||||
|
* add permission authorization middleware to only specific routes ([f3dfc70](https://github.com/sasjs/server/commit/f3dfc7083fbfb4b447521341b1a86730fb90b4c0))
|
||||||
|
* bumping core and running lint ([a2d1396](https://github.com/sasjs/server/commit/a2d13960578014312d2cb5e03145bfd1829d99ec))
|
||||||
|
* controller fixed for deleting permission ([b5f595a](https://github.com/sasjs/server/commit/b5f595a25c50550d62482409353c7629c5a5c3e0))
|
||||||
|
* do not show admin users in add permission modal ([a75edba](https://github.com/sasjs/server/commit/a75edbaa327ec2af49523c13996ac283061da7d8))
|
||||||
|
* export GroupResponse interface ([38a7db8](https://github.com/sasjs/server/commit/38a7db8514de0acd94d74ba96bc1efb732add30c))
|
||||||
|
* move permission filter modal to separate file and icons for different actions ([d000f75](https://github.com/sasjs/server/commit/d000f7508f6d7384afffafee4179151fca802ca8))
|
||||||
|
* principalId type changed to number from any ([4fcc191](https://github.com/sasjs/server/commit/4fcc191ce9edc7e4dcd8821fb8019f4eea5db4ea))
|
||||||
|
* remove clientId from principal types ([0781ddd](https://github.com/sasjs/server/commit/0781ddd64e3b5e5ca39647bb4e4e1a9332a0f4f8))
|
||||||
|
* remove duplicates principals from permission filter modal ([5b319f9](https://github.com/sasjs/server/commit/5b319f9ad1f941b306db6b9473a2128b2e42bf76))
|
||||||
|
* show loading spinner in studio while executing code ([496247d](https://github.com/sasjs/server/commit/496247d0b9975097a008cf4d3a999d77648fd930))
|
||||||
|
* show permission component only in server mode ([f863b81](https://github.com/sasjs/server/commit/f863b81a7d40a1296a061ec93946f204382af2c3))
|
||||||
|
* update permission model ([39fc908](https://github.com/sasjs/server/commit/39fc908de1945f2aaea18d14e6bce703f6bf0c06))
|
||||||
|
* update permission response ([e516b77](https://github.com/sasjs/server/commit/e516b7716da5ff7e23350a5f77cfa073b1171175))
|
||||||
|
* **web:** only admin should be able to add, update or delete permission ([be8635c](https://github.com/sasjs/server/commit/be8635ccc5eb34c3f0a5951c8a0421292ef69c97))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add api endpoint for deleting permission ([0171344](https://github.com/sasjs/server/commit/01713440a4fa661b76368785c0ca731f096ac70a))
|
||||||
|
* add api endpoint for updating permission setting ([540f54f](https://github.com/sasjs/server/commit/540f54fb77b364822da7889dbe75c02242f48a59))
|
||||||
|
* add authorize middleware for validating permissions ([7d916ec](https://github.com/sasjs/server/commit/7d916ec3e9ef579dde1b73015715cd01098c2018))
|
||||||
|
* add basic UI for settings and permissions ([5652325](https://github.com/sasjs/server/commit/56523254525a66e756196e90b39a2b8cdadc1518))
|
||||||
|
* add documentation link under usename dropdown menu ([eeb63b3](https://github.com/sasjs/server/commit/eeb63b330c292afcdd5c8f006882b224c4235068))
|
||||||
|
* add permission model ([6bea1f7](https://github.com/sasjs/server/commit/6bea1f76668ddb070ad95b3e02c31238af67c346))
|
||||||
|
* add UI for updating permission ([e8c21a4](https://github.com/sasjs/server/commit/e8c21a43b215f5fced0463b70747cda1191a4e01))
|
||||||
|
* add validation for registering permission ([e5200c1](https://github.com/sasjs/server/commit/e5200c1000903185dfad9ee49c99583e473c4388))
|
||||||
|
* add, remove and update permissions from web component ([97ecfdc](https://github.com/sasjs/server/commit/97ecfdc95563c72dbdecaebcb504e5194250a763))
|
||||||
|
* added get authorizedRoutes api endpoint ([b10e932](https://github.com/sasjs/server/commit/b10e9326058193dd65a57fab2d2f05b7b06096e7))
|
||||||
|
* created modal for adding permission ([1413b18](https://github.com/sasjs/server/commit/1413b1850838ecc988ab289da4541bde36a9a346))
|
||||||
|
* defined register permission and get all permissions api endpoints ([1103ffe](https://github.com/sasjs/server/commit/1103ffe07b88496967cb03683b08f058ca3bbb9f))
|
||||||
|
* update swagger docs ([797c2bc](https://github.com/sasjs/server/commit/797c2bcc39005a05a995be15a150d584fecae259))
|
||||||
|
|
||||||
|
# [0.9.0](https://github.com/sasjs/server/compare/v0.8.3...v0.9.0) (2022-07-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* removed secrets from env variables ([9c3da56](https://github.com/sasjs/server/commit/9c3da56901672a818f54267f9defc9f4701ab7fb))
|
||||||
|
|
||||||
|
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deploy:** extract first json from zip file ([e290751](https://github.com/sasjs/server/commit/e290751c872d24009482871a8c398e834357dcde))
|
||||||
|
|
||||||
## [0.8.2](https://github.com/sasjs/server/compare/v0.8.1...v0.8.2) (2022-06-22)
|
## [0.8.2](https://github.com/sasjs/server/compare/v0.8.1...v0.8.2) (2022-06-22)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -64,12 +64,30 @@ 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
|
||||||
@@ -81,6 +99,9 @@ PROTOCOL=
|
|||||||
# default: 5000
|
# default: 5000
|
||||||
PORT=
|
PORT=
|
||||||
|
|
||||||
|
# options: [sas9|sasviya]
|
||||||
|
# If not present, mocking function is disabled
|
||||||
|
MOCK_SERVERTYPE=
|
||||||
|
|
||||||
#
|
#
|
||||||
## Additional SAS Options
|
## Additional SAS Options
|
||||||
@@ -104,13 +125,19 @@ 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`
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
|
||||||
REFRESH_TOKEN_SECRET=<secret>
|
|
||||||
AUTH_CODE_SECRET=<secret>
|
|
||||||
SESSION_SECRET=<secret>
|
|
||||||
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
|
||||||
|
|
||||||
|
# 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=
|
||||||
@@ -140,12 +167,8 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json
|
|||||||
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
||||||
LOG_FORMAT_MORGAN=
|
LOG_FORMAT_MORGAN=
|
||||||
|
|
||||||
# A comma separated string that defines the available runTimes.
|
# This location is for server logs with classical UNIX logrotate behavior
|
||||||
# Priority is given to the runtime that comes first in the string.
|
LOG_LOCATION=./sasjs_root/logs
|
||||||
# Possible options at the moment are sas and js
|
|
||||||
|
|
||||||
# options: [sas,js|js,sas|sas|js] default:sas
|
|
||||||
RUN_TIMES=
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,23 @@ PORT=[5000] default value is 5000
|
|||||||
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
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
|
||||||
|
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
|
||||||
REFRESH_TOKEN_SECRET=<secret>
|
|
||||||
AUTH_CODE_SECRET=<secret>
|
|
||||||
SESSION_SECRET=<secret>
|
|
||||||
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
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
LOG_FORMAT_MORGAN=common
|
LOG_FORMAT_MORGAN=common
|
||||||
|
LOG_LOCATION=./sasjs_root/logs
|
||||||
0
api/mocks/custom/.keep
Normal file
0
api/mocks/custom/.keep
Normal file
1
api/mocks/generic/sas9/logged-in
Normal file
1
api/mocks/generic/sas9/logged-in
Normal file
@@ -0,0 +1 @@
|
|||||||
|
You have signed in.
|
||||||
1
api/mocks/generic/sas9/logged-out
Normal file
1
api/mocks/generic/sas9/logged-out
Normal file
@@ -0,0 +1 @@
|
|||||||
|
You have signed out.
|
||||||
30
api/mocks/generic/sas9/login
Normal file
30
api/mocks/generic/sas9/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="LT-8-WGkt9EXwICBihaVbxGc92opjufTK1D" 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/generic/sas9/public-access-denied
Normal file
1
api/mocks/generic/sas9/public-access-denied
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Public access has been denied.
|
||||||
1
api/mocks/generic/sas9/sas-stored-process
Normal file
1
api/mocks/generic/sas9/sas-stored-process
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"title": "Log Off SAS Demo User"
|
||||||
2234
api/package-lock.json
generated
2234
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,10 +4,10 @@
|
|||||||
"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": "nodemon ./src/server.ts",
|
"start": "NODE_ENV=development nodemon ./src/server.ts",
|
||||||
"start:prod": "node ./build/src/server.js",
|
"start:prod": "node ./build/src/server.js",
|
||||||
"build": "rimraf build && tsc",
|
"build": "rimraf build && tsc",
|
||||||
"postbuild": "npm run copy:files",
|
"postbuild": "npm run copy:files",
|
||||||
@@ -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.27.3",
|
"@sasjs/core": "^4.40.1",
|
||||||
"@sasjs/utils": "2.42.1",
|
"@sasjs/utils": "2.48.1",
|
||||||
"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",
|
||||||
|
"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",
|
||||||
"url": "^0.10.3"
|
"url": "^0.10.3"
|
||||||
@@ -72,11 +74,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",
|
||||||
@@ -85,10 +87,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.0.0",
|
||||||
|
"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",
|
||||||
|
|||||||
@@ -47,41 +47,6 @@ components:
|
|||||||
- userId
|
- userId
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
LoginPayload:
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: 'Username for user'
|
|
||||||
example: secretuser
|
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
description: 'Password for user'
|
|
||||||
example: secretpassword
|
|
||||||
required:
|
|
||||||
- username
|
|
||||||
- password
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
AuthorizeResponse:
|
|
||||||
properties:
|
|
||||||
code:
|
|
||||||
type: string
|
|
||||||
description: 'Authorization code'
|
|
||||||
example: someRandomCryptoString
|
|
||||||
required:
|
|
||||||
- code
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
AuthorizePayload:
|
|
||||||
properties:
|
|
||||||
clientId:
|
|
||||||
type: string
|
|
||||||
description: 'Client ID'
|
|
||||||
example: clientID1
|
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
ClientPayload:
|
ClientPayload:
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
clientId:
|
||||||
@@ -97,52 +62,12 @@ components:
|
|||||||
- clientSecret
|
- clientSecret
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
IRecordOfAny:
|
|
||||||
properties: {}
|
|
||||||
type: object
|
|
||||||
additionalProperties: {}
|
|
||||||
LogLine:
|
|
||||||
properties:
|
|
||||||
line:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- line
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
HTTPHeaders:
|
|
||||||
properties: {}
|
|
||||||
type: object
|
|
||||||
additionalProperties:
|
|
||||||
type: string
|
|
||||||
ExecuteReturnJsonResponse:
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
_webout:
|
|
||||||
anyOf:
|
|
||||||
-
|
|
||||||
type: string
|
|
||||||
-
|
|
||||||
$ref: '#/components/schemas/IRecordOfAny'
|
|
||||||
log:
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/LogLine'
|
|
||||||
type: array
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
httpHeaders:
|
|
||||||
$ref: '#/components/schemas/HTTPHeaders'
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
- _webout
|
|
||||||
- log
|
|
||||||
- httpHeaders
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
RunTimeType:
|
RunTimeType:
|
||||||
enum:
|
enum:
|
||||||
- sas
|
- sas
|
||||||
- js
|
- js
|
||||||
|
- py
|
||||||
|
- r
|
||||||
type: string
|
type: string
|
||||||
ExecuteCodePayload:
|
ExecuteCodePayload:
|
||||||
properties:
|
properties:
|
||||||
@@ -265,7 +190,7 @@ components:
|
|||||||
- fileTree
|
- fileTree
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
UpdateFileResponse:
|
FileFolderResponse:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
@@ -275,6 +200,31 @@ components:
|
|||||||
- status
|
- status
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
AddFolderPayload:
|
||||||
|
properties:
|
||||||
|
folderPath:
|
||||||
|
type: string
|
||||||
|
description: 'Location of folder'
|
||||||
|
example: /Public/someFolder
|
||||||
|
required:
|
||||||
|
- folderPath
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
RenamePayload:
|
||||||
|
properties:
|
||||||
|
oldPath:
|
||||||
|
type: string
|
||||||
|
description: 'Old path of file/folder'
|
||||||
|
example: /Public/someFolder
|
||||||
|
newPath:
|
||||||
|
type: string
|
||||||
|
description: 'New path of file/folder'
|
||||||
|
example: /Public/newFolder
|
||||||
|
required:
|
||||||
|
- oldPath
|
||||||
|
- newPath
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
TreeNode:
|
TreeNode:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
@@ -283,6 +233,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
absolutePath:
|
absolutePath:
|
||||||
type: string
|
type: string
|
||||||
|
isFolder:
|
||||||
|
type: boolean
|
||||||
children:
|
children:
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/TreeNode'
|
$ref: '#/components/schemas/TreeNode'
|
||||||
@@ -291,6 +243,7 @@ components:
|
|||||||
- name
|
- name
|
||||||
- relativePath
|
- relativePath
|
||||||
- absolutePath
|
- absolutePath
|
||||||
|
- isFolder
|
||||||
- children
|
- children
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
@@ -314,10 +267,13 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
displayName:
|
displayName:
|
||||||
type: string
|
type: string
|
||||||
|
isAdmin:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- username
|
- username
|
||||||
- displayName
|
- displayName
|
||||||
|
- isAdmin
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
GroupResponse:
|
GroupResponse:
|
||||||
@@ -435,27 +391,13 @@ components:
|
|||||||
- description
|
- description
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
_LeanDocument__LeanDocument_T__:
|
FlattenMaps_T_:
|
||||||
properties: {}
|
properties: {}
|
||||||
type: object
|
type: object
|
||||||
Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__:
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
description: 'The string version of this documents _id.'
|
|
||||||
_id:
|
|
||||||
$ref: '#/components/schemas/_LeanDocument__LeanDocument_T__'
|
|
||||||
description: 'This documents _id.'
|
|
||||||
__v:
|
|
||||||
description: 'This documents __v.'
|
|
||||||
type: object
|
|
||||||
description: 'From T, pick a set of properties whose keys are in the union K'
|
|
||||||
Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_:
|
|
||||||
$ref: '#/components/schemas/Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__'
|
|
||||||
description: 'Construct a type with the properties of T except for those in type K.'
|
|
||||||
LeanDocument_this_:
|
|
||||||
$ref: '#/components/schemas/Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_'
|
|
||||||
IGroup:
|
IGroup:
|
||||||
$ref: '#/components/schemas/LeanDocument_this_'
|
$ref: '#/components/schemas/FlattenMaps_T_'
|
||||||
|
ObjectId:
|
||||||
|
type: string
|
||||||
InfoResponse:
|
InfoResponse:
|
||||||
properties:
|
properties:
|
||||||
mode:
|
mode:
|
||||||
@@ -480,7 +422,94 @@ components:
|
|||||||
- runTimes
|
- runTimes
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
ExecuteReturnJsonPayload:
|
AuthorizedRoutesResponse:
|
||||||
|
properties:
|
||||||
|
paths:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- paths
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
PermissionDetailsResponse:
|
||||||
|
properties:
|
||||||
|
permissionId:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
setting:
|
||||||
|
type: string
|
||||||
|
user:
|
||||||
|
$ref: '#/components/schemas/UserResponse'
|
||||||
|
group:
|
||||||
|
$ref: '#/components/schemas/GroupDetailsResponse'
|
||||||
|
required:
|
||||||
|
- permissionId
|
||||||
|
- path
|
||||||
|
- type
|
||||||
|
- setting
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
PermissionType:
|
||||||
|
enum:
|
||||||
|
- Route
|
||||||
|
type: string
|
||||||
|
PermissionSettingForRoute:
|
||||||
|
enum:
|
||||||
|
- Grant
|
||||||
|
- Deny
|
||||||
|
type: string
|
||||||
|
PrincipalType:
|
||||||
|
enum:
|
||||||
|
- user
|
||||||
|
- group
|
||||||
|
type: string
|
||||||
|
RegisterPermissionPayload:
|
||||||
|
properties:
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
description: 'Name of affected resource'
|
||||||
|
example: /SASjsApi/code/execute
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/PermissionType'
|
||||||
|
description: 'Type of affected resource'
|
||||||
|
example: Route
|
||||||
|
setting:
|
||||||
|
$ref: '#/components/schemas/PermissionSettingForRoute'
|
||||||
|
description: 'The indication of whether (and to what extent) access is provided'
|
||||||
|
example: Grant
|
||||||
|
principalType:
|
||||||
|
$ref: '#/components/schemas/PrincipalType'
|
||||||
|
description: 'Indicates the type of principal'
|
||||||
|
example: user
|
||||||
|
principalId:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: 'The id of user or group to which a rule is assigned.'
|
||||||
|
example: 123
|
||||||
|
required:
|
||||||
|
- path
|
||||||
|
- type
|
||||||
|
- setting
|
||||||
|
- principalType
|
||||||
|
- principalId
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
UpdatePermissionPayload:
|
||||||
|
properties:
|
||||||
|
setting:
|
||||||
|
$ref: '#/components/schemas/PermissionSettingForRoute'
|
||||||
|
description: 'The indication of whether (and to what extent) access is provided'
|
||||||
|
example: Grant
|
||||||
|
required:
|
||||||
|
- setting
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
ExecutePostRequestPayload:
|
||||||
properties:
|
properties:
|
||||||
_program:
|
_program:
|
||||||
type: string
|
type: string
|
||||||
@@ -488,6 +517,41 @@ components:
|
|||||||
example: /Public/somefolder/some.file
|
example: /Public/somefolder/some.file
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
LoginPayload:
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: 'Username for user'
|
||||||
|
example: secretuser
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
description: 'Password for user'
|
||||||
|
example: secretpassword
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
AuthorizeResponse:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: 'Authorization code'
|
||||||
|
example: someRandomCryptoString
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
AuthorizePayload:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
type: string
|
||||||
|
description: 'Client ID'
|
||||||
|
example: clientID1
|
||||||
|
required:
|
||||||
|
- clientId
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
type: http
|
type: http
|
||||||
@@ -558,24 +622,28 @@ paths:
|
|||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters: []
|
||||||
/:
|
/SASjsApi/authConfig:
|
||||||
get:
|
get:
|
||||||
operationId: Home
|
operationId: GetDetail
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Ok
|
description: Ok
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema: {}
|
||||||
type: string
|
examples:
|
||||||
summary: 'Render index.html'
|
'Example 1':
|
||||||
|
value: {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'}}
|
||||||
|
summary: 'Gives the detail of Auth Mechanism.'
|
||||||
tags:
|
tags:
|
||||||
- Web
|
- Auth_Config
|
||||||
security: []
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
parameters: []
|
parameters: []
|
||||||
/SASLogon/login:
|
/SASjsApi/authConfig/synchroniseWithLDAP:
|
||||||
post:
|
post:
|
||||||
operationId: Login
|
operationId: SynchroniseWithLDAP
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Ok
|
description: Ok
|
||||||
@@ -583,60 +651,21 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
properties:
|
properties:
|
||||||
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
|
groupCount: {type: number, format: double}
|
||||||
loggedIn: {type: boolean}
|
userCount: {type: number, format: double}
|
||||||
required:
|
required:
|
||||||
- user
|
- groupCount
|
||||||
- loggedIn
|
- userCount
|
||||||
type: object
|
type: object
|
||||||
summary: 'Accept a valid username/password'
|
|
||||||
tags:
|
|
||||||
- Web
|
|
||||||
security: []
|
|
||||||
parameters: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/LoginPayload'
|
|
||||||
/SASLogon/authorize:
|
|
||||||
post:
|
|
||||||
operationId: Authorize
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/AuthorizeResponse'
|
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {code: someRandomCryptoString}
|
value: {users: 5, groups: 3}
|
||||||
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
summary: 'Synchronises LDAP users and groups with internal DB and returns the count of imported users and groups.'
|
||||||
tags:
|
tags:
|
||||||
- Web
|
- Auth_Config
|
||||||
security: []
|
security:
|
||||||
parameters: []
|
-
|
||||||
requestBody:
|
bearerAuth: []
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/AuthorizePayload'
|
|
||||||
/SASLogon/logout:
|
|
||||||
get:
|
|
||||||
operationId: Logout
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema: {}
|
|
||||||
summary: 'Destroy the session stored in cookies'
|
|
||||||
tags:
|
|
||||||
- Web
|
|
||||||
security: []
|
|
||||||
parameters: []
|
parameters: []
|
||||||
/SASjsApi/client:
|
/SASjsApi/client:
|
||||||
post:
|
post:
|
||||||
@@ -673,11 +702,13 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
|
anyOf:
|
||||||
description: 'Execute SAS code.'
|
- {type: string}
|
||||||
summary: 'Run SAS Code and returns log'
|
- {type: string, format: byte}
|
||||||
|
description: 'Execute Code on the Specified Runtime'
|
||||||
|
summary: 'Run Code and Return Webout Content and Log'
|
||||||
tags:
|
tags:
|
||||||
- CODE
|
- Code
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
@@ -838,7 +869,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UpdateFileResponse'
|
$ref: '#/components/schemas/FileFolderResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: success}
|
value: {status: success}
|
||||||
@@ -847,7 +878,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UpdateFileResponse'
|
$ref: '#/components/schemas/FileFolderResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: failure, message: 'File request failed.'}
|
value: {status: failure, message: 'File request failed.'}
|
||||||
@@ -860,7 +891,7 @@ paths:
|
|||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
-
|
-
|
||||||
description: 'Location of SAS program'
|
description: 'Location of file'
|
||||||
in: query
|
in: query
|
||||||
name: _filePath
|
name: _filePath
|
||||||
required: false
|
required: false
|
||||||
@@ -889,7 +920,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UpdateFileResponse'
|
$ref: '#/components/schemas/FileFolderResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: success}
|
value: {status: success}
|
||||||
@@ -898,7 +929,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UpdateFileResponse'
|
$ref: '#/components/schemas/FileFolderResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: failure, message: 'File request failed.'}
|
value: {status: failure, message: 'File request failed.'}
|
||||||
@@ -962,6 +993,102 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: /Public/somefolder
|
example: /Public/somefolder
|
||||||
|
delete:
|
||||||
|
operationId: DeleteFolder
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
status: {type: string}
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
type: object
|
||||||
|
summary: 'Delete folder from SASjs Drive'
|
||||||
|
tags:
|
||||||
|
- Drive
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
in: query
|
||||||
|
name: _folderPath
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder/
|
||||||
|
post:
|
||||||
|
operationId: AddFolder
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/FileFolderResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {status: success}
|
||||||
|
'409':
|
||||||
|
description: 'Folder already exists'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/FileFolderResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {status: failure, message: 'Add folder request failed.'}
|
||||||
|
summary: 'Create an empty folder in SASjs Drive'
|
||||||
|
tags:
|
||||||
|
- Drive
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AddFolderPayload'
|
||||||
|
/SASjsApi/drive/rename:
|
||||||
|
post:
|
||||||
|
operationId: Rename
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/FileFolderResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {status: success}
|
||||||
|
'409':
|
||||||
|
description: 'Folder already exists'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/FileFolderResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {status: failure, message: 'rename request failed.'}
|
||||||
|
summary: 'Renames a file/folder in SASjs Drive'
|
||||||
|
tags:
|
||||||
|
- Drive
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/RenamePayload'
|
||||||
/SASjsApi/drive/filetree:
|
/SASjsApi/drive/filetree:
|
||||||
get:
|
get:
|
||||||
operationId: GetFileTree
|
operationId: GetFileTree
|
||||||
@@ -993,7 +1120,7 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: [{id: 123, username: johnusername, displayName: John}, {id: 456, username: starkusername, displayName: Stark}]
|
value: [{id: 123, username: johnusername, displayName: John, isAdmin: false}, {id: 456, username: starkusername, displayName: Stark, isAdmin: true}]
|
||||||
summary: 'Get list of all users (username, displayname). All users can request this.'
|
summary: 'Get list of all users (username, displayname). All users can request this.'
|
||||||
tags:
|
tags:
|
||||||
- User
|
- User
|
||||||
@@ -1312,7 +1439,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
allOf:
|
allOf:
|
||||||
- {$ref: '#/components/schemas/IGroup'}
|
- {$ref: '#/components/schemas/IGroup'}
|
||||||
- {properties: {_id: {}}, required: [_id], type: object}
|
- {properties: {_id: {$ref: '#/components/schemas/ObjectId'}}, required: [_id], type: object}
|
||||||
summary: 'Delete a group. Admin task only.'
|
summary: 'Delete a group. Admin task only.'
|
||||||
tags:
|
tags:
|
||||||
- Group
|
- Group
|
||||||
@@ -1422,6 +1549,128 @@ paths:
|
|||||||
- Info
|
- Info
|
||||||
security: []
|
security: []
|
||||||
parameters: []
|
parameters: []
|
||||||
|
/SASjsApi/info/authorizedRoutes:
|
||||||
|
get:
|
||||||
|
operationId: AuthorizedRoutes
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthorizedRoutesResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {paths: [/AppStream, /SASjsApi/stp/execute]}
|
||||||
|
summary: 'Get the list of available routes to which permissions can be applied. Used to populate the dialog in the URI Permissions feature.'
|
||||||
|
tags:
|
||||||
|
- Info
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
/SASjsApi/permission:
|
||||||
|
get:
|
||||||
|
operationId: GetAllPermissions
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/PermissionDetailsResponse'
|
||||||
|
type: array
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: [{permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}, {permissionId: 124, path: /SASjsApi/code/execute, type: Route, setting: Grant, group: {groupId: 1, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}}]
|
||||||
|
description: "Get the list of permission rules applicable the authenticated user.\nIf 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.'
|
||||||
|
tags:
|
||||||
|
- Permission
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
post:
|
||||||
|
operationId: CreatePermission
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PermissionDetailsResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
|
||||||
|
summary: 'Create a new permission. Admin only.'
|
||||||
|
tags:
|
||||||
|
- Permission
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/RegisterPermissionPayload'
|
||||||
|
'/SASjsApi/permission/{permissionId}':
|
||||||
|
patch:
|
||||||
|
operationId: UpdatePermission
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PermissionDetailsResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
|
||||||
|
summary: 'Update permission setting. Admin only'
|
||||||
|
tags:
|
||||||
|
- Permission
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
description: 'The permission''s identifier'
|
||||||
|
in: path
|
||||||
|
name: permissionId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
format: double
|
||||||
|
type: number
|
||||||
|
example: 1234
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UpdatePermissionPayload'
|
||||||
|
delete:
|
||||||
|
operationId: DeletePermission
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: 'No content'
|
||||||
|
summary: 'Delete a permission. Admin only.'
|
||||||
|
tags:
|
||||||
|
- Permission
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
description: 'The user''s identifier'
|
||||||
|
in: path
|
||||||
|
name: permissionId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
format: double
|
||||||
|
type: number
|
||||||
|
example: 1234
|
||||||
/SASjsApi/session:
|
/SASjsApi/session:
|
||||||
get:
|
get:
|
||||||
operationId: Session
|
operationId: Session
|
||||||
@@ -1434,7 +1683,7 @@ paths:
|
|||||||
$ref: '#/components/schemas/UserResponse'
|
$ref: '#/components/schemas/UserResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {id: 123, username: johnusername, displayName: John}
|
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
|
||||||
summary: 'Get session info (username).'
|
summary: 'Get session info (username).'
|
||||||
tags:
|
tags:
|
||||||
- Session
|
- Session
|
||||||
@@ -1444,7 +1693,7 @@ paths:
|
|||||||
parameters: []
|
parameters: []
|
||||||
/SASjsApi/stp/execute:
|
/SASjsApi/stp/execute:
|
||||||
get:
|
get:
|
||||||
operationId: ExecuteReturnRaw
|
operationId: ExecuteGetRequest
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Ok
|
description: Ok
|
||||||
@@ -1454,8 +1703,8 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- {type: string}
|
- {type: string}
|
||||||
- {type: string, format: byte}
|
- {type: string, format: byte}
|
||||||
description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||||
summary: 'Execute a Stored Program, returns raw _webout content.'
|
summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
|
||||||
tags:
|
tags:
|
||||||
- STP
|
- STP
|
||||||
security:
|
security:
|
||||||
@@ -1463,7 +1712,7 @@ paths:
|
|||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
-
|
-
|
||||||
description: 'Location of SAS or JS code'
|
description: 'Location of code in SASjs Drive'
|
||||||
in: query
|
in: query
|
||||||
name: _program
|
name: _program
|
||||||
required: true
|
required: true
|
||||||
@@ -1471,19 +1720,18 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
example: /Projects/myApp/some/program
|
example: /Projects/myApp/some/program
|
||||||
post:
|
post:
|
||||||
operationId: ExecuteReturnJson
|
operationId: ExecutePostRequest
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Ok
|
description: Ok
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
|
anyOf:
|
||||||
examples:
|
- {type: string}
|
||||||
'Example 1':
|
- {type: string, format: byte}
|
||||||
value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}}
|
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||||
description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms\n\nThe response will be a JSON object with the following root attributes:\nlog, webout, headers.\n\nThe webout attribute will be nested JSON ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content."
|
summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
|
||||||
summary: 'Execute a Stored Program, return a JSON object'
|
|
||||||
tags:
|
tags:
|
||||||
- STP
|
- STP
|
||||||
security:
|
security:
|
||||||
@@ -1491,7 +1739,7 @@ paths:
|
|||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
-
|
-
|
||||||
description: 'Location of SAS or JS code'
|
description: 'Location of code in SASjs Drive'
|
||||||
in: query
|
in: query
|
||||||
name: _program
|
name: _program
|
||||||
required: false
|
required: false
|
||||||
@@ -1503,38 +1751,124 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
$ref: '#/components/schemas/ExecutePostRequestPayload'
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
operationId: Home
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: 'Render index.html'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
/SASLogon/login:
|
||||||
|
post:
|
||||||
|
operationId: Login
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
user: {properties: {isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [isAdmin, displayName, username, id], type: object}
|
||||||
|
loggedIn: {type: boolean}
|
||||||
|
required:
|
||||||
|
- user
|
||||||
|
- loggedIn
|
||||||
|
type: object
|
||||||
|
summary: 'Accept a valid username/password'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/LoginPayload'
|
||||||
|
/SASLogon/authorize:
|
||||||
|
post:
|
||||||
|
operationId: Authorize
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthorizeResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {code: someRandomCryptoString}
|
||||||
|
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthorizePayload'
|
||||||
|
/SASLogon/logout:
|
||||||
|
get:
|
||||||
|
operationId: Logout
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
summary: 'Destroy the session stored in cookies'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
servers:
|
servers:
|
||||||
-
|
-
|
||||||
url: /
|
url: /
|
||||||
tags:
|
tags:
|
||||||
-
|
-
|
||||||
name: Info
|
name: Auth
|
||||||
description: 'Get Server Info'
|
description: 'Operations about auth'
|
||||||
-
|
-
|
||||||
name: Session
|
name: Auth_Config
|
||||||
description: 'Get Session information'
|
description: 'Operations about external auth providers'
|
||||||
-
|
|
||||||
name: User
|
|
||||||
description: 'Operations about users'
|
|
||||||
-
|
-
|
||||||
name: Client
|
name: Client
|
||||||
description: 'Operations about clients'
|
description: 'Operations about clients'
|
||||||
-
|
-
|
||||||
name: Auth
|
name: Code
|
||||||
description: 'Operations about auth'
|
description: 'Execution of code (various runtimes are supported)'
|
||||||
-
|
-
|
||||||
name: Drive
|
name: Drive
|
||||||
description: 'Operations about drive'
|
description: 'Operations on SASjs Drive'
|
||||||
-
|
-
|
||||||
name: Group
|
name: Group
|
||||||
description: 'Operations about group'
|
description: 'Operations on groups and group memberships'
|
||||||
|
-
|
||||||
|
name: Info
|
||||||
|
description: 'Get Server Information'
|
||||||
|
-
|
||||||
|
name: Permission
|
||||||
|
description: 'Operations about permissions'
|
||||||
|
-
|
||||||
|
name: Session
|
||||||
|
description: 'Get Session information'
|
||||||
-
|
-
|
||||||
name: STP
|
name: STP
|
||||||
description: 'Operations about STP'
|
description: 'Execution of Stored Programs'
|
||||||
-
|
-
|
||||||
name: CODE
|
name: User
|
||||||
description: 'Operations on SAS code'
|
description: 'Operations with users'
|
||||||
-
|
-
|
||||||
name: Web
|
name: Web
|
||||||
description: 'Operations on Web'
|
description: 'Operations on Web'
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import {
|
|||||||
readFile,
|
readFile,
|
||||||
SASJsFileType
|
SASJsFileType
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
import { apiRoot, sysInitCompiledPath } from '../src/utils'
|
import { apiRoot, sysInitCompiledPath } from '../src/utils/file'
|
||||||
|
|
||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
const compiledSystemInit = async (systemInit: string) =>
|
const compiledSystemInit = async (systemInit: string) =>
|
||||||
'options ps=max;\n' +
|
'options ls=max ps=max;\n' +
|
||||||
(await loadDependenciesFile({
|
(await loadDependenciesFile({
|
||||||
fileContent: systemInit,
|
fileContent: systemInit,
|
||||||
type: SASJsFileType.job,
|
type: SASJsFileType.job,
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import {
|
|||||||
listFilesInFolder
|
listFilesInFolder
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
|
import {
|
||||||
|
apiRoot,
|
||||||
|
sasJSCoreMacros,
|
||||||
|
sasJSCoreMacrosInfo
|
||||||
|
} from '../src/utils/file'
|
||||||
|
|
||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
|
|||||||
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()
|
||||||
21
api/src/app-modules/configureCors.ts
Normal file
21
api/src/app-modules/configureCors.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
import cors from 'cors'
|
||||||
|
import { CorsType } from '../utils'
|
||||||
|
|
||||||
|
export const configureCors = (app: Express) => {
|
||||||
|
const { CORS, WHITELIST } = process.env
|
||||||
|
|
||||||
|
if (CORS === CorsType.ENABLED) {
|
||||||
|
const whiteList: string[] = []
|
||||||
|
WHITELIST?.split(' ')
|
||||||
|
?.filter((url) => !!url)
|
||||||
|
.forEach((url) => {
|
||||||
|
if (url.startsWith('http'))
|
||||||
|
// removing trailing slash of URLs listing for CORS
|
||||||
|
whiteList.push(url.replace(/\/$/, ''))
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('All CORS Requests are enabled for:', whiteList)
|
||||||
|
app.use(cors({ credentials: true, origin: whiteList }))
|
||||||
|
}
|
||||||
|
}
|
||||||
32
api/src/app-modules/configureExpressSession.ts
Normal file
32
api/src/app-modules/configureExpressSession.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
import session from 'express-session'
|
||||||
|
import MongoStore from 'connect-mongo'
|
||||||
|
|
||||||
|
import { ModeType } from '../utils'
|
||||||
|
import { cookieOptions } from '../app'
|
||||||
|
|
||||||
|
export const configureExpressSession = (app: Express) => {
|
||||||
|
const { MODE } = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
let store: MongoStore | undefined
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
store = MongoStore.create({
|
||||||
|
client: mongoose.connection!.getClient() as any,
|
||||||
|
collectionName: 'sessions'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: process.secrets.SESSION_SECRET,
|
||||||
|
saveUninitialized: false, // don't create session until something stored
|
||||||
|
resave: false, //don't save session if unmodified
|
||||||
|
store,
|
||||||
|
cookie: cookieOptions
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
api/src/app-modules/configureLogger.ts
Normal file
33
api/src/app-modules/configureLogger.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { Express } from 'express'
|
||||||
|
import morgan from 'morgan'
|
||||||
|
import { createStream } from 'rotating-file-stream'
|
||||||
|
import { generateTimestamp } from '@sasjs/utils'
|
||||||
|
import { getLogFolder } from '../utils'
|
||||||
|
|
||||||
|
export const configureLogger = (app: Express) => {
|
||||||
|
const { LOG_FORMAT_MORGAN } = process.env
|
||||||
|
|
||||||
|
let options
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV !== 'development' &&
|
||||||
|
process.env.NODE_ENV !== 'test'
|
||||||
|
) {
|
||||||
|
const timestamp = generateTimestamp()
|
||||||
|
const filename = `${timestamp}.log`
|
||||||
|
const logsFolder = getLogFolder()
|
||||||
|
|
||||||
|
// create a rotating write stream
|
||||||
|
var accessLogStream = createStream(filename, {
|
||||||
|
interval: '1d', // rotate daily
|
||||||
|
path: logsFolder
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Writing Logs to :', path.join(logsFolder, filename))
|
||||||
|
|
||||||
|
options = { stream: accessLogStream }
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup the logger
|
||||||
|
app.use(morgan(LOG_FORMAT_MORGAN as string, options))
|
||||||
|
}
|
||||||
26
api/src/app-modules/configureSecurity.ts
Normal file
26
api/src/app-modules/configureSecurity.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
import { getEnvCSPDirectives } from '../utils/parseHelmetConfig'
|
||||||
|
import { HelmetCoepType, ProtocolType } from '../utils'
|
||||||
|
import helmet from 'helmet'
|
||||||
|
|
||||||
|
export const configureSecurity = (app: Express) => {
|
||||||
|
const { PROTOCOL, HELMET_CSP_CONFIG_PATH, HELMET_COEP } = process.env
|
||||||
|
|
||||||
|
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
||||||
|
HELMET_CSP_CONFIG_PATH
|
||||||
|
)
|
||||||
|
if (PROTOCOL === ProtocolType.HTTP)
|
||||||
|
cspConfigJson['upgrade-insecure-requests'] = null
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
||||||
|
...cspConfigJson
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
4
api/src/app-modules/index.ts
Normal file
4
api/src/app-modules/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './configureCors'
|
||||||
|
export * from './configureExpressSession'
|
||||||
|
export * from './configureLogger'
|
||||||
|
export * from './configureSecurity'
|
||||||
140
api/src/app.ts
140
api/src/app.ts
@@ -1,30 +1,25 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { ErrorRequestHandler } from 'express'
|
import express, { ErrorRequestHandler, CookieOptions } from 'express'
|
||||||
import csrf from 'csurf'
|
|
||||||
import session from 'express-session'
|
|
||||||
import MongoStore from 'connect-mongo'
|
|
||||||
import morgan from 'morgan'
|
|
||||||
import cookieParser from 'cookie-parser'
|
import cookieParser from 'cookie-parser'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import cors from 'cors'
|
|
||||||
import helmet from 'helmet'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
connectDB,
|
|
||||||
copySASjsCore,
|
copySASjsCore,
|
||||||
CorsType,
|
|
||||||
getWebBuildFolder,
|
getWebBuildFolder,
|
||||||
HelmetCoepType,
|
|
||||||
instantiateLogger,
|
instantiateLogger,
|
||||||
loadAppStreamConfig,
|
loadAppStreamConfig,
|
||||||
ModeType,
|
|
||||||
ProtocolType,
|
ProtocolType,
|
||||||
ReturnCode,
|
ReturnCode,
|
||||||
setProcessVariables,
|
setProcessVariables,
|
||||||
setupFolders,
|
setupFolders,
|
||||||
verifyEnvVariables
|
verifyEnvVariables
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
|
import {
|
||||||
|
configureCors,
|
||||||
|
configureExpressSession,
|
||||||
|
configureLogger,
|
||||||
|
configureSecurity
|
||||||
|
} from './app-modules'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
@@ -34,108 +29,49 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
|||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.use(cookieParser())
|
const { PROTOCOL } = process.env
|
||||||
|
|
||||||
const {
|
export const cookieOptions: CookieOptions = {
|
||||||
MODE,
|
|
||||||
CORS,
|
|
||||||
WHITELIST,
|
|
||||||
PROTOCOL,
|
|
||||||
HELMET_CSP_CONFIG_PATH,
|
|
||||||
HELMET_COEP,
|
|
||||||
LOG_FORMAT_MORGAN
|
|
||||||
} = process.env
|
|
||||||
|
|
||||||
app.use(morgan(LOG_FORMAT_MORGAN as string))
|
|
||||||
|
|
||||||
export const cookieOptions = {
|
|
||||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
|
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
|
||||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||||
}
|
}
|
||||||
|
|
||||||
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
|
||||||
HELMET_CSP_CONFIG_PATH
|
|
||||||
)
|
|
||||||
if (PROTOCOL === ProtocolType.HTTP)
|
|
||||||
cspConfigJson['upgrade-insecure-requests'] = null
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* CSRF Protection *
|
|
||||||
***********************************/
|
|
||||||
export const csrfProtection = csrf({ cookie: cookieOptions })
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* Handle security and origin *
|
|
||||||
***********************************/
|
|
||||||
app.use(
|
|
||||||
helmet({
|
|
||||||
contentSecurityPolicy: {
|
|
||||||
directives: {
|
|
||||||
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
|
||||||
...cspConfigJson
|
|
||||||
}
|
|
||||||
},
|
|
||||||
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* Enabling CORS *
|
|
||||||
***********************************/
|
|
||||||
if (CORS === CorsType.ENABLED) {
|
|
||||||
const whiteList: string[] = []
|
|
||||||
WHITELIST?.split(' ')
|
|
||||||
?.filter((url) => !!url)
|
|
||||||
.forEach((url) => {
|
|
||||||
if (url.startsWith('http'))
|
|
||||||
// removing trailing slash of URLs listing for CORS
|
|
||||||
whiteList.push(url.replace(/\/$/, ''))
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('All CORS Requests are enabled for:', whiteList)
|
|
||||||
app.use(cors({ credentials: true, origin: whiteList }))
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* DB Connection & *
|
|
||||||
* Express Sessions *
|
|
||||||
* With Mongo Store *
|
|
||||||
***********************************/
|
|
||||||
if (MODE === ModeType.Server) {
|
|
||||||
let store: MongoStore | undefined
|
|
||||||
|
|
||||||
// NOTE: when exporting app.js as agent for supertest
|
|
||||||
// we should exclude connecting to the real database
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
|
||||||
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
|
|
||||||
|
|
||||||
store = MongoStore.create({ clientPromise, collectionName: 'sessions' })
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
session({
|
|
||||||
secret: process.env.SESSION_SECRET as string,
|
|
||||||
saveUninitialized: false, // don't create session until something stored
|
|
||||||
resave: false, //don't save session if unmodified
|
|
||||||
store,
|
|
||||||
cookie: cookieOptions
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(express.json({ limit: '100mb' }))
|
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
|
||||||
|
|
||||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
if (err.code === 'EBADCSRFTOKEN')
|
|
||||||
return res.status(400).send('Invalid CSRF token!')
|
|
||||||
|
|
||||||
console.error(err.stack)
|
console.error(err.stack)
|
||||||
res.status(500).send('Something broke!')
|
res.status(500).send('Something broke!')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default setProcessVariables().then(async () => {
|
export default setProcessVariables().then(async () => {
|
||||||
|
app.use(cookieParser())
|
||||||
|
|
||||||
|
configureLogger(app)
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Handle security and origin *
|
||||||
|
***********************************/
|
||||||
|
configureSecurity(app)
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Enabling CORS *
|
||||||
|
***********************************/
|
||||||
|
configureCors(app)
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* DB Connection & *
|
||||||
|
* Express Sessions *
|
||||||
|
* With Mongo Store *
|
||||||
|
***********************************/
|
||||||
|
configureExpressSession(app)
|
||||||
|
|
||||||
|
app.use(express.json({ limit: '100mb' }))
|
||||||
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
|
// Body parser is used for decoding the formdata on POST request.
|
||||||
|
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
||||||
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
await setupFolders()
|
await setupFolders()
|
||||||
await copySASjsCore()
|
await copySASjsCore()
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { InfoJWT } from '../types'
|
|||||||
import {
|
import {
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
generateRefreshToken,
|
generateRefreshToken,
|
||||||
|
getTokensFromDB,
|
||||||
removeTokensInDB,
|
removeTokensInDB,
|
||||||
saveTokensInDB
|
saveTokensInDB
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
@@ -73,6 +74,15 @@ const token = async (data: any): Promise<TokenResponse> => {
|
|||||||
|
|
||||||
AuthController.deleteCode(userInfo.userId, clientId)
|
AuthController.deleteCode(userInfo.userId, clientId)
|
||||||
|
|
||||||
|
// get tokens from DB
|
||||||
|
const existingTokens = await getTokensFromDB(userInfo.userId, clientId)
|
||||||
|
if (existingTokens) {
|
||||||
|
return {
|
||||||
|
accessToken: existingTokens.accessToken,
|
||||||
|
refreshToken: existingTokens.refreshToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = generateAccessToken(userInfo)
|
const accessToken = generateAccessToken(userInfo)
|
||||||
const refreshToken = generateRefreshToken(userInfo)
|
const refreshToken = generateRefreshToken(userInfo)
|
||||||
|
|
||||||
@@ -129,8 +139,8 @@ const verifyAuthCode = async (
|
|||||||
clientId: string,
|
clientId: string,
|
||||||
code: string
|
code: string
|
||||||
): Promise<InfoJWT | undefined> => {
|
): Promise<InfoJWT | undefined> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
|
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
|
||||||
if (err) return resolve(undefined)
|
if (err) return resolve(undefined)
|
||||||
|
|
||||||
const clientInfo: InfoJWT = {
|
const clientInfo: InfoJWT = {
|
||||||
|
|||||||
185
api/src/controllers/authConfig.ts
Normal file
185
api/src/controllers/authConfig.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
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,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,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
moveFile,
|
moveFile,
|
||||||
createFolder,
|
createFolder,
|
||||||
deleteFile as deleteFileOnSystem,
|
deleteFile as deleteFileOnSystem,
|
||||||
|
deleteFolder as deleteFolderOnSystem,
|
||||||
folderExists,
|
folderExists,
|
||||||
listFilesInFolder,
|
listFilesInFolder,
|
||||||
listSubFoldersInFolder,
|
listSubFoldersInFolder,
|
||||||
@@ -58,11 +59,32 @@ interface GetFileTreeResponse {
|
|||||||
tree: TreeNode
|
tree: TreeNode
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateFileResponse {
|
interface FileFolderResponse {
|
||||||
status: string
|
status: string
|
||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AddFolderPayload {
|
||||||
|
/**
|
||||||
|
* Location of folder
|
||||||
|
* @example "/Public/someFolder"
|
||||||
|
*/
|
||||||
|
folderPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RenamePayload {
|
||||||
|
/**
|
||||||
|
* Old path of file/folder
|
||||||
|
* @example "/Public/someFolder"
|
||||||
|
*/
|
||||||
|
oldPath: string
|
||||||
|
/**
|
||||||
|
* New path of file/folder
|
||||||
|
* @example "/Public/newFolder"
|
||||||
|
*/
|
||||||
|
newPath: string
|
||||||
|
}
|
||||||
|
|
||||||
const fileTreeExample = getTreeExample()
|
const fileTreeExample = getTreeExample()
|
||||||
|
|
||||||
const successDeployResponse: DeployResponse = {
|
const successDeployResponse: DeployResponse = {
|
||||||
@@ -143,7 +165,7 @@ export class DriveController {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @summary Delete file from SASjs Drive
|
* @summary Delete file from SASjs Drive
|
||||||
* @query _filePath Location of SAS program
|
* @query _filePath Location of file
|
||||||
* @example _filePath "/Public/somefolder/some.file"
|
* @example _filePath "/Public/somefolder/some.file"
|
||||||
*/
|
*/
|
||||||
@Delete('/file')
|
@Delete('/file')
|
||||||
@@ -151,20 +173,31 @@ export class DriveController {
|
|||||||
return deleteFile(_filePath)
|
return deleteFile(_filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @summary Delete folder from SASjs Drive
|
||||||
|
* @query _folderPath Location of folder
|
||||||
|
* @example _folderPath "/Public/somefolder/"
|
||||||
|
*/
|
||||||
|
@Delete('/folder')
|
||||||
|
public async deleteFolder(@Query() _folderPath: string) {
|
||||||
|
return deleteFolder(_folderPath)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's optional to either provide `_filePath` in url as query parameter
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
* Or provide `filePath` in body as form field.
|
* Or provide `filePath` in body as form field.
|
||||||
* But it's required to provide else API will respond with Bad Request.
|
* But it's required to provide else API will respond with Bad Request.
|
||||||
*
|
*
|
||||||
* @summary Create a file in SASjs Drive
|
* @summary Create a file in SASjs Drive
|
||||||
* @param _filePath Location of SAS program
|
* @param _filePath Location of file
|
||||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<UpdateFileResponse>({
|
@Example<FileFolderResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<UpdateFileResponse>(403, 'File already exists', {
|
@Response<FileFolderResponse>(403, 'File already exists', {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@@ -173,10 +206,28 @@ export class DriveController {
|
|||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@Query() _filePath?: string,
|
@Query() _filePath?: string,
|
||||||
@FormField() filePath?: string
|
@FormField() filePath?: string
|
||||||
): Promise<UpdateFileResponse> {
|
): Promise<FileFolderResponse> {
|
||||||
return saveFile((_filePath ?? filePath)!, file)
|
return saveFile((_filePath ?? filePath)!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Create an empty folder in SASjs Drive
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<FileFolderResponse>({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
@Response<FileFolderResponse>(409, 'Folder already exists', {
|
||||||
|
status: 'failure',
|
||||||
|
message: 'Add folder request failed.'
|
||||||
|
})
|
||||||
|
@Post('/folder')
|
||||||
|
public async addFolder(
|
||||||
|
@Body() body: AddFolderPayload
|
||||||
|
): Promise<FileFolderResponse> {
|
||||||
|
return addFolder(body.folderPath)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's optional to either provide `_filePath` in url as query parameter
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
* Or provide `filePath` in body as form field.
|
* Or provide `filePath` in body as form field.
|
||||||
@@ -187,10 +238,10 @@ export class DriveController {
|
|||||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<UpdateFileResponse>({
|
@Example<FileFolderResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
|
@Response<FileFolderResponse>(403, `File doesn't exist`, {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@@ -199,10 +250,28 @@ export class DriveController {
|
|||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@Query() _filePath?: string,
|
@Query() _filePath?: string,
|
||||||
@FormField() filePath?: string
|
@FormField() filePath?: string
|
||||||
): Promise<UpdateFileResponse> {
|
): Promise<FileFolderResponse> {
|
||||||
return updateFile((_filePath ?? filePath)!, file)
|
return updateFile((_filePath ?? filePath)!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Renames a file/folder in SASjs Drive
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<FileFolderResponse>({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
@Response<FileFolderResponse>(409, 'Folder already exists', {
|
||||||
|
status: 'failure',
|
||||||
|
message: 'rename request failed.'
|
||||||
|
})
|
||||||
|
@Post('/rename')
|
||||||
|
public async rename(
|
||||||
|
@Body() body: RenamePayload
|
||||||
|
): Promise<FileFolderResponse> {
|
||||||
|
return rename(body.oldPath, body.newPath)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Fetch file tree within SASjs Drive.
|
* @summary Fetch file tree within SASjs Drive.
|
||||||
*
|
*
|
||||||
@@ -249,20 +318,26 @@ const getFile = async (req: express.Request, filePath: string) => {
|
|||||||
.join(getFilesFolder(), filePath)
|
.join(getFilesFolder(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath))
|
||||||
throw new Error('Cannot get file outside drive.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't get file outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull))) {
|
if (!(await fileExists(filePathFull)))
|
||||||
throw new Error("File doesn't exist.")
|
throw {
|
||||||
}
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: `File doesn't exist.`
|
||||||
|
}
|
||||||
|
|
||||||
const extension = path.extname(filePathFull).toLowerCase()
|
const extension = path.extname(filePathFull).toLowerCase()
|
||||||
if (extension === '.sas') {
|
if (extension === '.sas') {
|
||||||
req.res?.setHeader('Content-type', 'text/plain')
|
req.res?.setHeader('Content-type', 'text/plain')
|
||||||
}
|
}
|
||||||
|
|
||||||
req.res?.sendFile(path.resolve(filePathFull))
|
req.res?.sendFile(path.resolve(filePathFull), { dotfiles: 'allow' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFolder = async (folderPath?: string) => {
|
const getFolder = async (folderPath?: string) => {
|
||||||
@@ -273,17 +348,26 @@ const getFolder = async (folderPath?: string) => {
|
|||||||
.join(getFilesFolder(), folderPath)
|
.join(getFilesFolder(), folderPath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!folderPathFull.includes(driveFilesPath)) {
|
if (!folderPathFull.includes(driveFilesPath))
|
||||||
throw new Error('Cannot get folder outside drive.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't get folder outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await folderExists(folderPathFull))) {
|
if (!(await folderExists(folderPathFull)))
|
||||||
throw new Error("Folder doesn't exist.")
|
throw {
|
||||||
}
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: `Folder doesn't exist.`
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await isFolder(folderPathFull))) {
|
if (!(await isFolder(folderPathFull)))
|
||||||
throw new Error('Not a Folder.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: 'Not a Folder.'
|
||||||
|
}
|
||||||
|
|
||||||
const files: string[] = await listFilesInFolder(folderPathFull)
|
const files: string[] = await listFilesInFolder(folderPathFull)
|
||||||
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
||||||
@@ -302,19 +386,51 @@ const deleteFile = async (filePath: string) => {
|
|||||||
.join(getFilesFolder(), filePath)
|
.join(getFilesFolder(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath))
|
||||||
throw new Error('Cannot delete file outside drive.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't delete file outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull))) {
|
if (!(await fileExists(filePathFull)))
|
||||||
throw new Error('File does not exist.')
|
throw {
|
||||||
}
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: `File doesn't exist.`
|
||||||
|
}
|
||||||
|
|
||||||
await deleteFileOnSystem(filePathFull)
|
await deleteFileOnSystem(filePathFull)
|
||||||
|
|
||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteFolder = async (folderPath: string) => {
|
||||||
|
const driveFolderPath = getFilesFolder()
|
||||||
|
|
||||||
|
const folderPathFull = path
|
||||||
|
.join(getFilesFolder(), folderPath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
|
if (!folderPathFull.includes(driveFolderPath))
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't delete folder outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await folderExists(folderPathFull)))
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: `Folder doesn't exist.`
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteFolderOnSystem(folderPathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
const saveFile = async (
|
const saveFile = async (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
@@ -325,13 +441,19 @@ const saveFile = async (
|
|||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath))
|
||||||
throw new Error('Cannot put file outside drive.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't put file outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
if (await fileExists(filePathFull)) {
|
if (await fileExists(filePathFull))
|
||||||
throw new Error('File already exists.')
|
throw {
|
||||||
}
|
code: 409,
|
||||||
|
status: 'Conflict',
|
||||||
|
message: 'File already exists.'
|
||||||
|
}
|
||||||
|
|
||||||
const folderPath = path.dirname(filePathFull)
|
const folderPath = path.dirname(filePathFull)
|
||||||
await createFolder(folderPath)
|
await createFolder(folderPath)
|
||||||
@@ -340,6 +462,88 @@ const saveFile = async (
|
|||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addFolder = async (folderPath: string): Promise<FileFolderResponse> => {
|
||||||
|
const drivePath = getFilesFolder()
|
||||||
|
|
||||||
|
const folderPathFull = path
|
||||||
|
.join(drivePath, folderPath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
|
if (!folderPathFull.includes(drivePath))
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't put folder outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await folderExists(folderPathFull))
|
||||||
|
throw {
|
||||||
|
code: 409,
|
||||||
|
status: 'Conflict',
|
||||||
|
message: 'Folder already exists.'
|
||||||
|
}
|
||||||
|
|
||||||
|
await createFolder(folderPathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const rename = async (
|
||||||
|
oldPath: string,
|
||||||
|
newPath: string
|
||||||
|
): Promise<FileFolderResponse> => {
|
||||||
|
const drivePath = getFilesFolder()
|
||||||
|
|
||||||
|
const oldPathFull = path
|
||||||
|
.join(drivePath, oldPath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
|
const newPathFull = path
|
||||||
|
.join(drivePath, newPath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
|
if (!oldPathFull.includes(drivePath))
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Old path can't be outside of drive.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPathFull.includes(drivePath))
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `New path can't be outside of drive.`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await isFolder(oldPathFull)) {
|
||||||
|
if (await folderExists(newPathFull))
|
||||||
|
throw {
|
||||||
|
code: 409,
|
||||||
|
status: 'Conflict',
|
||||||
|
message: 'Folder with new name already exists.'
|
||||||
|
}
|
||||||
|
else moveFile(oldPathFull, newPathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
|
} else if (await fileExists(oldPathFull)) {
|
||||||
|
if (await fileExists(newPathFull))
|
||||||
|
throw {
|
||||||
|
code: 409,
|
||||||
|
status: 'Conflict',
|
||||||
|
message: 'File with new name already exists.'
|
||||||
|
}
|
||||||
|
else moveFile(oldPathFull, newPathFull)
|
||||||
|
return { status: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: 'No file/folder found for provided path.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateFile = async (
|
const updateFile = async (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
@@ -350,13 +554,19 @@ const updateFile = async (
|
|||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath))
|
||||||
throw new Error('Cannot modify file outside drive.')
|
throw {
|
||||||
}
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't modify file outside drive.`
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull))) {
|
if (!(await fileExists(filePathFull)))
|
||||||
throw new Error(`File doesn't exist.`)
|
throw {
|
||||||
}
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: `File doesn't exist.`
|
||||||
|
}
|
||||||
|
|
||||||
await moveFile(multerFile.path, filePathFull)
|
await moveFile(multerFile.path, filePathFull)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -20,7 +21,7 @@ export interface GroupResponse {
|
|||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroupDetailsResponse {
|
export interface GroupDetailsResponse {
|
||||||
groupId: number
|
groupId: number
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +201,7 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
|
|||||||
'groupId name description isActive users -_id'
|
'groupId name description isActive users -_id'
|
||||||
).populate(
|
).populate(
|
||||||
'users',
|
'users',
|
||||||
'id username displayName -_id'
|
'id username displayName isAdmin -_id'
|
||||||
)) as unknown as GroupDetailsResponse
|
)) as unknown as GroupDetailsResponse
|
||||||
if (!group)
|
if (!group)
|
||||||
throw {
|
throw {
|
||||||
@@ -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,9 +266,17 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'User not found.'
|
message: 'User not found.'
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedGroup = (action === 'addUser'
|
if (user.authProvider)
|
||||||
? await group.addUser(user._id)
|
throw {
|
||||||
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
|
code: 405,
|
||||||
|
status: 'Method Not Allowed',
|
||||||
|
message: `Can't add/remove user to group created by external auth provider.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedGroup =
|
||||||
|
action === 'addUser'
|
||||||
|
? await group.addUser(user)
|
||||||
|
: await group.removeUser(user)
|
||||||
|
|
||||||
if (!updatedGroup)
|
if (!updatedGroup)
|
||||||
throw {
|
throw {
|
||||||
@@ -260,9 +285,6 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'Unable to update group.'
|
message: 'Unable to update group.'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'addUser') user.addGroup(group._id)
|
|
||||||
else user.removeGroup(group._id)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupId: updatedGroup.groupId,
|
groupId: updatedGroup.groupId,
|
||||||
name: updatedGroup.name,
|
name: updatedGroup.name,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
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'
|
||||||
export * from './group'
|
export * from './group'
|
||||||
export * from './info'
|
export * from './info'
|
||||||
|
export * from './permission'
|
||||||
export * from './session'
|
export * from './session'
|
||||||
export * from './stp'
|
export * from './stp'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Route, Tags, Example, Get } from 'tsoa'
|
import { Route, Tags, Example, Get } from 'tsoa'
|
||||||
|
import { getAuthorizedRoutes } from '../utils'
|
||||||
|
export interface AuthorizedRoutesResponse {
|
||||||
|
paths: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface InfoResponse {
|
export interface InfoResponse {
|
||||||
mode: string
|
mode: string
|
||||||
@@ -36,4 +40,19 @@ export class InfoController {
|
|||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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>({
|
||||||
|
paths: ['/AppStream', '/SASjsApi/stp/execute']
|
||||||
|
})
|
||||||
|
@Get('/authorizedRoutes')
|
||||||
|
public authorizedRoutes(): AuthorizedRoutesResponse {
|
||||||
|
const response = {
|
||||||
|
paths: getAuthorizedRoutes()
|
||||||
|
}
|
||||||
|
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
|
||||||
@@ -68,10 +62,9 @@ export class ExecutionController {
|
|||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
vars,
|
vars,
|
||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
|
||||||
session: sessionByFileUpload,
|
session: sessionByFileUpload,
|
||||||
runTime
|
runTime
|
||||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||||
const sessionController = getSessionController(runTime)
|
const sessionController = getSessionController(runTime)
|
||||||
|
|
||||||
const session =
|
const session =
|
||||||
@@ -96,6 +89,7 @@ export class ExecutionController {
|
|||||||
vars,
|
vars,
|
||||||
session,
|
session,
|
||||||
weboutPath,
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
tokenFile,
|
tokenFile,
|
||||||
runTime,
|
runTime,
|
||||||
logPath,
|
logPath,
|
||||||
@@ -107,10 +101,7 @@ export class ExecutionController {
|
|||||||
? await readFile(headersPath)
|
? await readFile(headersPath)
|
||||||
: ''
|
: ''
|
||||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
const fileResponse: boolean =
|
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
||||||
httpHeaders.hasOwnProperty('content-type') &&
|
|
||||||
!returnJson && // not a POST Request
|
|
||||||
!isDebugOn(vars) // Debug is not enabled
|
|
||||||
|
|
||||||
const webout = (await fileExists(weboutPath))
|
const webout = (await fileExists(weboutPath))
|
||||||
? fileResponse
|
? fileResponse
|
||||||
@@ -121,19 +112,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,6 +126,7 @@ export class ExecutionController {
|
|||||||
name: 'files',
|
name: 'files',
|
||||||
relativePath: '',
|
relativePath: '',
|
||||||
absolutePath: getFilesFolder(),
|
absolutePath: getFilesFolder(),
|
||||||
|
isFolder: true,
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,15 +136,22 @@ export class ExecutionController {
|
|||||||
const currentNode = stack.pop()
|
const currentNode = stack.pop()
|
||||||
|
|
||||||
if (currentNode) {
|
if (currentNode) {
|
||||||
|
currentNode.isFolder = fs
|
||||||
|
.statSync(currentNode.absolutePath)
|
||||||
|
.isDirectory()
|
||||||
|
|
||||||
const children = fs.readdirSync(currentNode.absolutePath)
|
const children = fs.readdirSync(currentNode.absolutePath)
|
||||||
|
|
||||||
for (let child of children) {
|
for (let child of children) {
|
||||||
const absoluteChildPath = `${currentNode.absolutePath}/${child}`
|
const absoluteChildPath = path.join(currentNode.absolutePath, child)
|
||||||
|
// relative path will only be used in frontend component
|
||||||
|
// so, no need to convert '/' to platform specific separator
|
||||||
const relativeChildPath = `${currentNode.relativePath}/${child}`
|
const relativeChildPath = `${currentNode.relativePath}/${child}`
|
||||||
const childNode: TreeNode = {
|
const childNode: TreeNode = {
|
||||||
name: child,
|
name: child,
|
||||||
relativePath: relativeChildPath,
|
relativePath: relativeChildPath,
|
||||||
absolutePath: absoluteChildPath,
|
absolutePath: absoluteChildPath,
|
||||||
|
isFolder: false,
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
currentNode.children.push(childNode)
|
currentNode.children.push(childNode)
|
||||||
|
|||||||
@@ -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/plain')
|
||||||
|
|
||||||
|
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/plain')
|
||||||
|
|
||||||
// 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,9 +134,14 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
isWindows() ? '-nosplash' : '',
|
isWindows() ? '-nologo' : '',
|
||||||
isWindows() ? '-icon' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||||
isWindows() ? '-nologo' : ''
|
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
session.completed = true
|
session.completed = true
|
||||||
@@ -137,7 +175,7 @@ ${autoExecContent}`
|
|||||||
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)
|
||||||
|
|
||||||
@@ -162,66 +200,17 @@ ${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 (process.sessionController) return process.sessionController
|
||||||
return getSASSessionController()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runTime === RunTimeType.JS) {
|
process.sessionController =
|
||||||
return getJSSessionController()
|
runTime === RunTimeType.SAS
|
||||||
}
|
? new SASSessionController()
|
||||||
|
: 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,16 +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 = '${tokenFile}';
|
const _SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||||
const _sasjs_username = '${preProgramVariables?.username}';
|
const _SASJS_USERID = '${preProgramVariables?.userId}';
|
||||||
const _sasjs_userid = '${preProgramVariables?.userId}';
|
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||||
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
const _METAPERSON = _SASJS_DISPLAYNAME;
|
||||||
const _metaperson = _sasjs_displayname;
|
const _METAUSER = _SASJS_USERNAME;
|
||||||
const _metauser = _sasjs_username;
|
const SASJSPROCESSMODE = 'Stored Program';
|
||||||
const sasjsprocessmode = 'Stored Program';
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const requiredModules = `const fs = require('fs')`
|
const requiredModules = `const fs = require('fs')`
|
||||||
@@ -53,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,9 @@ 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()
|
||||||
|
|
||||||
|
proc printto print="%sysfunc(getoption(log))";
|
||||||
|
run;
|
||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
@@ -60,7 +68,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'
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ 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,6 +52,82 @@ 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!')
|
||||||
|
}
|
||||||
|
|
||||||
|
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(executablePath, [codePath], {
|
||||||
|
stdio: ['ignore', writeStream, writeStream]
|
||||||
|
})
|
||||||
|
|
||||||
|
// copy the code file 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
191
api/src/controllers/mock-sas9.ts
Normal file
191
api/src/controllers/mock-sas9.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { readFile } from '@sasjs/utils'
|
||||||
|
import express from 'express'
|
||||||
|
import path from 'path'
|
||||||
|
import { Request, Post, Get } from 'tsoa'
|
||||||
|
|
||||||
|
export interface Sas9Response {
|
||||||
|
content: string
|
||||||
|
redirect?: string
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockFileRead {
|
||||||
|
content: string
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockSas9Controller {
|
||||||
|
private loggedIn: string | undefined
|
||||||
|
|
||||||
|
@Get('/SASStoredProcess')
|
||||||
|
public async sasStoredProcess(): Promise<Sas9Response> {
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'sas-stored-process'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/SASStoredProcess/do/')
|
||||||
|
public async sasStoredProcessDo(
|
||||||
|
@Request() req: express.Request
|
||||||
|
): Promise<Sas9Response> {
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPublicAccount()) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/Login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let program = req.query._program?.toString() || ''
|
||||||
|
program = program.replace('/', '')
|
||||||
|
|
||||||
|
const content = await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
...program.split('/')
|
||||||
|
])
|
||||||
|
|
||||||
|
if (content.error) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedContent = parseJsonIfValid(content.content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: parsedContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'logged-in'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'login'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/SASLogon/login')
|
||||||
|
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
||||||
|
this.loggedIn = req.body.username
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'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',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'public-access-denied'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'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'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If JSON is valid it will be parsed otherwise will return text unaltered
|
||||||
|
* @param content string to be parsed
|
||||||
|
* @returns JSON or string
|
||||||
|
*/
|
||||||
|
const parseJsonIfValid = (content: string) => {
|
||||||
|
let fileContent = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileContent = JSON.parse(content)
|
||||||
|
} catch (err: any) {
|
||||||
|
fileContent = content
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileContent
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`
|
||||||
|
console.error(errMsg)
|
||||||
|
|
||||||
|
error = true
|
||||||
|
|
||||||
|
return errMsg
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: file,
|
||||||
|
error: error
|
||||||
|
}
|
||||||
|
}
|
||||||
368
api/src/controllers/permission.ts
Normal file
368
api/src/controllers/permission.ts
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import {
|
||||||
|
Security,
|
||||||
|
Route,
|
||||||
|
Tags,
|
||||||
|
Path,
|
||||||
|
Example,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Request
|
||||||
|
} from 'tsoa'
|
||||||
|
|
||||||
|
import Permission from '../model/Permission'
|
||||||
|
import User from '../model/User'
|
||||||
|
import Group from '../model/Group'
|
||||||
|
import { UserResponse } from './user'
|
||||||
|
import { GroupDetailsResponse } from './group'
|
||||||
|
|
||||||
|
export enum PermissionType {
|
||||||
|
route = 'Route'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PrincipalType {
|
||||||
|
user = 'user',
|
||||||
|
group = 'group'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PermissionSettingForRoute {
|
||||||
|
grant = 'Grant',
|
||||||
|
deny = 'Deny'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RegisterPermissionPayload {
|
||||||
|
/**
|
||||||
|
* Name of affected resource
|
||||||
|
* @example "/SASjsApi/code/execute"
|
||||||
|
*/
|
||||||
|
path: string
|
||||||
|
/**
|
||||||
|
* Type of affected resource
|
||||||
|
* @example "Route"
|
||||||
|
*/
|
||||||
|
type: PermissionType
|
||||||
|
/**
|
||||||
|
* The indication of whether (and to what extent) access is provided
|
||||||
|
* @example "Grant"
|
||||||
|
*/
|
||||||
|
setting: PermissionSettingForRoute
|
||||||
|
/**
|
||||||
|
* Indicates the type of principal
|
||||||
|
* @example "user"
|
||||||
|
*/
|
||||||
|
principalType: PrincipalType
|
||||||
|
/**
|
||||||
|
* The id of user or group to which a rule is assigned.
|
||||||
|
* @example 123
|
||||||
|
*/
|
||||||
|
principalId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdatePermissionPayload {
|
||||||
|
/**
|
||||||
|
* The indication of whether (and to what extent) access is provided
|
||||||
|
* @example "Grant"
|
||||||
|
*/
|
||||||
|
setting: PermissionSettingForRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionDetailsResponse {
|
||||||
|
permissionId: number
|
||||||
|
path: string
|
||||||
|
type: string
|
||||||
|
setting: string
|
||||||
|
user?: UserResponse
|
||||||
|
group?: GroupDetailsResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
@Security('bearerAuth')
|
||||||
|
@Route('SASjsApi/permission')
|
||||||
|
@Tags('Permission')
|
||||||
|
export class PermissionController {
|
||||||
|
/**
|
||||||
|
* 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[]>([
|
||||||
|
{
|
||||||
|
permissionId: 123,
|
||||||
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
|
setting: 'Grant',
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
username: 'johnSnow01',
|
||||||
|
displayName: 'John Snow',
|
||||||
|
isAdmin: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
permissionId: 124,
|
||||||
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
|
setting: 'Grant',
|
||||||
|
group: {
|
||||||
|
groupId: 1,
|
||||||
|
name: 'DCGroup',
|
||||||
|
description: 'This group represents Data Controller Users',
|
||||||
|
isActive: true,
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
@Get('/')
|
||||||
|
public async getAllPermissions(
|
||||||
|
@Request() request: express.Request
|
||||||
|
): Promise<PermissionDetailsResponse[]> {
|
||||||
|
return getAllPermissions(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Create a new permission. Admin only.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<PermissionDetailsResponse>({
|
||||||
|
permissionId: 123,
|
||||||
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
|
setting: 'Grant',
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
username: 'johnSnow01',
|
||||||
|
displayName: 'John Snow',
|
||||||
|
isAdmin: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@Post('/')
|
||||||
|
public async createPermission(
|
||||||
|
@Body() body: RegisterPermissionPayload
|
||||||
|
): Promise<PermissionDetailsResponse> {
|
||||||
|
return createPermission(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Update permission setting. Admin only
|
||||||
|
* @param permissionId The permission's identifier
|
||||||
|
* @example permissionId 1234
|
||||||
|
*/
|
||||||
|
@Example<PermissionDetailsResponse>({
|
||||||
|
permissionId: 123,
|
||||||
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
|
setting: 'Grant',
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
username: 'johnSnow01',
|
||||||
|
displayName: 'John Snow',
|
||||||
|
isAdmin: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@Patch('{permissionId}')
|
||||||
|
public async updatePermission(
|
||||||
|
@Path() permissionId: number,
|
||||||
|
@Body() body: UpdatePermissionPayload
|
||||||
|
): Promise<PermissionDetailsResponse> {
|
||||||
|
return updatePermission(permissionId, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Delete a permission. Admin only.
|
||||||
|
* @param permissionId The user's identifier
|
||||||
|
* @example permissionId 1234
|
||||||
|
*/
|
||||||
|
@Delete('{permissionId}')
|
||||||
|
public async deletePermission(@Path() permissionId: number) {
|
||||||
|
return deletePermission(permissionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllPermissions = async (
|
||||||
|
req: express.Request
|
||||||
|
): Promise<PermissionDetailsResponse[]> => {
|
||||||
|
const { user } = req
|
||||||
|
|
||||||
|
if (user?.isAdmin) return await Permission.get({})
|
||||||
|
else {
|
||||||
|
const permissions: PermissionDetailsResponse[] = []
|
||||||
|
|
||||||
|
const dbUser = await User.findOne({ id: user?.userId })
|
||||||
|
if (!dbUser)
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: 'User not found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ({
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
setting,
|
||||||
|
principalType,
|
||||||
|
principalId
|
||||||
|
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
|
||||||
|
const permission = new Permission({
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
setting
|
||||||
|
})
|
||||||
|
|
||||||
|
let user: UserResponse | undefined
|
||||||
|
let group: GroupDetailsResponse | undefined
|
||||||
|
|
||||||
|
switch (principalType) {
|
||||||
|
case PrincipalType.user: {
|
||||||
|
const userInDB = await User.findOne({ id: principalId })
|
||||||
|
if (!userInDB)
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: 'User not found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userInDB.isAdmin)
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: 'Can not add permission for admin user.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyExists = await Permission.findOne({
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
user: userInDB._id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (alreadyExists)
|
||||||
|
throw {
|
||||||
|
code: 409,
|
||||||
|
status: 'Conflict',
|
||||||
|
message:
|
||||||
|
'Permission already exists with provided Path, Type and User.'
|
||||||
|
}
|
||||||
|
|
||||||
|
permission.user = userInDB._id
|
||||||
|
|
||||||
|
user = {
|
||||||
|
id: userInDB.id,
|
||||||
|
username: userInDB.username,
|
||||||
|
displayName: userInDB.displayName,
|
||||||
|
isAdmin: userInDB.isAdmin
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case PrincipalType.group: {
|
||||||
|
const groupInDB = await Group.findOne({ groupId: principalId })
|
||||||
|
if (!groupInDB)
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: 'Group not found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyExists = await Permission.findOne({
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
group: groupInDB._id
|
||||||
|
})
|
||||||
|
if (alreadyExists)
|
||||||
|
throw {
|
||||||
|
code: 409,
|
||||||
|
status: 'Conflict',
|
||||||
|
message:
|
||||||
|
'Permission already exists with provided Path, Type and Group.'
|
||||||
|
}
|
||||||
|
|
||||||
|
permission.group = groupInDB._id
|
||||||
|
|
||||||
|
group = {
|
||||||
|
groupId: groupInDB.groupId,
|
||||||
|
name: groupInDB.name,
|
||||||
|
description: groupInDB.description,
|
||||||
|
isActive: groupInDB.isActive,
|
||||||
|
users: groupInDB.populate({
|
||||||
|
path: 'users',
|
||||||
|
select: 'id username displayName isAdmin -_id',
|
||||||
|
options: { limit: 15 }
|
||||||
|
}) as unknown as UserResponse[]
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: 'Invalid principal type. Valid types are user or group.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedPermission = await permission.save()
|
||||||
|
|
||||||
|
return {
|
||||||
|
permissionId: savedPermission.permissionId,
|
||||||
|
path: savedPermission.path,
|
||||||
|
type: savedPermission.type,
|
||||||
|
setting: savedPermission.setting,
|
||||||
|
user,
|
||||||
|
group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePermission = async (
|
||||||
|
id: number,
|
||||||
|
data: UpdatePermissionPayload
|
||||||
|
): Promise<PermissionDetailsResponse> => {
|
||||||
|
const { setting } = data
|
||||||
|
|
||||||
|
const updatedPermission = (await Permission.findOneAndUpdate(
|
||||||
|
{ permissionId: id },
|
||||||
|
{ setting },
|
||||||
|
{ new: true }
|
||||||
|
)
|
||||||
|
.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'
|
||||||
|
})) as unknown as PermissionDetailsResponse
|
||||||
|
if (!updatedPermission)
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: 'Permission not found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletePermission = async (id: number) => {
|
||||||
|
const permission = await Permission.findOne({ permissionId: id })
|
||||||
|
if (!permission)
|
||||||
|
throw {
|
||||||
|
code: 404,
|
||||||
|
status: 'Not Found',
|
||||||
|
message: 'Permission not found.'
|
||||||
|
}
|
||||||
|
await Permission.deleteOne({ permissionId: id })
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ export class SessionController {
|
|||||||
@Example<UserResponse>({
|
@Example<UserResponse>({
|
||||||
id: 123,
|
id: 123,
|
||||||
username: 'johnusername',
|
username: 'johnusername',
|
||||||
displayName: 'John'
|
displayName: 'John',
|
||||||
|
isAdmin: false
|
||||||
})
|
})
|
||||||
@Get('/')
|
@Get('/')
|
||||||
public async session(
|
public async session(
|
||||||
@@ -26,5 +27,6 @@ export class SessionController {
|
|||||||
const session = (req: express.Request) => ({
|
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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,78 @@ 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
|
)
|
||||||
// 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 +105,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,16 +17,22 @@ 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 {
|
||||||
|
getUserAutoExec,
|
||||||
|
updateUserAutoExec,
|
||||||
|
ModeType,
|
||||||
|
AuthProviderType
|
||||||
|
} from '../utils'
|
||||||
import { GroupResponse } from './group'
|
import { GroupResponse } from './group'
|
||||||
|
|
||||||
export interface UserResponse {
|
export interface UserResponse {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
isAdmin: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserDetailsResponse {
|
export interface UserDetailsResponse {
|
||||||
id: number
|
id: number
|
||||||
displayName: string
|
displayName: string
|
||||||
username: string
|
username: string
|
||||||
@@ -48,12 +54,14 @@ export class UserController {
|
|||||||
{
|
{
|
||||||
id: 123,
|
id: 123,
|
||||||
username: 'johnusername',
|
username: 'johnusername',
|
||||||
displayName: 'John'
|
displayName: 'John',
|
||||||
|
isAdmin: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 456,
|
id: 456,
|
||||||
username: 'starkusername',
|
username: 'starkusername',
|
||||||
displayName: 'Stark'
|
displayName: 'Stark',
|
||||||
|
isAdmin: true
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@Get('/')
|
@Get('/')
|
||||||
@@ -200,7 +208,7 @@ export class UserController {
|
|||||||
|
|
||||||
const getAllUsers = async (): Promise<UserResponse[]> =>
|
const getAllUsers = async (): Promise<UserResponse[]> =>
|
||||||
await User.find({})
|
await User.find({})
|
||||||
.select({ _id: 0, id: 1, username: 1, displayName: 1 })
|
.select({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 })
|
||||||
.exec()
|
.exec()
|
||||||
|
|
||||||
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||||
@@ -208,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)
|
||||||
@@ -252,7 +264,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,
|
||||||
@@ -281,6 +297,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 })
|
||||||
@@ -289,7 +323,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
|
||||||
}
|
}
|
||||||
@@ -302,7 +339,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,
|
||||||
@@ -329,11 +369,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)
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import { readFile } 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,
|
||||||
|
AuthProviderType,
|
||||||
|
LDAPClient
|
||||||
|
} from '../utils'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
import { AuthController } from './auth'
|
import { AuthController } from './auth'
|
||||||
|
|
||||||
@@ -80,8 +85,16 @@ const login = async (
|
|||||||
const user = await User.findOne({ username })
|
const user = await User.findOne({ username })
|
||||||
if (!user) throw new Error('Username is not found.')
|
if (!user) throw new Error('Username is not found.')
|
||||||
|
|
||||||
const validPass = user.comparePassword(password)
|
if (
|
||||||
if (!validPass) throw new Error('Invalid password.')
|
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
|
||||||
|
user.authProvider === AuthProviderType.LDAP
|
||||||
|
) {
|
||||||
|
const ldapClient = await LDAPClient.init()
|
||||||
|
await ldapClient.verifyUser(username, password)
|
||||||
|
} else {
|
||||||
|
const validPass = user.comparePassword(password)
|
||||||
|
if (!validPass) throw new Error('Invalid password.')
|
||||||
|
}
|
||||||
|
|
||||||
req.session.loggedIn = true
|
req.session.loggedIn = true
|
||||||
req.session.user = {
|
req.session.user = {
|
||||||
@@ -99,7 +112,8 @@ const login = async (
|
|||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName
|
displayName: user.displayName,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
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 { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
|
import {
|
||||||
|
fetchLatestAutoExec,
|
||||||
|
ModeType,
|
||||||
|
verifyTokenInDB,
|
||||||
|
isAuthorizingRoute,
|
||||||
|
isPublicRoute,
|
||||||
|
publicUser
|
||||||
|
} from '../utils'
|
||||||
import { desktopUser } from './desktop'
|
import { desktopUser } from './desktop'
|
||||||
|
import { authorize } from './authorize'
|
||||||
|
|
||||||
export const authenticateAccessToken: RequestHandler = async (
|
export const authenticateAccessToken: RequestHandler = async (
|
||||||
req,
|
req,
|
||||||
@@ -15,6 +23,10 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextFunction = isAuthorizingRoute(req)
|
||||||
|
? () => authorize(req, res, next)
|
||||||
|
: next
|
||||||
|
|
||||||
// if request is coming from web and has valid session
|
// if request is coming from web and has valid session
|
||||||
// it can be validated.
|
// it can be validated.
|
||||||
if (req.session?.loggedIn) {
|
if (req.session?.loggedIn) {
|
||||||
@@ -24,33 +36,37 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
if (user) {
|
if (user) {
|
||||||
if (user.isActive) {
|
if (user.isActive) {
|
||||||
req.user = user
|
req.user = user
|
||||||
return csrfProtection(req, res, next)
|
return csrfProtection(req, res, nextFunction)
|
||||||
} else return res.sendStatus(401)
|
} else return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticateToken(
|
await authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next,
|
nextFunction,
|
||||||
process.env.ACCESS_TOKEN_SECRET as string,
|
process.secrets.ACCESS_TOKEN_SECRET,
|
||||||
'accessToken'
|
'accessToken'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
export const authenticateRefreshToken: RequestHandler = async (
|
||||||
authenticateToken(
|
req,
|
||||||
|
res,
|
||||||
|
next
|
||||||
|
) => {
|
||||||
|
await authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next,
|
next,
|
||||||
process.env.REFRESH_TOKEN_SECRET as string,
|
process.secrets.REFRESH_TOKEN_SECRET,
|
||||||
'refreshToken'
|
'refreshToken'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticateToken = (
|
const authenticateToken = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
@@ -58,7 +74,7 @@ const authenticateToken = (
|
|||||||
tokenType: 'accessToken' | 'refreshToken'
|
tokenType: 'accessToken' | 'refreshToken'
|
||||||
) => {
|
) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server') {
|
if (MODE === ModeType.Desktop) {
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: 1234,
|
userId: 1234,
|
||||||
clientId: 'desktopModeClientId',
|
clientId: 'desktopModeClientId',
|
||||||
@@ -73,12 +89,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,
|
||||||
@@ -91,8 +107,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
api/src/middlewares/authorize.ts
Normal file
49
api/src/middlewares/authorize.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { RequestHandler } from 'express'
|
||||||
|
import User from '../model/User'
|
||||||
|
import Permission from '../model/Permission'
|
||||||
|
import {
|
||||||
|
PermissionSettingForRoute,
|
||||||
|
PermissionType
|
||||||
|
} from '../controllers/permission'
|
||||||
|
import { getPath, isPublicRoute } from '../utils'
|
||||||
|
|
||||||
|
export const authorize: RequestHandler = async (req, res, next) => {
|
||||||
|
const { user } = req
|
||||||
|
|
||||||
|
if (!user) return res.sendStatus(401)
|
||||||
|
|
||||||
|
// no need to check for permissions when user is admin
|
||||||
|
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 })
|
||||||
|
if (!dbUser) return res.sendStatus(401)
|
||||||
|
|
||||||
|
const path = getPath(req)
|
||||||
|
|
||||||
|
// find permission w.r.t user
|
||||||
|
const permission = await Permission.findOne({
|
||||||
|
path,
|
||||||
|
type: PermissionType.route,
|
||||||
|
user: dbUser._id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (permission) {
|
||||||
|
if (permission.setting === PermissionSettingForRoute.grant) return next()
|
||||||
|
else return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
|
||||||
|
// find permission w.r.t user's groups
|
||||||
|
for (const group of dbUser.groups) {
|
||||||
|
const groupPermission = await Permission.findOne({
|
||||||
|
path,
|
||||||
|
type: PermissionType.route,
|
||||||
|
group
|
||||||
|
})
|
||||||
|
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
return res.sendStatus(401)
|
||||||
|
}
|
||||||
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()
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
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'
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { RequestHandler } from 'express'
|
import { RequestHandler } from 'express'
|
||||||
|
import { ModeType } from '../utils'
|
||||||
|
|
||||||
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server') return next()
|
if (MODE === ModeType.Desktop) return next()
|
||||||
|
|
||||||
const { user } = req
|
const { user } = req
|
||||||
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
||||||
|
|||||||
45
api/src/model/Configuration.ts
Normal file
45
api/src/model/Configuration.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import mongoose, { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
export interface ConfigurationType {
|
||||||
|
/**
|
||||||
|
* SecretOrPrivateKey to sign Access Token
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
ACCESS_TOKEN_SECRET: string
|
||||||
|
/**
|
||||||
|
* SecretOrPrivateKey to sign Refresh Token
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
REFRESH_TOKEN_SECRET: string
|
||||||
|
/**
|
||||||
|
* SecretOrPrivateKey to sign Auth Code
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
AUTH_CODE_SECRET: string
|
||||||
|
/**
|
||||||
|
* Secret used to sign the session cookie
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
SESSION_SECRET: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfigurationSchema = new Schema<ConfigurationType>({
|
||||||
|
ACCESS_TOKEN_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
REFRESH_TOKEN_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
AUTH_CODE_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
SESSION_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mongoose.model('Configuration', ConfigurationSchema)
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||||
import User from './User'
|
import { GroupDetailsResponse } from '../controllers'
|
||||||
|
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
|
||||||
@@ -24,11 +28,13 @@ 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 {
|
||||||
addUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
addUser(user: IUser): Promise<GroupDetailsResponse>
|
||||||
removeUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
removeUser(user: IUser): Promise<GroupDetailsResponse>
|
||||||
|
hasUser(user: IUser): boolean
|
||||||
}
|
}
|
||||||
interface IGroupModel extends Model<IGroup> {}
|
interface IGroupModel extends Model<IGroup> {}
|
||||||
|
|
||||||
@@ -42,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
|
||||||
@@ -70,28 +80,31 @@ groupSchema.pre('remove', async function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Instance Methods
|
// Instance Methods
|
||||||
groupSchema.method(
|
groupSchema.method('addUser', async function (user: IUser) {
|
||||||
'addUser',
|
const userObjectId = user._id
|
||||||
async function (userObjectId: Schema.Types.ObjectId) {
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
if (userIdIndex === -1) {
|
||||||
if (userIdIndex === -1) {
|
this.users.push(userObjectId)
|
||||||
this.users.push(userObjectId)
|
user.addGroup(this._id)
|
||||||
}
|
|
||||||
this.markModified('users')
|
|
||||||
return this.save()
|
|
||||||
}
|
}
|
||||||
)
|
this.markModified('users')
|
||||||
groupSchema.method(
|
return this.save()
|
||||||
'removeUser',
|
})
|
||||||
async function (userObjectId: Schema.Types.ObjectId) {
|
groupSchema.method('removeUser', async function (user: IUser) {
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
const userObjectId = user._id
|
||||||
if (userIdIndex > -1) {
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
this.users.splice(userIdIndex, 1)
|
if (userIdIndex > -1) {
|
||||||
}
|
this.users.splice(userIdIndex, 1)
|
||||||
this.markModified('users')
|
user.removeGroup(this._id)
|
||||||
return this.save()
|
|
||||||
}
|
}
|
||||||
)
|
this.markModified('users')
|
||||||
|
return this.save()
|
||||||
|
})
|
||||||
|
groupSchema.method('hasUser', function (user: IUser) {
|
||||||
|
const userObjectId = user._id
|
||||||
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
|
return userIdIndex > -1
|
||||||
|
})
|
||||||
|
|
||||||
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
||||||
'Group',
|
'Group',
|
||||||
|
|||||||
73
api/src/model/Permission.ts
Normal file
73
api/src/model/Permission.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import mongoose, { Schema, model, Document, Model } from '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 {
|
||||||
|
path: string
|
||||||
|
type: string
|
||||||
|
setting: string
|
||||||
|
permissionId: number
|
||||||
|
user: Schema.Types.ObjectId
|
||||||
|
group: Schema.Types.ObjectId
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPermission extends IPermissionDocument {}
|
||||||
|
|
||||||
|
interface IPermissionModel extends Model<IPermission> {
|
||||||
|
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissionSchema = new Schema<IPermissionDocument>({
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
setting: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
user: { type: Schema.Types.ObjectId, ref: 'User' },
|
||||||
|
group: { type: Schema.Types.ObjectId, ref: 'Group' }
|
||||||
|
})
|
||||||
|
|
||||||
|
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<
|
||||||
|
IPermission,
|
||||||
|
IPermissionModel
|
||||||
|
>('Permission', permissionSchema)
|
||||||
|
|
||||||
|
export default Permission
|
||||||
@@ -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 {
|
||||||
/**
|
/**
|
||||||
@@ -35,15 +36,17 @@ export interface UserPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IUserDocument extends UserPayload, Document {
|
interface IUserDocument extends UserPayload, Document {
|
||||||
|
_id: Schema.Types.ObjectId
|
||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
autoExec: string
|
autoExec: string
|
||||||
groups: Schema.Types.ObjectId[]
|
groups: Schema.Types.ObjectId[]
|
||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
|
authProvider?: AuthProviderType
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUser extends IUserDocument {
|
export interface IUser extends IUserDocument {
|
||||||
comparePassword(password: string): boolean
|
comparePassword(password: string): boolean
|
||||||
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||||
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||||
@@ -66,6 +69,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
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
authenticateRefreshToken
|
authenticateRefreshToken
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
import { authorizeValidation, tokenValidation } from '../../utils'
|
import { tokenValidation } from '../../utils'
|
||||||
import { InfoJWT } from '../../types'
|
import { InfoJWT } from '../../types'
|
||||||
|
|
||||||
const authRouter = express.Router()
|
const authRouter = express.Router()
|
||||||
|
|||||||
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
|
||||||
@@ -11,8 +11,10 @@ import {
|
|||||||
extractName,
|
extractName,
|
||||||
fileBodyValidation,
|
fileBodyValidation,
|
||||||
fileParamValidation,
|
fileParamValidation,
|
||||||
|
folderBodyValidation,
|
||||||
folderParamValidation,
|
folderParamValidation,
|
||||||
isZipFile
|
isZipFile,
|
||||||
|
renameBodyValidation
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|
||||||
const controller = new DriveController()
|
const controller = new DriveController()
|
||||||
@@ -119,7 +121,11 @@ driveRouter.get('/file', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await controller.getFile(req, query._filePath)
|
await controller.getFile(req, query._filePath)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -132,7 +138,11 @@ driveRouter.get('/folder', async (req, res) => {
|
|||||||
const response = await controller.getFolder(query._folderPath)
|
const response = await controller.getFolder(query._folderPath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -145,7 +155,28 @@ driveRouter.delete('/file', async (req, res) => {
|
|||||||
const response = await controller.deleteFile(query._filePath)
|
const response = await controller.deleteFile(query._filePath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
driveRouter.delete('/folder', async (req, res) => {
|
||||||
|
const { error: errQ, value: query } = folderParamValidation(req.query, true)
|
||||||
|
|
||||||
|
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.deleteFolder(query._folderPath)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -172,11 +203,33 @@ driveRouter.post(
|
|||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await deleteFile(req.file.path)
|
await deleteFile(req.file.path)
|
||||||
res.status(403).send(err.toString())
|
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
driveRouter.post('/folder', async (req, res) => {
|
||||||
|
const { error, value: body } = folderBodyValidation(req.body)
|
||||||
|
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.addFolder(body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
driveRouter.patch(
|
driveRouter.patch(
|
||||||
'/file',
|
'/file',
|
||||||
(...arg) => multerSingle('file', arg),
|
(...arg) => multerSingle('file', arg),
|
||||||
@@ -200,11 +253,33 @@ driveRouter.patch(
|
|||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await deleteFile(req.file.path)
|
await deleteFile(req.file.path)
|
||||||
res.status(403).send(err.toString())
|
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
driveRouter.post('/rename', async (req, res) => {
|
||||||
|
const { error, value: body } = renameBodyValidation(req.body)
|
||||||
|
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.rename(body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
driveRouter.get('/fileTree', async (req, res) => {
|
driveRouter.get('/fileTree', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await controller.getFileTree()
|
const response = await controller.getFileTree()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import groupRouter from './group'
|
|||||||
import clientRouter from './client'
|
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 authConfigRouter from './authConfig'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
@@ -35,6 +37,20 @@ router.use('/group', desktopRestrict, groupRouter)
|
|||||||
router.use('/stp', authenticateAccessToken, stpRouter)
|
router.use('/stp', authenticateAccessToken, stpRouter)
|
||||||
router.use('/code', authenticateAccessToken, codeRouter)
|
router.use('/code', authenticateAccessToken, codeRouter)
|
||||||
router.use('/user', desktopRestrict, userRouter)
|
router.use('/user', desktopRestrict, userRouter)
|
||||||
|
router.use(
|
||||||
|
'/permission',
|
||||||
|
desktopRestrict,
|
||||||
|
authenticateAccessToken,
|
||||||
|
permissionRouter
|
||||||
|
)
|
||||||
|
|
||||||
|
router.use(
|
||||||
|
'/authConfig',
|
||||||
|
desktopRestrict,
|
||||||
|
authenticateAccessToken,
|
||||||
|
verifyAdmin,
|
||||||
|
authConfigRouter
|
||||||
|
)
|
||||||
|
|
||||||
router.use(
|
router.use(
|
||||||
'/',
|
'/',
|
||||||
|
|||||||
@@ -13,4 +13,14 @@ infoRouter.get('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
infoRouter.get('/authorizedRoutes', async (req, res) => {
|
||||||
|
const controller = new InfoController()
|
||||||
|
try {
|
||||||
|
const response = controller.authorizedRoutes()
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default infoRouter
|
export default infoRouter
|
||||||
|
|||||||
69
api/src/routes/api/permission.ts
Normal file
69
api/src/routes/api/permission.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { PermissionController } from '../../controllers/'
|
||||||
|
import { verifyAdmin } from '../../middlewares'
|
||||||
|
import {
|
||||||
|
registerPermissionValidation,
|
||||||
|
updatePermissionValidation
|
||||||
|
} from '../../utils'
|
||||||
|
|
||||||
|
const permissionRouter = express.Router()
|
||||||
|
const controller = new PermissionController()
|
||||||
|
|
||||||
|
permissionRouter.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await controller.getAllPermissions(req)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
delete err.code
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
permissionRouter.post('/', verifyAdmin, async (req, res) => {
|
||||||
|
const { error, value: body } = registerPermissionValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.createPermission(body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
delete err.code
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => {
|
||||||
|
const { permissionId } = req.params
|
||||||
|
|
||||||
|
const { error, value: body } = updatePermissionValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.updatePermission(permissionId, body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
delete err.code
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
permissionRouter.delete(
|
||||||
|
'/:permissionId',
|
||||||
|
verifyAdmin,
|
||||||
|
async (req: any, res) => {
|
||||||
|
const { permissionId } = req.params
|
||||||
|
|
||||||
|
try {
|
||||||
|
await controller.deletePermission(permissionId)
|
||||||
|
res.status(200).send('Permission Deleted!')
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
delete err.code
|
||||||
|
res.status(statusCode).send(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
export default permissionRouter
|
||||||
@@ -29,7 +29,13 @@ jest
|
|||||||
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||||
|
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import {
|
||||||
|
UserController,
|
||||||
|
PermissionController,
|
||||||
|
PermissionType,
|
||||||
|
PermissionSettingForRoute,
|
||||||
|
PrincipalType
|
||||||
|
} from '../../../controllers/'
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||||
const { getFilesFolder } = fileUtilModules
|
const { getFilesFolder } = fileUtilModules
|
||||||
@@ -43,11 +49,18 @@ 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
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
|
const permissionController = new PermissionController()
|
||||||
|
|
||||||
let accessToken: string
|
let accessToken: string
|
||||||
|
|
||||||
@@ -58,11 +71,32 @@ describe('drive', () => {
|
|||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
accessToken = generateAccessToken({
|
accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
clientId,
|
await permissionController.createPermission({
|
||||||
userId: dbUser.id
|
...permission,
|
||||||
|
path: '/SASjsApi/drive/deploy',
|
||||||
|
principalId: dbUser.id
|
||||||
|
})
|
||||||
|
await permissionController.createPermission({
|
||||||
|
...permission,
|
||||||
|
path: '/SASjsApi/drive/deploy/upload',
|
||||||
|
principalId: dbUser.id
|
||||||
|
})
|
||||||
|
await permissionController.createPermission({
|
||||||
|
...permission,
|
||||||
|
path: '/SASjsApi/drive/file',
|
||||||
|
principalId: dbUser.id
|
||||||
|
})
|
||||||
|
await permissionController.createPermission({
|
||||||
|
...permission,
|
||||||
|
path: '/SASjsApi/drive/folder',
|
||||||
|
principalId: dbUser.id
|
||||||
|
})
|
||||||
|
await permissionController.createPermission({
|
||||||
|
...permission,
|
||||||
|
path: '/SASjsApi/drive/rename',
|
||||||
|
principalId: dbUser.id
|
||||||
})
|
})
|
||||||
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -517,29 +551,29 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if folder is not present', async () => {
|
it('should respond with Not Found if folder is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
||||||
.expect(403)
|
.expect(404)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
|
expect(res.text).toEqual(`Folder doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if folderPath outside Drive', async () => {
|
it('should respond with Bad Request if folderPath outside Drive', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: '/../path/code.sas' })
|
.query({ _folderPath: '/../path/code.sas' })
|
||||||
.expect(403)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
|
expect(res.text).toEqual(`Can't get folder outside drive.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if folderPath is of a file', async () => {
|
it('should respond with Bad Request if folderPath is of a file', async () => {
|
||||||
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const filePath = '/my/path/code.sas'
|
const filePath = '/my/path/code.sas'
|
||||||
|
|
||||||
@@ -550,12 +584,96 @@ describe('drive', () => {
|
|||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: filePath })
|
.query({ _folderPath: filePath })
|
||||||
.expect(403)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Not a Folder.')
|
expect(res.text).toEqual('Not a Folder.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('post', () => {
|
||||||
|
const folderApi = '/SASjsApi/drive/folder'
|
||||||
|
const pathToDrive = fileUtilModules.getFilesFolder()
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await deleteFolder(path.join(pathToDrive, 'post'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create a folder on drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ folderPath: '/post/folder' })
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Conflict if the folder already exists', async () => {
|
||||||
|
await createFolder(path.join(pathToDrive, '/post/folder'))
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ folderPath: '/post/folder' })
|
||||||
|
.expect(409)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Folder already exists.`)
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(409)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the folderPath is outside drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ folderPath: '../sample' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Can't put folder outside drive.`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
const folderApi = '/SASjsApi/drive/folder'
|
||||||
|
const pathToDrive = fileUtilModules.getFilesFolder()
|
||||||
|
|
||||||
|
it('should delete a folder on drive', async () => {
|
||||||
|
await createFolder(path.join(pathToDrive, 'delete'))
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _folderPath: 'delete' })
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Not Found if the folder does not exists', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _folderPath: 'notExists' })
|
||||||
|
.expect(404)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Folder doesn't exist.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the folderPath is outside drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(folderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _folderPath: '../outsideDrive' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Can't delete folder outside drive.`)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('file', () => {
|
describe('file', () => {
|
||||||
@@ -601,7 +719,7 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if file is already present', async () => {
|
it('should respond with Conflict if file is already present', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
||||||
|
|
||||||
@@ -616,13 +734,13 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(403)
|
.expect(409)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: File already exists.')
|
expect(res.text).toEqual('File already exists.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
it('should respond with Bad Request if filePath outside Drive', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = '/../path/code.sas'
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
@@ -631,9 +749,9 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(403)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Cannot put file outside drive.')
|
expect(res.text).toEqual(`Can't put file outside drive.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -768,19 +886,19 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if file is not present', async () => {
|
it('should respond with Not Found if file is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch('/SASjsApi/drive/file')
|
.patch('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', `/my/path/code-3.sas`)
|
.field('filePath', `/my/path/code-3.sas`)
|
||||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
.expect(403)
|
.expect(404)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
expect(res.text).toEqual(`File doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
it('should respond with Bad Request if filePath outside Drive', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = '/../path/code.sas'
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
@@ -789,9 +907,9 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(403)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
|
expect(res.text).toEqual(`Can't modify file outside drive.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -896,25 +1014,25 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if file is not present', async () => {
|
it('should respond with Not Found if file is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/drive/file')
|
.get('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _filePath: `/my/path/code-4.sas` })
|
.query({ _filePath: `/my/path/code-4.sas` })
|
||||||
.expect(403)
|
.expect(404)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
expect(res.text).toEqual(`File doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
it('should respond with Bad Request if filePath outside Drive', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/drive/file')
|
.get('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _filePath: '/../path/code.sas' })
|
.query({ _filePath: '/../path/code.sas' })
|
||||||
.expect(403)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Cannot get file outside drive.')
|
expect(res.text).toEqual(`Can't get file outside drive.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -940,8 +1058,150 @@ describe('drive', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('rename', () => {
|
||||||
|
const renameApi = '/SASjsApi/drive/rename'
|
||||||
|
const pathToDrive = fileUtilModules.getFilesFolder()
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await deleteFolder(path.join(pathToDrive, 'rename'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should rename a folder', async () => {
|
||||||
|
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '/rename/folder', newPath: '/rename/renamed' })
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should rename a file', async () => {
|
||||||
|
await createFile(
|
||||||
|
path.join(pathToDrive, 'rename', 'file.txt'),
|
||||||
|
'some file content'
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
oldPath: '/rename/file.txt',
|
||||||
|
newPath: '/rename/renamed.txt'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({
|
||||||
|
status: 'success'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the oldPath is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ newPath: 'newPath' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`\"oldPath\" is required`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the newPath is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: 'oldPath' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`\"newPath\" is required`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the oldPath is outside drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '../outside', newPath: 'renamed' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Old path can't be outside of drive.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if the newPath is outside drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: 'older', newPath: '../outside' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`New path can't be outside of drive.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Not Found if the folder does not exist', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '/rename/not exists', newPath: '/rename/renamed' })
|
||||||
|
.expect(404)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('No file/folder found for provided path.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Conflict if the folder already exists', async () => {
|
||||||
|
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
|
||||||
|
await createFolder(path.join(pathToDrive, 'rename', 'exists'))
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '/rename/folder', newPath: '/rename/exists' })
|
||||||
|
.expect(409)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Folder with new name already exists.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Not Found if the file does not exist', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '/rename/file.txt', newPath: '/rename/renamed.txt' })
|
||||||
|
.expect(404)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('No file/folder found for provided path.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Conflict if the file already exists', async () => {
|
||||||
|
await createFile(
|
||||||
|
path.join(pathToDrive, 'rename', 'file.txt'),
|
||||||
|
'some file content'
|
||||||
|
)
|
||||||
|
await createFile(
|
||||||
|
path.join(pathToDrive, 'rename', 'exists.txt'),
|
||||||
|
'some existing content'
|
||||||
|
)
|
||||||
|
const res = await request(app)
|
||||||
|
.post(renameApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ oldPath: '/rename/file.txt', newPath: '/rename/exists.txt' })
|
||||||
|
.expect(409)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('File with new name already exists.')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const getExampleService = (): ServiceMember =>
|
const getExampleService = (): ServiceMember =>
|
||||||
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
|
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
|
||||||
.members[0] as ServiceMember
|
.members[0] as ServiceMember
|
||||||
|
|
||||||
|
const generateAndSaveToken = async (userId: number) => {
|
||||||
|
const adminAccessToken = generateAccessToken({
|
||||||
|
clientId,
|
||||||
|
userId
|
||||||
|
})
|
||||||
|
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
|
||||||
|
return adminAccessToken
|
||||||
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
596
api/src/routes/api/spec/permission.spec.ts
Normal file
596
api/src/routes/api/spec/permission.spec.ts
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
|
import request from 'supertest'
|
||||||
|
import appPromise from '../../../app'
|
||||||
|
import {
|
||||||
|
DriveController,
|
||||||
|
UserController,
|
||||||
|
GroupController,
|
||||||
|
PermissionController,
|
||||||
|
PrincipalType,
|
||||||
|
PermissionType,
|
||||||
|
PermissionSettingForRoute
|
||||||
|
} from '../../../controllers/'
|
||||||
|
import {
|
||||||
|
UserDetailsResponse,
|
||||||
|
PermissionDetailsResponse
|
||||||
|
} from '../../../controllers'
|
||||||
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
|
const deployPayload = {
|
||||||
|
appLoc: 'string',
|
||||||
|
streamWebFolder: 'string',
|
||||||
|
fileTree: {
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
name: 'string',
|
||||||
|
type: 'folder',
|
||||||
|
members: [
|
||||||
|
'string',
|
||||||
|
{
|
||||||
|
name: 'string',
|
||||||
|
type: 'service',
|
||||||
|
code: 'string'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = 'someclientID'
|
||||||
|
const adminUser = {
|
||||||
|
displayName: 'Test Admin',
|
||||||
|
username: 'testAdminUsername',
|
||||||
|
password: '12345678',
|
||||||
|
isAdmin: true,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
const user = {
|
||||||
|
displayName: 'Test User',
|
||||||
|
username: 'testUsername',
|
||||||
|
password: '87654321',
|
||||||
|
isAdmin: false,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = {
|
||||||
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: PermissionType.route,
|
||||||
|
setting: PermissionSettingForRoute.grant,
|
||||||
|
principalType: PrincipalType.user
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = {
|
||||||
|
name: 'DCGroup1',
|
||||||
|
description: 'DC group for testing purposes.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const userController = new UserController()
|
||||||
|
const groupController = new GroupController()
|
||||||
|
const permissionController = new PermissionController()
|
||||||
|
|
||||||
|
describe('permission', () => {
|
||||||
|
let app: Express
|
||||||
|
let con: Mongoose
|
||||||
|
let mongoServer: MongoMemoryServer
|
||||||
|
let adminAccessToken: string
|
||||||
|
let dbUser: UserDetailsResponse
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
|
mongoServer = await MongoMemoryServer.create()
|
||||||
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
|
adminAccessToken = await generateSaveTokenAndCreateUser()
|
||||||
|
dbUser = await userController.createUser(user)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await con.connection.dropDatabase()
|
||||||
|
await con.connection.close()
|
||||||
|
await mongoServer.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await deleteAllPermissions()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with new permission when principalType is user', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({ ...permission, principalId: dbUser.id })
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.permissionId).toBeTruthy()
|
||||||
|
expect(res.body.path).toEqual(permission.path)
|
||||||
|
expect(res.body.type).toEqual(permission.type)
|
||||||
|
expect(res.body.setting).toEqual(permission.setting)
|
||||||
|
expect(res.body.user).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with new permission when principalType is group', async () => {
|
||||||
|
const dbGroup = await groupController.createGroup(group)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
principalType: 'group',
|
||||||
|
principalId: dbGroup.groupId
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.permissionId).toBeTruthy()
|
||||||
|
expect(res.body.path).toEqual(permission.path)
|
||||||
|
expect(res.body.type).toEqual(permission.type)
|
||||||
|
expect(res.body.setting).toEqual(permission.setting)
|
||||||
|
expect(res.body.group).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.send(permission)
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send(permission)
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Admin account required')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if path is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
path: undefined
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"path" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if path is not valid', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
path: '/some/random/api/endpoint'
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
setting: undefined
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"setting" is required`)
|
||||||
|
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 () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
principalType: undefined
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"principalType" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if principal type is not valid', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
principalType: 'invalid'
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('"principalType" must be one of [user, group]')
|
||||||
|
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 principalId is not a number', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
principalId: 'someCharacters'
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('"principalId" must be a number')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if adding permission for admin user', async () => {
|
||||||
|
const adminUser = await userController.createUser({
|
||||||
|
...user,
|
||||||
|
username: 'adminUser',
|
||||||
|
isAdmin: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
principalId: adminUser.id
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Can not add permission for admin user.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Not Found (404) if user is not found', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
principalId: 123
|
||||||
|
})
|
||||||
|
.expect(404)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('User not found.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Not Found (404) if group is not found', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
principalType: 'group',
|
||||||
|
principalId: 123
|
||||||
|
})
|
||||||
|
.expect(404)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Group not found.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Conflict (409) if permission already exists', async () => {
|
||||||
|
await permissionController.createPermission({
|
||||||
|
...permission,
|
||||||
|
principalId: dbUser.id
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({ ...permission, principalId: dbUser.id })
|
||||||
|
.expect(409)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
'Permission already exists with provided Path, Type and User.'
|
||||||
|
)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
let dbPermission: PermissionDetailsResponse | undefined
|
||||||
|
beforeAll(async () => {
|
||||||
|
dbPermission = await permissionController.createPermission({
|
||||||
|
...permission,
|
||||||
|
principalId: dbUser.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await deleteAllPermissions()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with updated permission', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({ setting: PermissionSettingForRoute.deny })
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.setting).toEqual('Deny')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
|
.send()
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
||||||
|
const accessToken = await generateSaveTokenAndCreateUser({
|
||||||
|
...user,
|
||||||
|
username: 'update' + user.username
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Admin account required')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if setting is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"setting" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if setting is invalid', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
setting: 'invalid'
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with not found (404) if permission with provided id does not exist', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/permission/123')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
setting: PermissionSettingForRoute.deny
|
||||||
|
})
|
||||||
|
.expect(404)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Permission not found.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should delete permission', async () => {
|
||||||
|
const dbPermission = await permissionController.createPermission({
|
||||||
|
...permission,
|
||||||
|
principalId: dbUser.id
|
||||||
|
})
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Permission Deleted!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with not found (404) if permission with provided id does not exists', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.delete('/SASjsApi/permission/123')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(404)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Permission not found.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await permissionController.createPermission({
|
||||||
|
...permission,
|
||||||
|
path: '/test-1',
|
||||||
|
principalId: dbUser.id
|
||||||
|
})
|
||||||
|
await permissionController.createPermission({
|
||||||
|
...permission,
|
||||||
|
path: '/test-2',
|
||||||
|
principalId: dbUser.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should give a list of all permissions when user is admin', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/SASjsApi/permission/')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`should give a list of user's own permissions when user is not admin`, async () => {
|
||||||
|
const nonAdminUser = await userController.createUser({
|
||||||
|
...user,
|
||||||
|
username: 'get' + user.username
|
||||||
|
})
|
||||||
|
const accessToken = await generateAndSaveToken(nonAdminUser.id)
|
||||||
|
await permissionController.createPermission({
|
||||||
|
path: '/test-1',
|
||||||
|
type: PermissionType.route,
|
||||||
|
principalType: PrincipalType.user,
|
||||||
|
principalId: nonAdminUser.id,
|
||||||
|
setting: PermissionSettingForRoute.grant
|
||||||
|
})
|
||||||
|
|
||||||
|
const permissionCount = 1
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/SASjsApi/permission/')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body).toHaveLength(permissionCount)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('verify', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await permissionController.createPermission({
|
||||||
|
...permission,
|
||||||
|
path: '/SASjsApi/drive/deploy',
|
||||||
|
principalId: dbUser.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest
|
||||||
|
.spyOn(DriveController.prototype, 'deploy')
|
||||||
|
.mockImplementation((deployPayload) =>
|
||||||
|
Promise.resolve({
|
||||||
|
status: 'success',
|
||||||
|
message: 'Files deployed successfully to @sasjs/server.'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create files in SASJS drive', async () => {
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get('/SASjsApi/drive/deploy')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send(deployPayload)
|
||||||
|
.expect(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond unauthorized', async () => {
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get('/SASjsApi/drive/deploy/upload')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(401)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const generateSaveTokenAndCreateUser = async (
|
||||||
|
someUser?: any
|
||||||
|
): Promise<string> => {
|
||||||
|
const dbUser = await userController.createUser(someUser ?? adminUser)
|
||||||
|
|
||||||
|
return generateAndSaveToken(dbUser.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateAndSaveToken = async (userId: number) => {
|
||||||
|
const adminAccessToken = generateAccessToken({
|
||||||
|
clientId,
|
||||||
|
userId
|
||||||
|
})
|
||||||
|
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
|
||||||
|
return adminAccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAllPermissions = async () => {
|
||||||
|
const { collections } = mongoose.connection
|
||||||
|
const collection = collections['permissions']
|
||||||
|
await collection.deleteMany({})
|
||||||
|
}
|
||||||
@@ -4,7 +4,13 @@ import mongoose, { Mongoose } from 'mongoose'
|
|||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import {
|
||||||
|
UserController,
|
||||||
|
PermissionController,
|
||||||
|
PermissionType,
|
||||||
|
PermissionSettingForRoute,
|
||||||
|
PrincipalType
|
||||||
|
} from '../../../controllers/'
|
||||||
import {
|
import {
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
saveTokensInDB,
|
saveTokensInDB,
|
||||||
@@ -15,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'
|
||||||
@@ -33,20 +39,33 @@ 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 permissionController = new PermissionController()
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await appPromise
|
app = await appPromise
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
accessToken = await generateSaveTokenAndCreateUser(user)
|
const dbUser = await userController.createUser(user)
|
||||||
|
accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
await permissionController.createPermission({
|
||||||
|
path: '/SASjsApi/stp/execute',
|
||||||
|
type: PermissionType.route,
|
||||||
|
principalType: PrincipalType.user,
|
||||||
|
principalId: dbUser.id,
|
||||||
|
setting: PermissionSettingForRoute.grant
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -56,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()}`
|
||||||
@@ -77,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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -131,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)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -185,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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -261,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) => {
|
||||||
@@ -348,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({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -770,12 +805,14 @@ describe('user', () => {
|
|||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: adminUser.username,
|
username: adminUser.username,
|
||||||
displayName: adminUser.displayName
|
displayName: adminUser.displayName,
|
||||||
|
isAdmin: adminUser.isAdmin
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName
|
displayName: user.displayName,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -796,12 +833,14 @@ describe('user', () => {
|
|||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: adminUser.username,
|
username: adminUser.username,
|
||||||
displayName: adminUser.displayName
|
displayName: adminUser.displayName,
|
||||||
|
isAdmin: adminUser.isAdmin
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: 'randomUser',
|
username: 'randomUser',
|
||||||
displayName: user.displayName
|
displayName: user.displayName,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -39,21 +39,19 @@ 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/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 +65,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,
|
||||||
@@ -79,18 +76,49 @@ describe('web', () => {
|
|||||||
expect(res.body.user).toEqual({
|
expect(res.body.user).toEqual({
|
||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName
|
displayName: user.displayName,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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(res.text).toEqual('Invalid CSRF token!')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if CSRF Token is invalid', async () => {
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
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({})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('SASLogon/authorize', () => {
|
describe('SASLogon/authorize', () => {
|
||||||
let csrfToken: string
|
let csrfToken: string
|
||||||
let cookies: string
|
|
||||||
let authCookies: string
|
let authCookies: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
;({ csrfToken, cookies } = await getCSRF(app))
|
;({ csrfToken } = await getCSRF(app))
|
||||||
|
|
||||||
await userController.createUser(user)
|
await userController.createUser(user)
|
||||||
|
|
||||||
@@ -99,12 +127,7 @@ describe('web', () => {
|
|||||||
password: user.password
|
password: user.password
|
||||||
}
|
}
|
||||||
|
|
||||||
;({ cookies: authCookies } = await performLogin(
|
;({ authCookies } = await performLogin(app, credentials, csrfToken))
|
||||||
app,
|
|
||||||
credentials,
|
|
||||||
cookies,
|
|
||||||
csrfToken
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -116,17 +139,28 @@ describe('web', () => {
|
|||||||
it('should respond with authorization code', async () => {
|
it('should respond with authorization code', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASLogon/authorize')
|
.post('/SASLogon/authorize')
|
||||||
.set('Cookie', [authCookies, cookies].join('; '))
|
.set('Cookie', [authCookies].join('; '))
|
||||||
.set('x-xsrf-token', csrfToken)
|
.set('x-xsrf-token', csrfToken)
|
||||||
.send({ clientId })
|
.send({ clientId })
|
||||||
|
|
||||||
expect(res.body).toHaveProperty('code')
|
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 () => {
|
it('should respond with Bad Request if clientId is missing', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASLogon/authorize')
|
.post('/SASLogon/authorize')
|
||||||
.set('Cookie', [authCookies, cookies].join('; '))
|
.set('Cookie', [authCookies].join('; '))
|
||||||
.set('x-xsrf-token', csrfToken)
|
.set('x-xsrf-token', csrfToken)
|
||||||
.send({})
|
.send({})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
@@ -138,7 +172,7 @@ describe('web', () => {
|
|||||||
it('should respond with Forbidden if clientId is incorrect', async () => {
|
it('should respond with Forbidden if clientId is incorrect', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASLogon/authorize')
|
.post('/SASLogon/authorize')
|
||||||
.set('Cookie', [authCookies, cookies].join('; '))
|
.set('Cookie', [authCookies].join('; '))
|
||||||
.set('x-xsrf-token', csrfToken)
|
.set('x-xsrf-token', csrfToken)
|
||||||
.send({
|
.send({
|
||||||
clientId: 'WrongClientID'
|
clientId: 'WrongClientID'
|
||||||
@@ -153,30 +187,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,5 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { Request } from 'express'
|
import express, { Request } from 'express'
|
||||||
|
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'
|
||||||
@@ -9,10 +10,10 @@ const appStreams: { [key: string]: string } = {}
|
|||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.get('/', 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)
|
||||||
})
|
})
|
||||||
@@ -66,7 +67,7 @@ export const publishAppStream = async (
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get(`/*`, function (req: Request, res, next) {
|
router.get(`/*`, authenticateAccessToken, function (req: Request, res, next) {
|
||||||
const reqPath = req.path.replace(/^\//, '')
|
const reqPath = req.path.replace(/^\//, '')
|
||||||
|
|
||||||
// Redirecting to url with trailing slash for appStream base URL only
|
// Redirecting to url with trailing slash for appStream base URL only
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
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'
|
||||||
|
|
||||||
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('/', webRouter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
119
api/src/routes/web/sas9-web.ts
Normal file
119
api/src/routes/web/sas9-web.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { generateCSRFToken } from '../../middlewares'
|
||||||
|
import { WebController } from '../../controllers'
|
||||||
|
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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/', async (req, res) => {
|
||||||
|
const response = await controller.sasStoredProcessDo(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,4 +1,5 @@
|
|||||||
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, desktopRestrict } from '../../middlewares'
|
||||||
import { authorizeValidation, loginWebValidation } from '../../utils'
|
import { authorizeValidation, loginWebValidation } from '../../utils'
|
||||||
@@ -11,11 +12,15 @@ 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 codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||||
|
const injectedContent = response?.replace(
|
||||||
|
'</head>',
|
||||||
|
`${codeToInject}</head>`
|
||||||
|
)
|
||||||
|
|
||||||
return res.send(response)
|
return res.send(injectedContent)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ export interface TreeNode {
|
|||||||
name: string
|
name: string
|
||||||
relativePath: string
|
relativePath: string
|
||||||
absolutePath: string
|
absolutePath: string
|
||||||
|
isFolder: boolean
|
||||||
children: Array<TreeNode>
|
children: Array<TreeNode>
|
||||||
}
|
}
|
||||||
|
|||||||
8
api/src/types/system/process.d.ts
vendored
8
api/src/types/system/process.d.ts
vendored
@@ -2,11 +2,15 @@ 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
|
||||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
logsLoc: string
|
||||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
logsUUID: string
|
||||||
|
sessionController?: import('../../controllers/internal').SessionController
|
||||||
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[]
|
||||||
|
secrets: import('../../model/Configuration').ConfigurationType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { AppStreamConfig } from '../types'
|
|||||||
import { getAppStreamConfigPath } from './file'
|
import { getAppStreamConfigPath } from './file'
|
||||||
|
|
||||||
export const loadAppStreamConfig = async () => {
|
export const loadAppStreamConfig = async () => {
|
||||||
|
process.appStreamConfig = {}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') return
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
const appStreamConfigPath = getAppStreamConfigPath()
|
const appStreamConfigPath = getAppStreamConfigPath()
|
||||||
@@ -21,7 +23,6 @@ export const loadAppStreamConfig = async () => {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
appStreamConfig = {}
|
appStreamConfig = {}
|
||||||
}
|
}
|
||||||
process.appStreamConfig = {}
|
|
||||||
|
|
||||||
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
|
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
|
||||||
const { appLoc, streamWebFolder, streamLogo } = entry
|
const { appLoc, streamWebFolder, streamLogo } = entry
|
||||||
|
|||||||
@@ -9,7 +9,5 @@ export const connectDB = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Connected to DB!')
|
console.log('Connected to DB!')
|
||||||
await seedDB()
|
return seedDB()
|
||||||
|
|
||||||
return mongoose.connection
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -22,18 +22,21 @@ export const getDesktopUserAutoExecPath = () =>
|
|||||||
|
|
||||||
export const getSasjsRootFolder = () => process.driveLoc
|
export const getSasjsRootFolder = () => process.driveLoc
|
||||||
|
|
||||||
|
export const getLogFolder = () => process.logsLoc
|
||||||
|
|
||||||
export const getAppStreamConfigPath = () =>
|
export const getAppStreamConfigPath = () =>
|
||||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
||||||
|
|
||||||
export const getMacrosFolder = () =>
|
export const getMacrosFolder = () =>
|
||||||
path.join(getSasjsRootFolder(), 'sasjscore')
|
path.join(getSasjsRootFolder(), 'sas', 'sasautos')
|
||||||
|
|
||||||
|
export const getPackagesFolder = () =>
|
||||||
|
path.join(getSasjsRootFolder(), '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(getSasjsRootFolder(), 'files')
|
||||||
|
|
||||||
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
|
|
||||||
|
|
||||||
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
||||||
|
|
||||||
export const getSessionsFolder = () =>
|
export const getSessionsFolder = () =>
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateAccessToken = (data: InfoJWT) =>
|
export const generateAccessToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
|
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
|
||||||
expiresIn: '1day'
|
expiresIn: '1day'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateAuthCode = (data: InfoJWT) =>
|
export const generateAuthCode = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.AUTH_CODE_SECRET as string, {
|
jwt.sign(data, process.secrets.AUTH_CODE_SECRET, {
|
||||||
expiresIn: '30s'
|
expiresIn: '30s'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateRefreshToken = (data: InfoJWT) =>
|
export const generateRefreshToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
|
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
|
||||||
expiresIn: '30 days'
|
expiresIn: '30 days'
|
||||||
})
|
})
|
||||||
|
|||||||
35
api/src/utils/getAuthorizedRoutes.ts
Normal file
35
api/src/utils/getAuthorizedRoutes.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Request } from 'express'
|
||||||
|
|
||||||
|
const StaticAuthorizedRoutes = [
|
||||||
|
'/AppStream',
|
||||||
|
'/SASjsApi/code/execute',
|
||||||
|
'/SASjsApi/stp/execute',
|
||||||
|
'/SASjsApi/drive/deploy',
|
||||||
|
'/SASjsApi/drive/deploy/upload',
|
||||||
|
'/SASjsApi/drive/file',
|
||||||
|
'/SASjsApi/drive/folder',
|
||||||
|
'/SASjsApi/drive/fileTree',
|
||||||
|
'/SASjsApi/drive/rename'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const getAuthorizedRoutes = () => {
|
||||||
|
const streamingApps = Object.keys(process.appStreamConfig)
|
||||||
|
const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`)
|
||||||
|
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPath = (req: Request) => {
|
||||||
|
const { baseUrl, path: reqPath } = req
|
||||||
|
|
||||||
|
if (baseUrl === '/AppStream') {
|
||||||
|
const appStream = reqPath.split('/')[1]
|
||||||
|
|
||||||
|
// removing trailing slash of URLs
|
||||||
|
return (baseUrl + '/' + appStream).replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (baseUrl + reqPath).replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isAuthorizingRoute = (req: Request): boolean =>
|
||||||
|
getAuthorizedRoutes().includes(getPath(req))
|
||||||
@@ -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,7 +15,6 @@ 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('; ')}`)
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,13 +8,17 @@ export * from './file'
|
|||||||
export * from './generateAccessToken'
|
export * from './generateAccessToken'
|
||||||
export * from './generateAuthCode'
|
export * from './generateAuthCode'
|
||||||
export * from './generateRefreshToken'
|
export * from './generateRefreshToken'
|
||||||
|
export * from './getAuthorizedRoutes'
|
||||||
export * from './getCertificates'
|
export * from './getCertificates'
|
||||||
export * from './getDesktopFields'
|
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 './isPublicRoute'
|
||||||
|
export * from './ldapClient'
|
||||||
export * from './zipped'
|
export * from './zipped'
|
||||||
export * from './parseLogToArray'
|
export * from './parseLogToArray'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
|
|||||||
31
api/src/utils/isPublicRoute.ts
Normal file
31
api/src/utils/isPublicRoute.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
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.')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,88 @@
|
|||||||
import Client from '../model/Client'
|
import Client from '../model/Client'
|
||||||
|
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
|
import Configuration, { ConfigurationType } from '../model/Configuration'
|
||||||
|
|
||||||
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
|
export const SECRETS: ConfigurationType = {
|
||||||
|
ACCESS_TOKEN_SECRET: randomBytes(64).toString('hex'),
|
||||||
|
REFRESH_TOKEN_SECRET: randomBytes(64).toString('hex'),
|
||||||
|
AUTH_CODE_SECRET: randomBytes(64).toString('hex'),
|
||||||
|
SESSION_SECRET: randomBytes(64).toString('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const seedDB = async (): Promise<ConfigurationType> => {
|
||||||
|
// Checking if client is already in the database
|
||||||
|
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
||||||
|
if (!clientExist) {
|
||||||
|
const client = new Client(CLIENT)
|
||||||
|
await client.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking if 'AllUsers' Group is already in the database
|
||||||
|
let groupExist = await Group.findOne({ name: GROUP.name })
|
||||||
|
if (!groupExist) {
|
||||||
|
const group = new Group(GROUP)
|
||||||
|
groupExist = await group.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - Group created: ${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()
|
||||||
|
|
||||||
|
console.log(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking if user is already in the database
|
||||||
|
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||||
|
if (!usernameExist) {
|
||||||
|
const user = new User(ADMIN_USER)
|
||||||
|
usernameExist = await user.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupExist.hasUser(usernameExist)) {
|
||||||
|
groupExist.addUser(usernameExist)
|
||||||
|
console.log(
|
||||||
|
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checking if configuration is present in the database
|
||||||
|
let configExist = await Configuration.findOne()
|
||||||
|
if (!configExist) {
|
||||||
|
const configuration = new Configuration(SECRETS)
|
||||||
|
configExist = await configuration.save()
|
||||||
|
|
||||||
|
console.log('DB Seed - configuration added')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ACCESS_TOKEN_SECRET: configExist.ACCESS_TOKEN_SECRET,
|
||||||
|
REFRESH_TOKEN_SECRET: configExist.REFRESH_TOKEN_SECRET,
|
||||||
|
AUTH_CODE_SECRET: configExist.AUTH_CODE_SECRET,
|
||||||
|
SESSION_SECRET: configExist.SESSION_SECRET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const GROUP = {
|
||||||
|
name: 'AllUsers',
|
||||||
|
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',
|
||||||
@@ -13,23 +96,3 @@ const ADMIN_USER = {
|
|||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export const seedDB = async () => {
|
|
||||||
// Checking if client is already in the database
|
|
||||||
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
|
||||||
if (!clientExist) {
|
|
||||||
const client = new Client(CLIENT)
|
|
||||||
await client.save()
|
|
||||||
|
|
||||||
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checking if user is already in the database
|
|
||||||
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
|
||||||
if (!usernameExist) {
|
|
||||||
const user = new User(ADMIN_USER)
|
|
||||||
await user.save()
|
|
||||||
|
|
||||||
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,26 +1,42 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||||
|
|
||||||
import { getDesktopFields, ModeType, RunTimeType } from '.'
|
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
|
||||||
|
|
||||||
export const setProcessVariables = async () => {
|
export const setProcessVariables = async () => {
|
||||||
|
const { MODE, RUN_TIMES } = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
// NOTE: when exporting app.js as agent for supertest
|
||||||
|
// it should prevent connecting to the real database
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
const secrets = await connectDB()
|
||||||
|
|
||||||
|
process.secrets = secrets
|
||||||
|
} else {
|
||||||
|
process.secrets = SECRETS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { MODE, RUN_TIMES } = process.env
|
|
||||||
|
|
||||||
process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? []
|
process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? []
|
||||||
|
|
||||||
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
|
||||||
@@ -28,7 +44,18 @@ export const setProcessVariables = async () => {
|
|||||||
await createFolder(absPath)
|
await createFolder(absPath)
|
||||||
process.driveLoc = getRealPath(absPath)
|
process.driveLoc = getRealPath(absPath)
|
||||||
|
|
||||||
|
const { LOG_LOCATION } = process.env
|
||||||
|
const absLogsPath = getAbsolutePath(
|
||||||
|
LOG_LOCATION ?? `sasjs_root${path.sep}logs`,
|
||||||
|
process.cwd()
|
||||||
|
)
|
||||||
|
await createFolder(absLogsPath)
|
||||||
|
process.logsLoc = getRealPath(absLogsPath)
|
||||||
|
|
||||||
|
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
|
console.log('sasLogs: ', process.logsLoc)
|
||||||
console.log('runTimes: ', process.runTimes)
|
console.log('runTimes: ', process.runTimes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
||||||
import { getDesktopUserAutoExecPath, getFilesFolder } from './file'
|
import {
|
||||||
|
getDesktopUserAutoExecPath,
|
||||||
|
getFilesFolder,
|
||||||
|
getPackagesFolder
|
||||||
|
} from './file'
|
||||||
import { ModeType } from './verifyEnvVariables'
|
import { ModeType } from './verifyEnvVariables'
|
||||||
|
|
||||||
export const setupFolders = async () => {
|
export const setupFolders = async () => {
|
||||||
const drivePath = getFilesFolder()
|
const drivePath = getFilesFolder()
|
||||||
await createFolder(drivePath)
|
await createFolder(drivePath)
|
||||||
|
await createFolder(getPackagesFolder())
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Desktop) {
|
if (process.env.MODE === ModeType.Desktop) {
|
||||||
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { extractHeaders } from '..'
|
import { extractHeaders } from '../extractHeaders'
|
||||||
|
|
||||||
describe('extractHeaders', () => {
|
describe('extractHeaders', () => {
|
||||||
it('should return valid http headers', () => {
|
it('should return valid http headers', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseLogToArray } from '..'
|
import { parseLogToArray } from '../parseLogToArray'
|
||||||
|
|
||||||
describe('parseLogToArray', () => {
|
describe('parseLogToArray', () => {
|
||||||
it('should parse log to array type', () => {
|
it('should parse log to array type', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { MulterFile } from '../types/Upload'
|
import { MulterFile } from '../types/Upload'
|
||||||
import { listFilesInFolder, readFileBinary } from '@sasjs/utils'
|
import { listFilesInFolder, readFileBinary, isWindows } from '@sasjs/utils'
|
||||||
|
|
||||||
interface FilenameMapSingle {
|
interface FilenameMapSingle {
|
||||||
fieldName: string
|
fieldName: string
|
||||||
@@ -118,15 +118,69 @@ export const generateFileUploadJSCode = async (
|
|||||||
if (fileName.includes('req_file')) {
|
if (fileName.includes('req_file')) {
|
||||||
fileCount++
|
fileCount++
|
||||||
const filePath = path.join(sessionFolder, fileName)
|
const filePath = path.join(sessionFolder, fileName)
|
||||||
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${filePath}')`
|
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${
|
||||||
|
isWindows() ? filePath.replace(/\\/g, '\\\\') : filePath
|
||||||
|
}')`
|
||||||
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
||||||
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
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,10 @@
|
|||||||
import Joi from 'joi'
|
import Joi from 'joi'
|
||||||
import { RunTimeType } from '.'
|
import {
|
||||||
|
PermissionType,
|
||||||
|
PermissionSettingForRoute,
|
||||||
|
PrincipalType
|
||||||
|
} from '../controllers/permission'
|
||||||
|
import { getAuthorizedRoutes } from './getAuthorizedRoutes'
|
||||||
|
|
||||||
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||||
const passwordSchema = Joi.string().min(6).max(1024)
|
const passwordSchema = Joi.string().min(6).max(1024)
|
||||||
@@ -86,6 +91,30 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
|||||||
clientSecret: Joi.string().required()
|
clientSecret: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
|
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
path: Joi.string()
|
||||||
|
.required()
|
||||||
|
.valid(...getAuthorizedRoutes()),
|
||||||
|
type: Joi.string()
|
||||||
|
.required()
|
||||||
|
.valid(...Object.values(PermissionType)),
|
||||||
|
setting: Joi.string()
|
||||||
|
.required()
|
||||||
|
.valid(...Object.values(PermissionSettingForRoute)),
|
||||||
|
principalType: Joi.string()
|
||||||
|
.required()
|
||||||
|
.valid(...Object.values(PrincipalType)),
|
||||||
|
principalId: Joi.number().required()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
|
export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
setting: Joi.string()
|
||||||
|
.required()
|
||||||
|
.valid(...Object.values(PermissionSettingForRoute))
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
export const deployValidation = (data: any): Joi.ValidationResult =>
|
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
||||||
@@ -116,9 +145,23 @@ export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
|||||||
_filePath: filePathSchema
|
_filePath: filePathSchema
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
export const folderParamValidation = (
|
||||||
|
data: any,
|
||||||
|
folderPathRequired?: boolean
|
||||||
|
): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
_folderPath: Joi.string()
|
_folderPath: folderPathRequired ? Joi.string().required() : Joi.string()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
|
export const folderBodyValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
folderPath: Joi.string().required()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
|
export const renameBodyValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
oldPath: Joi.string().required(),
|
||||||
|
newPath: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
export const runCodeValidation = (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 {
|
||||||
@@ -39,6 +50,8 @@ export enum ReturnCode {
|
|||||||
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 +68,8 @@ export const verifyEnvVariables = (): ReturnCode => {
|
|||||||
|
|
||||||
errors.push(...verifyExecutablePaths())
|
errors.push(...verifyExecutablePaths())
|
||||||
|
|
||||||
|
errors.push(...verifyLDAPVariables())
|
||||||
|
|
||||||
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 +80,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,39 +110,22 @@ const verifyMODE = (): string[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Server) {
|
if (process.env.MODE === ModeType.Server) {
|
||||||
const {
|
const { DB_CONNECT, AUTH_PROVIDERS } = process.env
|
||||||
ACCESS_TOKEN_SECRET,
|
|
||||||
REFRESH_TOKEN_SECRET,
|
|
||||||
AUTH_CODE_SECRET,
|
|
||||||
SESSION_SECRET,
|
|
||||||
DB_CONNECT
|
|
||||||
} = process.env
|
|
||||||
|
|
||||||
if (!ACCESS_TOKEN_SECRET)
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
errors.push(
|
|
||||||
`- ACCESS_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!REFRESH_TOKEN_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- REFRESH_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!AUTH_CODE_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- AUTH_CODE_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!SESSION_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- SESSION_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
||||||
@@ -151,8 +166,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 =
|
||||||
@@ -235,7 +269,8 @@ const verifyRUN_TIMES = (): string[] => {
|
|||||||
|
|
||||||
const verifyExecutablePaths = () => {
|
const verifyExecutablePaths = () => {
|
||||||
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(',')
|
||||||
@@ -247,6 +282,61 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
return errors
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export const extractJSONFromZip = async (zipFile: Express.Multer.File) => {
|
|||||||
|
|
||||||
for await (const entry of zip) {
|
for await (const entry of zip) {
|
||||||
const fileName = entry.path as string
|
const fileName = entry.path as string
|
||||||
if (fileName.toUpperCase().endsWith('.JSON') && fileName === fileInZip) {
|
// grab the first json found in .zip
|
||||||
|
if (fileName.toUpperCase().endsWith('.JSON')) {
|
||||||
fileContent = await entry.buffer()
|
fileContent = await entry.buffer()
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user