1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +00:00

Compare commits

..

324 Commits

Author SHA1 Message Date
semantic-release-bot
b64c0c12da chore(release): 0.23.2 [skip ci]
## [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](14731e8824))
* bumping sasjs/score ([258cc35](258cc35f14))
* reverting commit ([fda0e0b](fda0e0b57d))
2022-10-06 12:41:15 +00:00
Allan Bowe
79bc7b0e28 Merge pull request #300 from sasjs/corebump
fix: bumping sasjs/score
2022-10-06 13:36:20 +01:00
Allan Bowe
fda0e0b57d fix: reverting commit 2022-10-06 12:35:59 +00:00
Allan Bowe
14731e8824 fix: bump in correct place 2022-10-06 12:34:48 +00:00
Allan Bowe
258cc35f14 fix: bumping sasjs/score 2022-10-06 12:32:13 +00:00
semantic-release-bot
2295a518f0 chore(release): 0.23.1 [skip ci]
## [0.23.1](https://github.com/sasjs/server/compare/v0.23.0...v0.23.1) (2022-10-04)

### Bug Fixes

* ldap issues ([4d64420](4d64420c45))
2022-10-04 16:54:37 +00:00
Allan Bowe
1e5d621817 Merge pull request #298 from sasjs/fix-ldap
fix: ldap issues
2022-10-03 18:49:53 +01:00
4d64420c45 fix: ldap issues
logic fixed for updating user created by external auth provider
remove internal from AuthProviderType
replace AUTH_MECHANISM with AUTH_PROVIDERS
2022-10-03 21:24:10 +05:00
semantic-release-bot
799339de30 chore(release): 0.23.0 [skip ci]
# [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](424f0fc1fa))
2022-10-03 15:13:11 +00:00
Allan Bowe
042ed41189 Merge pull request #297 from sasjs/issue-292
feat: Enable SAS_PACKAGES in SASjs Server
2022-10-03 16:08:30 +01:00
424f0fc1fa feat: Enable SAS_PACKAGES in SASjs Server 2022-10-03 19:43:02 +05:00
semantic-release-bot
deafebde05 chore(release): 0.22.1 [skip ci]
## [0.22.1](https://github.com/sasjs/server/compare/v0.22.0...v0.22.1) (2022-10-03)

### Bug Fixes

* spelling issues ([3bb0597](3bb05974d2))
2022-10-03 13:17:14 +00:00
Allan Bowe
b66dc86b01 Merge pull request #296 from sasjs/spellingz
fix: spelling issues
2022-10-03 14:11:55 +01:00
Allan Bowe
3bb05974d2 fix: spelling issues 2022-10-03 13:10:30 +00:00
semantic-release-bot
d1c1a59e91 chore(release): 0.22.0 [skip ci]
# [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](68f0c5c588))
* no need to restrict api endpoints when ldap auth is applied ([a142660](a14266077d))
* remove authProvider attribute from user and group payload interface ([bbd7786](bbd7786c6c))

### Features

* implemented LDAP authentication ([f915c51](f915c51b07))
2022-10-03 12:13:18 +00:00
Allan Bowe
668aff83fd Merge pull request #293 from sasjs/ldap
feat: integratedLDAP authentication
2022-10-03 13:09:07 +01:00
3fc06b80fc chore: add specs 2022-10-01 16:08:29 +05:00
bbd7786c6c fix: remove authProvider attribute from user and group payload interface 2022-10-01 15:06:55 +05:00
68f0c5c588 fix: do not throw error on deleting group when it is created by an external auth provider 2022-10-01 14:52:36 +05:00
semantic-release-bot
69ddf313b8 chore(release): 0.21.7 [skip ci]
## [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](fe3e5088f8))
2022-09-30 21:44:16 +00:00
Saad Jutt
65e404cdbd Merge pull request #294 from sasjs/csrf-package-migration
fix: csrf package is changed to pillarjs-csrf
2022-10-01 02:39:06 +05:00
a14266077d fix: no need to restrict api endpoints when ldap auth is applied 2022-09-30 14:41:09 +05:00
Saad Jutt
fda6ad6356 chore(csrf): removed _csrf completely 2022-09-30 03:07:21 +05:00
Saad Jutt
fe3e5088f8 fix: csrf package is changed to pillarjs-csrf 2022-09-29 20:33:30 +05:00
f915c51b07 feat: implemented LDAP authentication 2022-09-29 18:41:28 +05:00
semantic-release-bot
375f924f45 chore(release): 0.21.6 [skip ci]
## [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](40f95f9072))
2022-09-23 09:33:49 +00:00
Allan Bowe
72329e30ed Merge pull request #291 from sasjs/issue-290
fix: in getTokensFromDB handle the scenario when tokens are expired
2022-09-23 10:29:51 +01:00
40f95f9072 fix: in getTokensFromDB handle the scenario when tokens are expired 2022-09-23 09:35:30 +05:00
semantic-release-bot
58e8a869ef chore(release): 0.21.5 [skip ci]
## [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](249604384e))
2022-09-22 15:50:53 +00:00
Allan Bowe
b558a3d01d Merge pull request #289 from sasjs/issue-288
fix: made files extensions case insensitive
2022-09-22 16:47:00 +01:00
249604384e fix: made files extensions case insensitive 2022-09-22 20:37:16 +05:00
semantic-release-bot
056a436e10 chore(release): 0.21.4 [skip ci]
## [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](a0e7875ae6))
2022-09-21 20:02:09 +00:00
Allan Bowe
06d59c618c Merge pull request #287 from sasjs/varfix
fix: removing single quotes from _program value
2022-09-21 20:58:28 +01:00
Allan Bowe
a0e7875ae6 fix: removing single quotes from _program value 2022-09-21 19:57:32 +00:00
semantic-release-bot
24966e695a chore(release): 0.21.3 [skip ci]
## [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](330c020933))
2022-09-21 17:49:49 +00:00
Allan Bowe
5c40d8a342 Merge pull request #286 from sasjs/issue-279
fix: return same tokens if not expired
2022-09-21 18:46:07 +01:00
6f5566dabb chore: lint fix 2022-09-21 22:29:50 +05:00
d93470d183 chore: improve code 2022-09-21 22:27:27 +05:00
330c020933 fix: return same tokens if not expired 2022-09-21 22:12:03 +05:00
munja
a810f6c7cf chore(docs): updating swagger definitions 2022-09-21 11:08:12 +01:00
semantic-release-bot
5d6c6086b4 chore(release): 0.21.2 [skip ci]
## [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](9977c9d161))
* **studio:** inject program path to code before sending for execution ([edc2e2a](edc2e2a302))
2022-09-20 21:08:08 +00:00
Allan Bowe
0edcbdcefc Merge pull request #283 from sasjs/fix-default-content-type
fix: default content-type for sas programs should be text/plain
2022-09-20 22:04:27 +01:00
Allan Bowe
ea0222f218 Merge pull request #285 from sasjs/issue-280
fix(studio): inject program path to code before sending for execution
2022-09-20 22:04:16 +01:00
edc2e2a302 fix(studio): inject program path to code before sending for execution 2022-09-21 01:57:01 +05:00
Allan Bowe
efd2e1450e Merge pull request #284 from sasjs/apidocs
chore(docs): updating API docs
2022-09-20 12:25:12 +01:00
munja
1092a73c10 chore(docs): updating API docs 2022-09-20 12:20:50 +01:00
9977c9d161 fix: default content-type for sas programs should be text/plain 2022-09-20 02:32:22 +05:00
semantic-release-bot
5c0eff5197 chore(release): 0.21.1 [skip ci]
## [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](0749d65173))
2022-09-19 18:58:46 +00:00
Allan Bowe
3bda991a58 Merge pull request #282 from sasjs/issue-281
fix: SASJS_WEBOUT_HEADERS path for windows
2022-09-19 19:54:13 +01:00
0327f7c6ec chore: no need to escapeWinSlash in _sasjs_webout_headers 2022-09-19 23:51:18 +05:00
92549402eb chore: use utility function escapeWinSlashes 2022-09-19 23:36:04 +05:00
semantic-release-bot
b88c911527 chore(release): 0.21.0 [skip ci]
# [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](06d3b17154))
2022-09-19 12:54:27 +00:00
Saad Jutt
8b12f31060 Merge pull request #276 from sasjs/sas9-mock
SAS9 mocker improved - public access denied scenario
2022-09-19 17:50:45 +05:00
Saad Jutt
e65cba9af0 chore: removed deprecated body-parser 2022-09-19 17:47:29 +05:00
0749d65173 fix: SASJS_WEBOUT_HEADERS path for windows 2022-09-19 15:53:51 +05:00
semantic-release-bot
a9c9b734f5 chore(release): 0.20.0 [skip ci]
# [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](d6651bbdbe))
2022-09-16 11:55:57 +00:00
Saad Jutt
39da41c9f1 Merge pull request #277 from sasjs/r-integration
R integration
2022-09-16 16:51:06 +05:00
662b2ca36a chore: replace env variable RSCRIPT_PATH with R_PATH 2022-09-09 15:23:46 +05:00
16b7aa6abb chore: merge js, py and r session controller classes to base session controller class 2022-09-09 00:49:26 +05:00
4560ef942f chore(web): refactor react code 2022-09-08 21:49:35 +05:00
06d3b17154 feat: sas9 mocker improved - public access denied scenario 2022-09-07 18:48:56 +02:00
d6651bbdbe feat: add support for R stored programs 2022-09-06 21:52:21 +05:00
b9d032f148 chore: update swagger.yaml 2022-09-06 21:51:17 +05:00
semantic-release-bot
70655e74d3 chore(release): 0.19.0 [skip ci]
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)

### Features

* added mocking endpoints ([0a0ba2c](0a0ba2cca5))
2022-09-05 12:21:34 +00:00
Allan Bowe
cb82fea0d8 Merge pull request #264 from sasjs/mocker
Mocker
2022-09-05 13:16:10 +01:00
b9a596616d chore: cleanup 2022-09-05 12:20:56 +02:00
semantic-release-bot
72a5393be3 chore(release): 0.18.0 [skip ci]
# [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](ee2db276bb))
2022-09-02 19:26:49 +00:00
Allan Bowe
769a840e9f Merge pull request #273 from sasjs/issue-270
feat: add option for program launch in context menu
2022-09-02 20:23:02 +01:00
730c7c52ac chore: remove commented code 2022-09-03 00:09:48 +05:00
ee2db276bb feat: add option for program launch in context menu 2022-09-02 23:40:02 +05:00
semantic-release-bot
d0a24aacb6 chore(release): 0.17.5 [skip ci]
## [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](393b5eaf99))
2022-09-02 18:08:49 +00:00
Allan Bowe
57dfdf89a4 Merge pull request #272 from sasjs/allanbowe/session-crashed-since-271
fix: SASINITIALFOLDER split over 2 params, closes #271
2022-09-02 19:03:37 +01:00
Allan Bowe
393b5eaf99 fix: SASINITIALFOLDER split over 2 params, closes #271 2022-09-02 17:59:10 +00:00
Saad Jutt
7477326b22 chore: lower cased env values 2022-09-01 23:38:04 +05:00
Saad Jutt
76bf84316e chore: MOCK_SERVERTYPE instead of string literals 2022-09-01 23:34:57 +05:00
semantic-release-bot
e355276e44 chore(release): 0.17.4 [skip ci]
## [0.17.4](https://github.com/sasjs/server/compare/v0.17.3...v0.17.4) (2022-09-01)

### Bug Fixes

* invalid JS logic ([9f06080](9f06080348))
2022-09-01 12:50:14 +00:00
Allan Bowe
a3a9e3bd9f Merge pull request #269 from sasjs/allanbowe/error-unrecognized-sas-267
fix: invalid JS logic
2022-09-01 13:41:52 +01:00
Allan Bowe
9f06080348 fix: invalid JS logic 2022-09-01 12:35:58 +00:00
semantic-release-bot
4bbf9cfdb3 chore(release): 0.17.3 [skip ci]
## [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](e63271a67a))
2022-09-01 12:25:33 +00:00
Allan Bowe
e8e71fcde9 Merge pull request #268 from sasjs/allanbowe/error-unrecognized-sas-267
fix: making SASINITIALFOLDER option windows only.  Closes #267
2022-09-01 13:21:25 +01:00
Allan Bowe
e63271a67a fix: making SASINITIALFOLDER option windows only. Closes #267 2022-09-01 12:18:53 +00:00
7633608318 chore: mocker architecture fix, env validation 2022-08-31 13:31:28 +02:00
semantic-release-bot
e67d27d264 chore(release): 0.17.2 [skip ci]
## [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](a5ee2f2923))
2022-08-31 09:35:51 +00:00
Allan Bowe
53033ccc96 Merge pull request #262 from sasjs/allanbowe/sas-default-folder-should-260
fix: addition of SASINITIALFOLDER startup option.  Closes #260
2022-08-31 10:32:14 +01:00
semantic-release-bot
6131ed1cbe chore(release): 0.17.1 [skip ci]
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)

### Bug Fixes

* typo mistake ([ee17d37](ee17d37aa1))
2022-08-30 18:15:33 +00:00
Allan Bowe
5d624e3399 Merge pull request #266 from sasjs/issue-265
fix: typo mistake
2022-08-30 19:10:47 +01:00
ee17d37aa1 fix: typo mistake 2022-08-30 22:42:11 +05:00
572fe22d50 chore: mocksas9 controller 2022-08-30 17:27:37 +02:00
091268bf58 chore: mocking only mandatory bits from sas9 responses 2022-08-29 12:40:29 +02:00
71a4a48443 chore: generic sas9 mock responses 2022-08-29 10:30:01 +02:00
3b188cd724 style: lint 2022-08-26 18:03:28 +02:00
eeba2328c0 chore: added login, logout endpoints 2022-08-26 17:59:07 +02:00
0a0ba2cca5 feat: added mocking endpoints 2022-08-25 15:58:08 +02:00
semantic-release-bot
476f834a80 chore(release): 0.17.0 [skip ci]
# [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](bce83cb6fb))

### Features

* add the functionality of saving file by ctrl + s in editor ([3a3c90d](3a3c90d9e6))
2022-08-25 09:47:48 +00:00
Sabir Hassan
8b8739a873 Merge pull request #263 from sasjs/ctrl-save
feat: add the functionality of saving file by ctrl + s in editor
2022-08-25 14:43:50 +05:00
bce83cb6fb fix: allow underscores in file name 2022-08-25 14:27:42 +05:00
3a3c90d9e6 feat: add the functionality of saving file by ctrl + s in editor 2022-08-25 14:12:51 +05:00
semantic-release-bot
e63eaa5302 chore(release): 0.16.1 [skip ci]
## [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](98ea2ac9b9))
2022-08-24 16:07:11 +00:00
Sabir Hassan
65de1bb175 Merge pull request #261 from sasjs/issue-258
fix: update response of /SASjsApi/stp/execute and /SASjsApi/code/execute
2022-08-24 21:02:57 +05:00
Allan Bowe
a5ee2f2923 fix: addition of SASINITIALFOLDER startup option. Closes #260 2022-08-19 15:20:36 +00:00
98ea2ac9b9 fix: update response of /SASjsApi/stp/execute and /SASjsApi/code/execute 2022-08-19 15:06:39 +05:00
semantic-release-bot
e94c56b23f chore(release): 0.16.0 [skip ci]
# [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](882bedd5d5))
* update content for code.sas file ([02e88ae](02e88ae728))
* update default content type for python and js runtimes ([8780b80](8780b800a3))

### Features

* implement the logic for running python stored programs ([b06993a](b06993ab9e))
2022-08-17 20:49:53 +00:00
Allan Bowe
64f80e958d Merge pull request #259 from sasjs/python-runtime
feat: implement the logic for running python stored programs
2022-08-17 21:45:55 +01:00
bd97363c13 chore: quick fixes 2022-08-18 01:39:03 +05:00
02e88ae728 fix: update content for code.sas file 2022-08-18 01:20:33 +05:00
882bedd5d5 fix: add a new variable _SASJS_WEBOUT_HEADERS to code.js and code.py 2022-08-18 01:19:47 +05:00
8780b800a3 fix: update default content type for python and js runtimes 2022-08-18 01:17:17 +05:00
4c11082796 chore: addressed comments 2022-08-17 21:24:30 +05:00
a9b25b8880 chore: added specs for stp 2022-08-16 22:39:15 +05:00
b06993ab9e feat: implement the logic for running python stored programs 2022-08-16 15:51:37 +05:00
semantic-release-bot
f736e67517 chore(release): 0.15.3 [skip ci]
## [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](f8bb7327a8))
2022-08-11 15:07:31 +00:00
Allan Bowe
0f4a60c0c7 Merge pull request #254 from sasjs/allanbowe/sasjs-server-does-not-253
fix: adding proc printto in precode to enable print output in log.  Closes #253
2022-08-11 16:03:13 +01:00
Allan Bowe
f8bb7327a8 fix: adding proc printto in precode to enable print output in log. Closes #253 2022-08-11 15:01:46 +00:00
semantic-release-bot
abce135da2 chore(release): 0.15.2 [skip ci]
## [0.15.2](https://github.com/sasjs/server/compare/v0.15.1...v0.15.2) (2022-08-10)

### Bug Fixes

* remove vulnerabitities ([f27ac51](f27ac51fc4))
2022-08-10 11:28:07 +00:00
Allan Bowe
a6c014946a Merge pull request #252 from sasjs/fix-vulnerabilities
fix: remove vulnerabitities
2022-08-10 12:23:23 +01:00
f27ac51fc4 fix: remove vulnerabitities 2022-08-10 16:10:37 +05:00
semantic-release-bot
cb5be1be21 chore(release): 0.15.1 [skip ci]
## [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](d99fdd1ec7))
2022-08-10 10:34:36 +00:00
Allan Bowe
d90fa9e5dd Merge pull request #251 from sasjs/issue-250
fix(web): fix UI responsiveness
2022-08-10 11:29:41 +01:00
d99fdd1ec7 fix(web): fix UI responsiveness 2022-08-10 15:18:05 +05:00
semantic-release-bot
399b5edad0 chore(release): 0.15.0 [skip ci]
# [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](e215958b8b))
* improve mobile view for studio page ([c67d3ee](c67d3ee2f1))
* improve responsiveness for mobile view ([6ef40b9](6ef40b954a))
* improve user experience for adding permissions ([7a162ed](7a162eda8f))
* show logout button only when user is logged in ([9227cd4](9227cd449d))

### Features

* add multiple permission for same combination of type and principal at once ([754704b](754704bca8))
2022-08-05 09:59:19 +00:00
Allan Bowe
1dbc12e96b Merge pull request #249 from sasjs/issue-225
feat: add multiple permission for same combination of type and principal at once
2022-08-05 10:55:32 +01:00
e215958b8b fix: after selecting file in sidebar collapse sidebar in mobile view 2022-08-05 14:18:59 +05:00
9227cd449d fix: show logout button only when user is logged in 2022-08-05 01:22:27 +05:00
c67d3ee2f1 fix: improve mobile view for studio page 2022-08-05 01:10:15 +05:00
6ef40b954a fix: improve responsiveness for mobile view 2022-08-04 22:57:21 +05:00
semantic-release-bot
0d913baff1 chore(release): 0.14.1 [skip ci]
## [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](87c03c5f8d))
* **cookie:** XSRF cookie is removed and passed token in head section ([77f8d30](77f8d30baf))
* **env:** check added for not providing WHITELIST ([5966016](5966016853))
* **web:** show login on logged-out state ([f7fcc77](f7fcc7741a))
2022-08-04 12:10:31 +00:00
Allan Bowe
3671736c3d Merge pull request #248 from sasjs/cookies-management
fix(cookie): XSRF cookie is removed and passed token in head section
2022-08-04 13:06:30 +01:00
34cd84d8a9 chore: improve interface for add permission response 2022-08-04 16:34:15 +05:00
Saad Jutt
f7fcc7741a fix(web): show login on logged-out state 2022-08-04 05:39:28 +05:00
Saad Jutt
18052fdbf6 test: fixed failed specs 2022-08-04 04:01:51 +05:00
Saad Jutt
5966016853 fix(env): check added for not providing WHITELIST 2022-08-04 03:32:04 +05:00
Saad Jutt
87c03c5f8d fix(apps): App Stream logo fix 2022-08-04 03:03:27 +05:00
7a162eda8f fix: improve user experience for adding permissions 2022-08-04 02:51:59 +05:00
754704bca8 feat: add multiple permission for same combination of type and principal at once 2022-08-03 23:26:31 +05:00
Saad Jutt
77f8d30baf fix(cookie): XSRF cookie is removed and passed token in head section 2022-08-03 03:38:11 +05:00
semantic-release-bot
78bea7c154 chore(release): 0.14.0 [skip ci]
# [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](d3a516c36e))
* call jwt.verify in synchronous way ([254bc07](254bc07da7))

### Features

* add public group to DB on seed ([c3e3bef](c3e3befc17))
* bypass authentication when route is enabled for public group ([68515f9](68515f95a6))
2022-08-02 19:08:38 +00:00
Saad Jutt
9c3b155c12 Merge pull request #246 from sasjs/issue-240
feat: bypass authentication when route is enabled for public group
2022-08-03 00:03:43 +05:00
Allan Bowe
98e501334f Update seedDB.ts 2022-08-02 19:33:16 +01:00
Allan Bowe
bbfd53e79e Update group.spec.ts 2022-08-02 19:32:44 +01:00
254bc07da7 fix: call jwt.verify in synchronous way 2022-08-02 23:05:42 +05:00
f978814ca7 chore: code refactor 2022-08-02 22:16:41 +05:00
68515f95a6 feat: bypass authentication when route is enabled for public group 2022-08-02 18:06:33 +05:00
d3a516c36e fix: add restriction on add/remove user to public group 2022-08-02 18:05:28 +05:00
c3e3befc17 feat: add public group to DB on seed 2022-08-02 18:04:00 +05:00
semantic-release-bot
275de9478e chore(release): 0.13.3 [skip ci]
## [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](8a3054e19a))
* update schema of Permission ([5d5a9d3](5d5a9d3788))
2022-08-02 12:01:53 +00:00
Allan Bowe
1a3ef62cb2 Merge pull request #243 from sasjs/issue-241
fix: show non-admin user his own permissions only
2022-08-02 12:57:57 +01:00
semantic-release-bot
9eb5f3ca4d chore(release): 0.13.2 [skip ci]
## [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](916947dffa))
2022-08-01 22:42:31 +00:00
Allan Bowe
916947dffa fix: adding ls=max to reduce log size and improve readability 2022-08-01 22:38:31 +00:00
79b7827b7c chore: update tabs label in setting page 2022-08-01 23:01:05 +05:00
37e1aa9b61 chore: spec fixed 2022-08-01 22:54:31 +05:00
7e504008b7 chore: quick fix 2022-08-01 22:50:18 +05:00
5d5a9d3788 fix: update schema of Permission 2022-08-01 21:33:10 +05:00
semantic-release-bot
7c79d6479c chore(release): 0.13.1 [skip ci]
## [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](77db14c690))
2022-07-31 17:09:11 +00:00
Allan Bowe
3e635f422a Merge pull request #245 from sasjs/allanbowe/avoid-batch-sas-window-244
fix: adding options to prevent unwanted windows on windows.  Closes #244
2022-07-31 18:05:05 +01:00
Allan Bowe
77db14c690 fix: adding options to prevent unwanted windows on windows. Closes #244 2022-07-31 16:58:33 +00:00
b7dff341f0 chore: fix specs 2022-07-30 00:18:02 +05:00
8a3054e19a fix: show non-admin user his own permissions only 2022-07-30 00:01:15 +05:00
semantic-release-bot
a531de2adb chore(release): 0.13.0 [skip ci]
# [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](7681722e5a))
* move api button to user menu ([8de032b](8de032b543))

### Features

* add action and command to editor ([706e228](706e228a8e))
2022-07-28 19:27:12 +00:00
Allan Bowe
c458d94493 Merge pull request #239 from sasjs/issue-238
fix: improve user experience in the studio
2022-07-28 20:21:48 +01:00
706e228a8e feat: add action and command to editor 2022-07-28 23:56:44 +05:00
7681722e5a fix: autofocus input field and submit on enter 2022-07-28 23:55:59 +05:00
8de032b543 fix: move api button to user menu 2022-07-28 23:54:40 +05:00
semantic-release-bot
998ef213e9 chore(release): 0.12.1 [skip ci]
## [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](c574b42235))
* **web:** saveAs functionality fixed in studio page ([3c987c6](3c987c61dd))
* **web:** show original name as default name in rename file/folder modal ([9640f65](9640f65264))
* **web:** webout tab item fixed in studio page ([7cdffe3](7cdffe30e3))
* **web:** when no file is selected save the editor content to local storage ([3b1fcb9](3b1fcb937d))
2022-07-26 20:52:05 +00:00
Allan Bowe
f8b0f98678 Merge pull request #236 from sasjs/fix-studio
fix: issues fixed in studio page
2022-07-26 21:48:20 +01:00
9640f65264 fix(web): show original name as default name in rename file/folder modal 2022-07-27 01:44:13 +05:00
c574b42235 fix(web): disable launch icon button when file content is not saved 2022-07-27 01:42:46 +05:00
468d1a929d chore(web): quick fixes 2022-07-27 00:47:38 +05:00
7cdffe30e3 fix(web): webout tab item fixed in studio page 2022-07-26 23:53:07 +05:00
3b1fcb937d fix(web): when no file is selected save the editor content to local storage 2022-07-26 23:30:41 +05:00
3c987c61dd fix(web): saveAs functionality fixed in studio page 2022-07-26 23:15:42 +05:00
0a780697da chore(web): move hooks to hooks folder 2022-07-26 23:14:29 +05:00
83d819df53 chore(web): created custom useStateWithCallback hook 2022-07-26 23:12:55 +05:00
semantic-release-bot
95df2b21d6 chore(release): 0.12.0 [skip ci]
# [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](0f19384999))
* remove drive component ([06d7c91](06d7c91fc3))

### Features

* add api end point for delete folder ([08e0c61](08e0c61e0f))
* add sidebar(drive) to left of studio ([6c35412](6c35412d2f))
* created api endpoint for adding empty folder in drive ([941917e](941917e508))
* implemented api for renaming file/folder ([fdcaba9](fdcaba9d56))
* implemented delete file/folder functionality ([177675b](177675bc89))
* implemented functionality for adding file/folder from sidebar context menu ([0ce94a5](0ce94a553e))
* implemented the functionality for renaming file/folder from context menu ([7010a6a](7010a6a120))
* prevent user from leaving studio page when there are unsaved changes ([6c75502](6c7550286b))
* **web:** add difference view editor in studio ([420a61a](420a61a5a6))
2022-07-26 14:29:41 +00:00
Allan Bowe
accdf914f1 Merge pull request #235 from sasjs/issue-198
feat: deprecate drive and add sidebar for file navigation to studio
2022-07-26 15:25:26 +01:00
15bdd2d7f0 chore: close file menu after clicking on diff editor menu item 2022-07-26 14:54:06 +05:00
2ce947d216 chore: code fixes 2022-07-26 14:16:27 +05:00
ce2114e3f6 chore: code fixes 2022-07-26 00:07:11 +05:00
6c7550286b feat: prevent user from leaving studio page when there are unsaved changes 2022-07-25 22:41:05 +05:00
2360e104bd chore: reduce the padding between tree items 2022-07-25 15:11:02 +05:00
420a61a5a6 feat(web): add difference view editor in studio 2022-07-25 15:01:04 +05:00
04e0f9efe3 chore: merge main into issue-198 2022-07-22 22:31:32 +05:00
99172cd9ed chore: add specs 2022-07-22 22:18:03 +05:00
57daad0c26 chore: error response codes for drive api 2022-07-22 16:58:26 +05:00
cc1e4543fc chore: add specs 2022-07-21 23:03:56 +05:00
03cb89d14f chore: code fixes 2022-07-21 23:03:40 +05:00
72140d73c2 chore: modified folderParamValidation method 2022-07-21 14:08:44 +05:00
efcefd2a42 chore: quick fix 2022-07-21 13:25:46 +05:00
06d7c91fc3 fix: remove drive component 2022-07-20 23:53:42 +05:00
7010a6a120 feat: implemented the functionality for renaming file/folder from context menu 2022-07-20 23:46:39 +05:00
fdcaba9d56 feat: implemented api for renaming file/folder 2022-07-20 23:45:11 +05:00
48688a6547 chore: update swagger docs 2022-07-20 16:52:49 +05:00
0ce94a553e feat: implemented functionality for adding file/folder from sidebar context menu 2022-07-20 16:45:45 +05:00
941917e508 feat: created api endpoint for adding empty folder in drive 2022-07-20 16:43:43 +05:00
semantic-release-bot
5706371ffd chore(release): 0.11.5 [skip ci]
## [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](ce5218a227))
2022-07-19 23:03:43 +00:00
Saad Jutt
ce5218a227 fix: Revert "fix(security): missing cookie flags are added"
This reverts commit 526402fd73.
2022-07-20 03:58:25 +05:00
semantic-release-bot
8b62755f39 chore(release): 0.11.4 [skip ci]
## [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](526402fd73))
2022-07-19 21:06:05 +00:00
Allan Bowe
cb84c3ebbb Merge pull request #234 from sasjs/issue147
fix(security): missing cookie flags are added
2022-07-19 22:02:05 +01:00
Saad Jutt
526402fd73 fix(security): missing cookie flags are added 2022-07-20 01:40:31 +05:00
177675bc89 feat: implemented delete file/folder functionality 2022-07-19 22:49:34 +05:00
721165ff12 chore: add delete confirmation modal and use it in permission component 2022-07-19 22:48:22 +05:00
08e0c61e0f feat: add api end point for delete folder 2022-07-19 22:41:03 +05:00
semantic-release-bot
1b234eb2b1 chore(release): 0.11.3 [skip ci]
## [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](299512135d))
2022-07-19 14:50:19 +00:00
Allan Bowe
ef25eec11f Merge pull request #233 from sasjs/issue-227
fix: filePath fix in code.js file for windows
2022-07-19 15:46:18 +01:00
3e53f70928 chore: update swagger docs 2022-07-19 16:14:40 +05:00
0f19384999 fix: fileTree api response to include an additional attribute isFolder 2022-07-19 16:13:46 +05:00
63dd6813c0 chore: lint fix 2022-07-19 13:07:34 +05:00
299512135d fix: filePath fix in code.js file for windows 2022-07-19 13:00:33 +05:00
6c35412d2f feat: add sidebar(drive) to left of studio 2022-07-18 22:39:09 +05:00
27410bc32b chore: add file path input modal 2022-07-18 22:37:32 +05:00
849b2dd468 chore: add custom tree view component 2022-07-18 22:32:10 +05:00
semantic-release-bot
a1a182698e chore(release): 0.11.2 [skip ci]
## [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](d2ddd8aaca))
2022-07-18 12:39:49 +00:00
Allan Bowe
4be692b24b Merge pull request #232 from sasjs/issue229
fix: apply icon option only for sas.exe
2022-07-18 13:34:21 +01:00
Allan Bowe
d2ddd8aaca fix: apply icon option only for sas.exe 2022-07-18 12:33:52 +00:00
semantic-release-bot
3a45e8f525 chore(release): 0.11.1 [skip ci]
## [0.11.1](https://github.com/sasjs/server/compare/v0.11.0...v0.11.1) (2022-07-18)

### Bug Fixes

* bank operator ([aa02741](aa027414ed))
* ensuring nosplash option only applies for sas.exe ([65e6de9](65e6de9663)), closes [#229](https://github.com/sasjs/server/issues/229)
2022-07-18 12:14:31 +00:00
Allan Bowe
c0e2f55a7b Merge pull request #231 from sasjs/issue229
fix: bank operator
2022-07-18 13:10:30 +01:00
Allan Bowe
aa027414ed fix: bank operator 2022-07-18 12:09:54 +00:00
Allan Bowe
8c4c52b1a9 Merge pull request #230 from sasjs/issue229
fix: ensuring nosplash option only applies for sas.exe
2022-07-18 12:58:15 +01:00
Allan Bowe
ff420434ae chore: removing line added automatically 2022-07-18 11:57:19 +00:00
Allan Bowe
65e6de9663 fix: ensuring nosplash option only applies for sas.exe
Closes #229
2022-07-18 11:55:35 +00:00
semantic-release-bot
2e53d43e11 chore(release): 0.11.0 [skip ci]
# [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](e024a92f16))

### Features

* **logs:** logs to file with rotating + code split into files ([92fda18](92fda183f3))
2022-07-16 21:58:08 +00:00
Allan Bowe
3795f748a7 Merge pull request #228 from sasjs/issue217
Issue217
2022-07-16 22:54:13 +01:00
Saad Jutt
e024a92f16 fix(logs): logs location is configurable 2022-07-16 05:07:00 +05:00
Saad Jutt
92fda183f3 feat(logs): logs to file with rotating + code split into files 2022-07-16 04:42:54 +05:00
Saad Jutt
6f2e6efd03 chore: fixed few vulnerabilites 2022-07-16 03:30:29 +05:00
30d7a65358 chore: fix breaking changes caused by react-router-dom update 2022-07-15 18:42:59 +05:00
5e930f14d2 chore: bump mui/icons-material and react-router-dom versions 2022-07-15 18:41:11 +05:00
9bc68b1cdc chore: update swagger docs 2022-07-15 18:40:02 +05:00
Allan Bowe
3b4e9d20d4 Create FUNDING.yml 2022-07-08 20:51:10 +01:00
semantic-release-bot
4a67d0c63a chore(release): 0.10.0 [skip ci]
# [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](e54a09db19))
* add isAdmin attribute to return response of get session and login requests ([bdf63df](bdf63df1d9))
* add permission authorization middleware to only specific routes ([f3dfc70](f3dfc7083f))
* bumping core and running lint ([a2d1396](a2d1396057))
* controller fixed for deleting permission ([b5f595a](b5f595a25c))
* do not show admin users in add permission modal ([a75edba](a75edbaa32))
* export GroupResponse interface ([38a7db8](38a7db8514))
* move permission filter modal to separate file and icons for different actions ([d000f75](d000f7508f))
* principalId type changed to  number from any ([4fcc191](4fcc191ce9))
* remove clientId from principal types ([0781ddd](0781ddd64e))
* remove duplicates principals from permission filter modal ([5b319f9](5b319f9ad1))
* show loading spinner in studio while executing code ([496247d](496247d0b9))
* show permission component only in server mode ([f863b81](f863b81a7d))
* update permission model ([39fc908](39fc908de1))
* update permission response ([e516b77](e516b7716d))
* **web:** only admin should be able to add, update or delete permission ([be8635c](be8635ccc5))

### Features

* add api endpoint for deleting permission ([0171344](01713440a4))
* add api endpoint for updating permission setting ([540f54f](540f54fb77))
* add authorize middleware for validating permissions ([7d916ec](7d916ec3e9))
* add basic UI for settings and permissions ([5652325](5652325452))
* add documentation link under usename dropdown menu ([eeb63b3](eeb63b330c))
* add permission model ([6bea1f7](6bea1f7666))
* add UI for updating permission ([e8c21a4](e8c21a43b2))
* add validation for registering permission ([e5200c1](e5200c1000))
* add, remove and update permissions from web component ([97ecfdc](97ecfdc955))
* added get authorizedRoutes api endpoint ([b10e932](b10e932605))
* created modal for adding permission ([1413b18](1413b18508))
* defined register permission and get all permissions api endpoints ([1103ffe](1103ffe07b))
* update swagger docs ([797c2bc](797c2bcc39))
2022-07-06 12:31:43 +00:00
Allan Bowe
dea204e3c5 Merge pull request #221 from sasjs/improve-web-UI
fix: Improve web UI
2022-07-06 13:26:19 +01:00
Allan Bowe
5f9e83759c Merge pull request #215 from sasjs/descs
chore: update descriptions
2022-07-06 13:26:00 +01:00
Allan Bowe
fefe63deb1 Merge pull request #151 from sasjs/issue-139
feat: enable permissions
2022-07-06 13:25:27 +01:00
ddd179bbee chore: added specs for verifying permissions 2022-07-05 16:18:14 +05:00
a10b87930c chore: quick fix 2022-07-05 15:29:44 +05:00
496247d0b9 fix: show loading spinner in studio while executing code 2022-07-05 08:23:51 +05:00
eeb63b330c feat: add documentation link under usename dropdown menu 2022-07-05 08:23:36 +05:00
Saad Jutt
1108d3dd7b chore: quick fix 2022-07-05 05:30:13 +05:00
Saad Jutt
7edb47a4cb chore: build fix 2022-07-05 03:40:54 +05:00
Saad Jutt
451cb4f6dd chore: fixed specs 2022-07-05 03:26:37 +05:00
Saad Jutt
0b759a5594 chore: code fixes API + web 2022-07-05 02:34:33 +05:00
Saad Jutt
5338ffb211 chore: Merge branch 'main' into issue-139 2022-07-04 23:59:51 +05:00
e42fdd3575 chore: conditionally call authorize middleware from authenticateToken 2022-07-04 20:13:46 +05:00
b10e932605 feat: added get authorizedRoutes api endpoint 2022-07-04 19:14:06 +05:00
e54a09db19 fix: add authorize middleware for appStreams 2022-07-04 17:14:17 +05:00
4c35e04802 chore: add snackbar for showing success alert 2022-07-04 16:00:23 +05:00
b5f595a25c fix: controller fixed for deleting permission 2022-07-04 04:27:58 +05:00
semantic-release-bot
a131adbae7 chore(release): 0.9.0 [skip ci]
# [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](9c3da56901))
2022-07-03 10:40:36 +00:00
Allan Bowe
a20c3b9719 Merge pull request #220 from sasjs/issue213
feat: removed secrets from env variables
2022-07-03 11:36:24 +01:00
Saad Jutt
eee3a7b084 chore: code refactor 2022-07-03 07:03:15 +05:00
Saad Jutt
9c3da56901 feat: removed secrets from env variables 2022-07-03 06:56:18 +05:00
Allan Bowe
7e6524d7e4 chore: removing badge 2022-07-02 15:12:27 +01:00
Allan Bowe
0ea2690616 adding matrix chat link 2022-07-02 13:10:20 +01:00
semantic-release-bot
b369759f0f chore(release): 0.8.3 [skip ci]
## [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](e290751c87))
2022-07-02 10:01:26 +00:00
Allan Bowe
ac9a835c5a Merge pull request #219 from sasjs/issue211
fix(deploy): extract first json from zip file
2022-07-02 10:57:16 +01:00
Saad Jutt
e290751c87 fix(deploy): extract first json from zip file 2022-07-02 14:39:33 +05:00
e516b7716d fix: update permission response 2022-07-02 01:03:53 +05:00
f3dfc7083f fix: add permission authorization middleware to only specific routes 2022-07-01 16:50:24 +05:00
7d916ec3e9 feat: add authorize middleware for validating permissions 2022-06-29 23:06:58 +05:00
70f279a49c chore: update swagger.yaml 2022-06-28 09:23:53 +05:00
66a3537271 chore: add specs 2022-06-28 06:50:35 +05:00
ca64c13909 chore: add principal type and permission setting enums 2022-06-28 00:00:04 +05:00
0a73a35547 chore: improve error handling 2022-06-27 23:21:48 +05:00
a75edbaa32 fix: do not show admin users in add permission modal 2022-06-26 01:49:07 +05:00
4ddfec0403 chore: add isAdmin field in user response 2022-06-26 01:48:31 +05:00
35439d7d51 chore: throw error when adding permission for admin user 2022-06-24 23:19:19 +05:00
907aa485fd chore: throw error when creating duplicate permission 2022-06-24 23:15:41 +05:00
888627e1c8 chore: close filterModal after applying/reseting 2022-06-24 22:32:18 +05:00
9cb9e2dd33 chore: add filter based on principal type 2022-06-24 16:28:41 +05:00
54d4bf835d chore: show principal type in permissions list 2022-06-24 15:50:09 +05:00
67fe298fd5 chore: lint fixes 2022-06-24 14:55:05 +05:00
97ecfdc955 feat: add, remove and update permissions from web component 2022-06-24 14:48:57 +05:00
5b319f9ad1 fix: remove duplicates principals from permission filter modal 2022-06-23 23:58:40 +05:00
be8635ccc5 fix(web): only admin should be able to add, update or delete permission 2022-06-23 23:35:06 +05:00
f863b81a7d fix: show permission component only in server mode 2022-06-23 23:14:54 +05:00
bdf63df1d9 fix: add isAdmin attribute to return response of get session and login requests 2022-06-23 22:50:00 +05:00
4c6b9c5e93 Merge branch 'main' into issue-139 2022-06-23 17:21:52 +05:00
Allan Bowe
a2d1396057 fix: bumping core and running lint 2022-06-23 09:00:22 +00:00
Allan Bowe
b2f21eb3ac chore: update descriptions 2022-06-23 08:44:12 +00:00
semantic-release-bot
71bcbb9134 chore(release): 0.8.2 [skip ci]
## [0.8.2](https://github.com/sasjs/server/compare/v0.8.1...v0.8.2) (2022-06-22)

### Bug Fixes

* getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes ([5cc85b5](5cc85b57f8))
2022-06-22 10:18:59 +00:00
Allan Bowe
c86f0feff8 Merge pull request #214 from sasjs/fix-runtime-filePath
fix: getRuntimeAndFilePath function
2022-06-22 12:14:12 +02:00
Allan Bowe
d3d2ab9a36 Update getRunTimeAndFilePath.ts 2022-06-22 11:12:48 +01:00
5cc85b57f8 fix: getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes 2022-06-22 14:24:06 +05:00
semantic-release-bot
ae0fc0c48c chore(release): 0.8.1 [skip ci]
## [0.8.1](https://github.com/sasjs/server/compare/v0.8.0...v0.8.1) (2022-06-21)

### Bug Fixes

* make CA_ROOT optional in getCertificates method ([1b5859e](1b5859ee37))
* update /logout route to /SASLogon/logout ([65380be](65380be2f3))
2022-06-21 20:10:31 +00:00
Saad Jutt
555c5d54e2 Merge pull request #212 from sasjs/update-logout-route
Update logout route
2022-06-21 13:06:30 -07:00
1b5859ee37 fix: make CA_ROOT optional in getCertificates method 2022-06-22 00:25:41 +05:00
65380be2f3 fix: update /logout route to /SASLogon/logout 2022-06-22 00:24:41 +05:00
Yury Shkoda
1933be15c2 Merge pull request #210 from sasjs/pr-template
chore(template): added pull request template
2022-06-21 19:11:19 +03:00
Yury Shkoda
56b20beb8c chore(template): added pull request template 2022-06-21 19:07:14 +03:00
semantic-release-bot
bfc5ac6a4f chore(release): 0.8.0 [skip ci]
# [0.8.0](https://github.com/sasjs/server/compare/v0.7.3...v0.8.0) (2022-06-21)

### Features

* **certs:** ENV variables updated and set CA Root for HTTPS server ([2119e9d](2119e9de9a))
2022-06-21 07:24:26 +00:00
Allan Bowe
6376173de0 Merge pull request #209 from sasjs/certificate-ca-root
feat(certs): ENV variables updated and set CA Root for HTTPS server
2022-06-21 09:18:15 +02:00
Saad Jutt
3130fbeff0 chore: code refactor for getting session controller 2022-06-21 03:41:44 +05:00
Saad Jutt
01e9a1d9e9 chore: README.md and .env.example updated 2022-06-21 03:26:12 +05:00
Saad Jutt
2119e9de9a feat(certs): ENV variables updated and set CA Root for HTTPS server 2022-06-21 03:17:14 +05:00
semantic-release-bot
87dbab98f6 chore(release): 0.7.3 [skip ci]
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)

### Bug Fixes

* path descriptions and defaults ([5d5d6ce](5d5d6ce326))
2022-06-20 15:31:20 +00:00
Allan Bowe
1bf122a0a2 Merge pull request #207 from sasjs/allanbowe/sasjs-server-fails-to-205
fix: path descriptions and defaults
2022-06-20 17:26:56 +02:00
Allan Bowe
5d5d6ce326 fix: path descriptions and defaults 2022-06-20 15:07:17 +00:00
semantic-release-bot
620eddb713 chore(release): 0.7.2 [skip ci]
## [0.7.2](https://github.com/sasjs/server/compare/v0.7.1...v0.7.2) (2022-06-20)

### Bug Fixes

* removing UTF-8 options from commandline.  There appears to be no reliable way to enforce ([f6dc74f](f6dc74f16b))
2022-06-20 15:03:27 +00:00
Allan Bowe
3c92034da3 Merge pull request #206 from sasjs/allanbowe/sasjs-server-fails-to-205
fix: removing UTF-8 options from commandline.
2022-06-20 16:57:58 +02:00
Allan Bowe
f6dc74f16b fix: removing UTF-8 options from commandline. There appears to be no reliable way to enforce
UTF-8 without additional modifications to the PATH variable to ensure a DBCS instance of SAS.
2022-06-20 14:38:19 +00:00
fa63dc071b chore: update specs and swagger.yaml 2022-05-18 00:29:42 +05:00
e8c21a43b2 feat: add UI for updating permission 2022-05-18 00:20:49 +05:00
1413b18508 feat: created modal for adding permission 2022-05-18 00:05:28 +05:00
dfbd155711 chore: move common interfaces to utils folder 2022-05-18 00:04:37 +05:00
4fcc191ce9 fix: principalId type changed to number from any 2022-05-18 00:03:11 +05:00
d000f7508f fix: move permission filter modal to separate file and icons for different actions 2022-05-17 15:42:29 +05:00
5652325452 feat: add basic UI for settings and permissions 2022-05-16 23:53:30 +05:00
0781ddd64e fix: remove clientId from principal types 2022-05-16 19:56:56 +05:00
7be77cc38a chore: remvoe code redundancy and add specs for get permissions api endpoint 2022-05-10 07:05:59 +05:00
98b8a75148 chore: add specs for delete permission api endpoint 2022-05-10 06:40:34 +05:00
72a3197a06 chore: add spec for update permission when permission with provided id not exists 2022-05-10 06:25:52 +05:00
fce05d6959 chore: add spec for invalid principal type 2022-05-10 06:18:19 +05:00
1aec3abd28 chore: add specs for update permission api endpoint 2022-05-10 06:11:24 +05:00
9136c95013 chore: write specs for create permission api endpoint 2022-05-09 13:08:15 +05:00
Saad Jutt
89b32e70ff refactor: code in permission controller 2022-04-30 03:49:26 +05:00
01713440a4 feat: add api endpoint for deleting permission 2022-04-30 01:16:52 +05:00
540f54fb77 feat: add api endpoint for updating permission setting 2022-04-30 01:02:47 +05:00
bf906aa544 Merge branch 'main' into issue-139 2022-04-29 15:41:35 +05:00
797c2bcc39 feat: update swagger docs 2022-04-29 15:31:24 +05:00
1103ffe07b feat: defined register permission and get all permissions api endpoints 2022-04-29 15:30:41 +05:00
e5200c1000 feat: add validation for registering permission 2022-04-29 15:28:29 +05:00
38a7db8514 fix: export GroupResponse interface 2022-04-29 15:27:34 +05:00
39fc908de1 fix: update permission model 2022-04-29 15:26:26 +05:00
be009d5b02 Merge branch 'main' into issue-139 2022-04-29 00:32:36 +05:00
6bea1f7666 feat: add permission model 2022-04-28 21:18:23 +05:00
156 changed files with 10586 additions and 3825 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [sasjs]

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@ node_modules/
.env*
sas/
sasjs_root/
api/mocks/custom/*
!api/mocks/custom/.keep
tmp/
build/
sasjsbuild/

View File

@@ -1,3 +1,458 @@
## [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)
### Bug Fixes
* getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes ([5cc85b5](https://github.com/sasjs/server/commit/5cc85b57f80b13296156811fe966d7b37d45f213))
## [0.8.1](https://github.com/sasjs/server/compare/v0.8.0...v0.8.1) (2022-06-21)
### Bug Fixes
* make CA_ROOT optional in getCertificates method ([1b5859e](https://github.com/sasjs/server/commit/1b5859ee37ae73c419115b9debfd5141a79733de))
* update /logout route to /SASLogon/logout ([65380be](https://github.com/sasjs/server/commit/65380be2f3945bae559f1749064845b514447a53))
# [0.8.0](https://github.com/sasjs/server/compare/v0.7.3...v0.8.0) (2022-06-21)
### Features
* **certs:** ENV variables updated and set CA Root for HTTPS server ([2119e9d](https://github.com/sasjs/server/commit/2119e9de9ab1e5ce1222658f554ac74f4f35cf4d))
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)
### Bug Fixes
* path descriptions and defaults ([5d5d6ce](https://github.com/sasjs/server/commit/5d5d6ce3265a43af2e22bcd38cda54fafaf7b3ef))
## [0.7.2](https://github.com/sasjs/server/compare/v0.7.1...v0.7.2) (2022-06-20)
### Bug Fixes
* removing UTF-8 options from commandline. There appears to be no reliable way to enforce ([f6dc74f](https://github.com/sasjs/server/commit/f6dc74f16bddafa1de9c83c2f27671a241abdad4))
## [0.7.1](https://github.com/sasjs/server/compare/v0.7.0...v0.7.1) (2022-06-20)

19
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1,19 @@
## Issue
Link any related issue(s) in this section.
## Intent
What this PR intends to achieve.
## Implementation
What code changes have been made to achieve the intent.
## Checks
- [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] Any new functionality has been unit tested.
- [ ] All unit tests are passing (`npm test`).
- [ ] All CI checks are green.
- [ ] Reviewer is assigned.

View File

@@ -64,12 +64,30 @@ Example contents of a `.env` file:
# Server mode is multi-user and suitable for intranet / internet use
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)
SAS_PATH=/path/to/sas/executable.exe
# Path to Node.js executable
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
# This location is for SAS WORK, staged files, DRIVE, configuration etc
SASJS_ROOT=./sasjs_root
@@ -81,6 +99,9 @@ PROTOCOL=
# default: 5000
PORT=
# options: [sas9|sasviya]
# If not present, mocking function is disabled
MOCK_SERVERTYPE=
#
## Additional SAS Options
@@ -99,17 +120,24 @@ SASV9_OPTIONS= -NOXCMD
## Additional Web Server Options
#
# ENV variables required for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem
# ENV variables for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem (required)
CERT_CHAIN=certificate.pem (required)
CA_ROOT=fullchain.pem (optional)
# ENV variables required for MODE: `server`
ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>
SESSION_SECRET=<secret>
## ENV variables required for MODE: `server`
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`
# If enabled, be sure to also configure the WHITELIST of third party servers.
CORS=
@@ -139,12 +167,8 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
LOG_FORMAT_MORGAN=
# A comma separated string that defines the available runTimes.
# Priority is given to the runtime that comes first in the string.
# Possible options at the moment are sas and js
# options: [sas,js|js,sas|sas|js] default:sas
RUN_TIMES=
# This location is for server logs with classical UNIX logrotate behavior
LOG_LOCATION=./sasjs_root/logs
```

View File

@@ -4,23 +4,31 @@ WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http
PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem
CERT_CHAIN=certificate.pem
CA_ROOT=fullchain.pem
PORT=[5000] default value is 5000
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
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
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
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
PYTHON_PATH=/usr/bin/python
R_PATH=/usr/bin/Rscript
SASJS_ROOT=./sasjs_root
LOG_FORMAT_MORGAN=common
LOG_FORMAT_MORGAN=common
LOG_LOCATION=./sasjs_root/logs

0
api/mocks/custom/.keep Normal file
View File

View File

@@ -0,0 +1 @@
You have signed in.

View File

@@ -0,0 +1 @@
You have signed out.

View 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>

View File

@@ -0,0 +1 @@
Public access has been denied.

View File

@@ -0,0 +1 @@
"title": "Log Off SAS Demo User"

2234
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,10 @@
"description": "Api of SASjs server",
"main": "./src/server.ts",
"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",
"prebuild": "npm run initial",
"start": "nodemon ./src/server.ts",
"start": "NODE_ENV=development nodemon ./src/server.ts",
"start:prod": "node ./build/src/server.js",
"build": "rimraf build && tsc",
"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": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"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/",
"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/",
"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",
"pkg": {
"assets": [
"./build/public/**/*",
"./build/sasjsbuild/**/*",
"./build/sasjscore/**/*",
"./build/sas/**/*",
"./web/build/**/*"
],
"targets": [
@@ -47,22 +48,23 @@
},
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^4.27.3",
"@sasjs/utils": "2.42.1",
"@sasjs/core": "^4.40.1",
"@sasjs/utils": "2.48.1",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1",
"express-session": "^1.17.2",
"helmet": "^5.0.2",
"joi": "^17.4.2",
"jsonwebtoken": "^8.5.1",
"ldapjs": "2.3.3",
"mongoose": "^6.0.12",
"mongoose-sequence": "^5.3.1",
"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",
"unzipper": "^0.10.11",
"url": "^0.10.3"
@@ -72,11 +74,11 @@
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.12",
"@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24",
"@types/jsonwebtoken": "^8.5.5",
"@types/ldapjs": "^2.2.4",
"@types/mongoose-sequence": "^3.0.6",
"@types/morgan": "^1.9.3",
"@types/multer": "^1.4.7",
@@ -85,10 +87,13 @@
"@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9",
"axios": "0.27.2",
"csrf": "^3.1.0",
"dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
"mongodb-memory-server": "^8.0.0",
"nodejs-file-downloader": "4.10.2",
"nodemon": "^2.0.7",
"pkg": "5.6.0",
"prettier": "^2.3.1",

View File

@@ -47,41 +47,6 @@ components:
- userId
type: object
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:
properties:
clientId:
@@ -97,52 +62,12 @@ components:
- clientSecret
type: object
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:
enum:
- sas
- js
- py
- r
type: string
ExecuteCodePayload:
properties:
@@ -265,7 +190,7 @@ components:
- fileTree
type: object
additionalProperties: false
UpdateFileResponse:
FileFolderResponse:
properties:
status:
type: string
@@ -275,6 +200,31 @@ components:
- status
type: object
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:
properties:
name:
@@ -283,6 +233,8 @@ components:
type: string
absolutePath:
type: string
isFolder:
type: boolean
children:
items:
$ref: '#/components/schemas/TreeNode'
@@ -291,6 +243,7 @@ components:
- name
- relativePath
- absolutePath
- isFolder
- children
type: object
additionalProperties: false
@@ -314,10 +267,13 @@ components:
type: string
displayName:
type: string
isAdmin:
type: boolean
required:
- id
- username
- displayName
- isAdmin
type: object
additionalProperties: false
GroupResponse:
@@ -435,27 +391,13 @@ components:
- description
type: object
additionalProperties: false
_LeanDocument__LeanDocument_T__:
FlattenMaps_T_:
properties: {}
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:
$ref: '#/components/schemas/LeanDocument_this_'
$ref: '#/components/schemas/FlattenMaps_T_'
ObjectId:
type: string
InfoResponse:
properties:
mode:
@@ -480,7 +422,94 @@ components:
- runTimes
type: object
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:
_program:
type: string
@@ -488,6 +517,41 @@ components:
example: /Public/somefolder/some.file
type: object
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:
bearerAuth:
type: http
@@ -558,24 +622,28 @@ paths:
-
bearerAuth: []
parameters: []
/:
/SASjsApi/authConfig:
get:
operationId: Home
operationId: GetDetail
responses:
'200':
description: Ok
content:
application/json:
schema:
type: string
summary: 'Render index.html'
schema: {}
examples:
'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:
- Web
security: []
- Auth_Config
security:
-
bearerAuth: []
parameters: []
/SASLogon/login:
/SASjsApi/authConfig/synchroniseWithLDAP:
post:
operationId: Login
operationId: SynchroniseWithLDAP
responses:
'200':
description: Ok
@@ -583,60 +651,21 @@ paths:
application/json:
schema:
properties:
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
loggedIn: {type: boolean}
groupCount: {type: number, format: double}
userCount: {type: number, format: double}
required:
- user
- loggedIn
- groupCount
- userCount
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'
value: {users: 5, groups: 3}
summary: 'Synchronises LDAP users and groups with internal DB and returns the count of imported users and groups.'
tags:
- Web
security: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizePayload'
/logout:
get:
operationId: Logout
responses:
'200':
description: Ok
content:
application/json:
schema: {}
summary: 'Accept a valid username/password'
tags:
- Web
security: []
- Auth_Config
security:
-
bearerAuth: []
parameters: []
/SASjsApi/client:
post:
@@ -673,11 +702,13 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
description: 'Execute SAS code.'
summary: 'Run SAS Code and returns log'
anyOf:
- {type: string}
- {type: string, format: byte}
description: 'Execute Code on the Specified Runtime'
summary: 'Run Code and Return Webout Content and Log'
tags:
- CODE
- Code
security:
-
bearerAuth: []
@@ -763,7 +794,7 @@ paths:
examples:
'Example 1':
value: {status: failure, message: 'Deployment failed!'}
description: "Accepts JSON file and zipped compressed JSON file as well.\r\nCompressed file should only contain one JSON file and should have same name\r\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\r\nAny other file or JSON file in zipped will be ignored!"
description: "Accepts JSON file and zipped compressed JSON file as well.\nCompressed file should only contain one JSON file and should have same name\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\nAny other file or JSON file in zipped will be ignored!"
summary: 'Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.'
tags:
- Drive
@@ -838,7 +869,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateFileResponse'
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: success}
@@ -847,11 +878,11 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateFileResponse'
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: failure, message: 'File request failed.'}
description: "It's optional to either provide `_filePath` in url as query parameter\r\nOr provide `filePath` in body as form field.\r\nBut it's required to provide else API will respond with Bad Request."
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Create a file in SASjs Drive'
tags:
- Drive
@@ -860,7 +891,7 @@ paths:
bearerAuth: []
parameters:
-
description: 'Location of SAS program'
description: 'Location of file'
in: query
name: _filePath
required: false
@@ -889,7 +920,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateFileResponse'
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: success}
@@ -898,11 +929,11 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateFileResponse'
$ref: '#/components/schemas/FileFolderResponse'
examples:
'Example 1':
value: {status: failure, message: 'File request failed.'}
description: "It's optional to either provide `_filePath` in url as query parameter\r\nOr provide `filePath` in body as form field.\r\nBut it's required to provide else API will respond with Bad Request."
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Modify a file in SASjs Drive'
tags:
- Drive
@@ -962,6 +993,102 @@ paths:
schema:
type: string
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:
get:
operationId: GetFileTree
@@ -993,7 +1120,7 @@ paths:
type: array
examples:
'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.'
tags:
- User
@@ -1312,7 +1439,7 @@ paths:
schema:
allOf:
- {$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.'
tags:
- Group
@@ -1422,6 +1549,128 @@ paths:
- Info
security: []
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:
get:
operationId: Session
@@ -1434,7 +1683,7 @@ paths:
$ref: '#/components/schemas/UserResponse'
examples:
'Example 1':
value: {id: 123, username: johnusername, displayName: John}
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
summary: 'Get session info (username).'
tags:
- Session
@@ -1444,7 +1693,7 @@ paths:
parameters: []
/SASjsApi/stp/execute:
get:
operationId: ExecuteReturnRaw
operationId: ExecuteGetRequest
responses:
'200':
description: Ok
@@ -1454,8 +1703,8 @@ paths:
anyOf:
- {type: string}
- {type: string, format: byte}
description: "Trigger a SAS or JS program using the _program URL parameter.\r\n\r\nAccepts URL parameters and file uploads. For more details, see docs:\r\n\r\nhttps://server.sasjs.io/storedprograms"
summary: 'Execute a Stored Program, returns raw _webout content.'
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 _webout and (optionally) log.'
tags:
- STP
security:
@@ -1463,7 +1712,7 @@ paths:
bearerAuth: []
parameters:
-
description: 'Location of SAS or JS code'
description: 'Location of code in SASjs Drive'
in: query
name: _program
required: true
@@ -1471,19 +1720,18 @@ paths:
type: string
example: /Projects/myApp/some/program
post:
operationId: ExecuteReturnJson
operationId: ExecutePostRequest
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
examples:
'Example 1':
value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}}
description: "Trigger a SAS or JS program using the _program URL parameter.\r\n\r\nAccepts URL parameters and file uploads. For more details, see docs:\r\n\r\nhttps://server.sasjs.io/storedprograms\r\n\r\nThe response will be a JSON object with the following root attributes:\r\nlog, webout, headers.\r\n\r\nThe webout attribute will be nested JSON ONLY if the response-header\r\ncontains a content-type of application/json AND it is valid JSON.\r\nOtherwise it will be a stringified version of the webout content."
summary: 'Execute a Stored Program, return a JSON object'
anyOf:
- {type: string}
- {type: string, format: byte}
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 _webout and (optionally) log.'
tags:
- STP
security:
@@ -1491,7 +1739,7 @@ paths:
bearerAuth: []
parameters:
-
description: 'Location of SAS or JS code'
description: 'Location of code in SASjs Drive'
in: query
name: _program
required: false
@@ -1503,38 +1751,124 @@ paths:
content:
application/json:
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:
-
url: /
tags:
-
name: Info
description: 'Get Server Info'
name: Auth
description: 'Operations about auth'
-
name: Session
description: 'Get Session information'
-
name: User
description: 'Operations about users'
name: Auth_Config
description: 'Operations about external auth providers'
-
name: Client
description: 'Operations about clients'
-
name: Auth
description: 'Operations about auth'
name: Code
description: 'Execution of code (various runtimes are supported)'
-
name: Drive
description: 'Operations about drive'
description: 'Operations on SASjs Drive'
-
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
description: 'Operations about STP'
description: 'Execution of Stored Programs'
-
name: CODE
description: 'Operations on SAS code'
name: User
description: 'Operations with users'
-
name: Web
description: 'Operations on Web'

View File

@@ -6,12 +6,12 @@ import {
readFile,
SASJsFileType
} 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 compiledSystemInit = async (systemInit: string) =>
'options ps=max;\n' +
'options ls=max ps=max;\n' +
(await loadDependenciesFile({
fileContent: systemInit,
type: SASJsFileType.job,

View File

@@ -8,7 +8,11 @@ import {
listFilesInFolder
} 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')

View 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()

View 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 }))
}
}

View 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
})
)
}
}

View 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))
}

View 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
})
)
}

View File

@@ -0,0 +1,4 @@
export * from './configureCors'
export * from './configureExpressSession'
export * from './configureLogger'
export * from './configureSecurity'

View File

@@ -1,30 +1,25 @@
import path from 'path'
import express, { ErrorRequestHandler } from 'express'
import csrf from 'csurf'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import morgan from 'morgan'
import express, { ErrorRequestHandler, CookieOptions } from 'express'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
import cors from 'cors'
import helmet from 'helmet'
import {
connectDB,
copySASjsCore,
CorsType,
getWebBuildFolder,
HelmetCoepType,
instantiateLogger,
loadAppStreamConfig,
ModeType,
ProtocolType,
ReturnCode,
setProcessVariables,
setupFolders,
verifyEnvVariables
} from './utils'
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
import {
configureCors,
configureExpressSession,
configureLogger,
configureSecurity
} from './app-modules'
dotenv.config()
@@ -34,108 +29,49 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express()
app.use(cookieParser())
const { PROTOCOL } = process.env
const {
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 = {
export const cookieOptions: CookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
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) => {
if (err.code === 'EBADCSRFTOKEN')
return res.status(400).send('Invalid CSRF token!')
console.error(err.stack)
res.status(500).send('Something broke!')
}
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 copySASjsCore()

View File

@@ -4,6 +4,7 @@ import { InfoJWT } from '../types'
import {
generateAccessToken,
generateRefreshToken,
getTokensFromDB,
removeTokensInDB,
saveTokensInDB
} from '../utils'
@@ -73,6 +74,15 @@ const token = async (data: any): Promise<TokenResponse> => {
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 refreshToken = generateRefreshToken(userInfo)
@@ -129,8 +139,8 @@ const verifyAuthCode = async (
clientId: string,
code: string
): Promise<InfoJWT | undefined> => {
return new Promise((resolve, reject) => {
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
return new Promise((resolve) => {
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
if (err) return resolve(undefined)
const clientInfo: InfoJWT = {

View 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
}

View File

@@ -1,7 +1,6 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal'
import { ExecuteReturnJsonResponse } from '.'
import { ExecutionController } from './internal'
import {
getPreProgramVariables,
getUserAutoExec,
@@ -25,17 +24,17 @@ interface ExecuteCodePayload {
@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('CODE')
@Tags('Code')
export class CodeController {
/**
* Execute SAS code.
* @summary Run SAS Code and returns log
* Execute Code on the Specified Runtime
* @summary Run Code and Return Webout Content and Log
*/
@Post('/execute')
public async executeCode(
@Request() request: express.Request,
@Body() body: ExecuteCodePayload
): Promise<ExecuteReturnJsonResponse> {
): Promise<string | Buffer> {
return executeCode(request, body)
}
}
@@ -51,22 +50,15 @@ const executeCode = async (
: await getUserAutoExec()
try {
const { webout, log, httpHeaders } =
(await new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
returnJson: true,
runTime: runTime
})) as ExecuteReturnJson
const { result } = await new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime
})
return {
status: 'success',
_webout: webout as string,
log: parseLogToArray(log),
httpHeaders
}
return result
} catch (err: any) {
throw {
code: 400,

View File

@@ -22,6 +22,7 @@ import {
moveFile,
createFolder,
deleteFile as deleteFileOnSystem,
deleteFolder as deleteFolderOnSystem,
folderExists,
listFilesInFolder,
listSubFoldersInFolder,
@@ -58,11 +59,32 @@ interface GetFileTreeResponse {
tree: TreeNode
}
interface UpdateFileResponse {
interface FileFolderResponse {
status: 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 successDeployResponse: DeployResponse = {
@@ -143,7 +165,7 @@ export class DriveController {
/**
*
* @summary Delete file from SASjs Drive
* @query _filePath Location of SAS program
* @query _filePath Location of file
* @example _filePath "/Public/somefolder/some.file"
*/
@Delete('/file')
@@ -151,20 +173,31 @@ export class DriveController {
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
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
*
* @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<UpdateFileResponse>({
@Example<FileFolderResponse>({
status: 'success'
})
@Response<UpdateFileResponse>(403, 'File already exists', {
@Response<FileFolderResponse>(403, 'File already exists', {
status: 'failure',
message: 'File request failed.'
})
@@ -173,10 +206,28 @@ export class DriveController {
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<UpdateFileResponse> {
): Promise<FileFolderResponse> {
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
* Or provide `filePath` in body as form field.
@@ -187,10 +238,10 @@ export class DriveController {
* @example _filePath "/Public/somefolder/some.file.sas"
*
*/
@Example<UpdateFileResponse>({
@Example<FileFolderResponse>({
status: 'success'
})
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
@Response<FileFolderResponse>(403, `File doesn't exist`, {
status: 'failure',
message: 'File request failed.'
})
@@ -199,10 +250,28 @@ export class DriveController {
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<UpdateFileResponse> {
): Promise<FileFolderResponse> {
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.
*
@@ -249,20 +318,26 @@ const getFile = async (req: express.Request, filePath: string) => {
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot get file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't get file outside drive.`
}
if (!(await fileExists(filePathFull))) {
throw new Error("File doesn't exist.")
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
const extension = path.extname(filePathFull).toLowerCase()
if (extension === '.sas') {
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) => {
@@ -273,17 +348,26 @@ const getFolder = async (folderPath?: string) => {
.join(getFilesFolder(), folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(driveFilesPath)) {
throw new Error('Cannot get folder outside drive.')
}
if (!folderPathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't get folder outside drive.`
}
if (!(await folderExists(folderPathFull))) {
throw new Error("Folder doesn't exist.")
}
if (!(await folderExists(folderPathFull)))
throw {
code: 404,
status: 'Not Found',
message: `Folder doesn't exist.`
}
if (!(await isFolder(folderPathFull))) {
throw new Error('Not a Folder.')
}
if (!(await isFolder(folderPathFull)))
throw {
code: 400,
status: 'Bad Request',
message: 'Not a Folder.'
}
const files: string[] = await listFilesInFolder(folderPathFull)
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
@@ -302,19 +386,51 @@ const deleteFile = async (filePath: string) => {
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot delete file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't delete file outside drive.`
}
if (!(await fileExists(filePathFull))) {
throw new Error('File does not exist.')
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
await deleteFileOnSystem(filePathFull)
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 (
filePath: string,
multerFile: Express.Multer.File
@@ -325,13 +441,19 @@ const saveFile = async (
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot put file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't put file outside drive.`
}
if (await fileExists(filePathFull)) {
throw new Error('File already exists.')
}
if (await fileExists(filePathFull))
throw {
code: 409,
status: 'Conflict',
message: 'File already exists.'
}
const folderPath = path.dirname(filePathFull)
await createFolder(folderPath)
@@ -340,6 +462,88 @@ const saveFile = async (
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 (
filePath: string,
multerFile: Express.Multer.File
@@ -350,13 +554,19 @@ const updateFile = async (
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot modify file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't modify file outside drive.`
}
if (!(await fileExists(filePathFull))) {
throw new Error(`File doesn't exist.`)
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
await moveFile(multerFile.path, filePathFull)

View File

@@ -10,8 +10,9 @@ import {
Body
} from 'tsoa'
import Group, { GroupPayload } from '../model/Group'
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
import User from '../model/User'
import { AuthProviderType } from '../utils'
import { UserResponse } from './user'
export interface GroupResponse {
@@ -20,7 +21,7 @@ export interface GroupResponse {
description: string
}
interface GroupDetailsResponse {
export interface GroupDetailsResponse {
groupId: number
name: string
description: string
@@ -147,12 +148,14 @@ export class GroupController {
@Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) {
const group = await Group.findOne({ groupId })
if (group) return await group.remove()
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
if (!group)
throw {
code: 404,
status: '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'
).populate(
'users',
'id username displayName -_id'
'id username displayName isAdmin -_id'
)) as unknown as GroupDetailsResponse
if (!group)
throw {
@@ -241,6 +244,20 @@ const updateUsersListInGroup = async (
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 })
if (!user)
throw {
@@ -249,9 +266,17 @@ const updateUsersListInGroup = async (
message: 'User not found.'
}
const updatedGroup = (action === 'addUser'
? await group.addUser(user._id)
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
if (user.authProvider)
throw {
code: 405,
status: 'Method Not Allowed',
message: `Can't add/remove user to group created by external auth provider.`
}
const updatedGroup =
action === 'addUser'
? await group.addUser(user)
: await group.removeUser(user)
if (!updatedGroup)
throw {
@@ -260,9 +285,6 @@ const updateUsersListInGroup = async (
message: 'Unable to update group.'
}
if (action === 'addUser') user.addGroup(group._id)
else user.removeGroup(group._id)
return {
groupId: updatedGroup.groupId,
name: updatedGroup.name,

View File

@@ -1,9 +1,11 @@
export * from './auth'
export * from './authConfig'
export * from './client'
export * from './code'
export * from './drive'
export * from './group'
export * from './info'
export * from './permission'
export * from './session'
export * from './stp'
export * from './user'

View File

@@ -1,4 +1,8 @@
import { Route, Tags, Example, Get } from 'tsoa'
import { getAuthorizedRoutes } from '../utils'
export interface AuthorizedRoutesResponse {
paths: string[]
}
export interface InfoResponse {
mode: string
@@ -36,4 +40,19 @@ export class InfoController {
}
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
}
}

View File

@@ -1,10 +1,6 @@
import path from 'path'
import fs from 'fs'
import {
getSASSessionController,
getJSSessionController,
processProgram
} from './'
import { getSessionController, processProgram } from './'
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
import { PreProgramVars, Session, TreeNode } from '../../types'
import {
@@ -24,12 +20,6 @@ export interface ExecuteReturnRaw {
result: string | Buffer
}
export interface ExecuteReturnJson {
httpHeaders: HTTPHeaders
webout: string | Buffer
log?: string
}
interface ExecuteFileParams {
programPath: string
preProgramVariables: PreProgramVars
@@ -72,14 +62,10 @@ export class ExecutionController {
preProgramVariables,
vars,
otherArgs,
returnJson,
session: sessionByFileUpload,
runTime
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
const sessionController = getSessionController(runTime)
const session =
sessionByFileUpload ?? (await sessionController.getSession())
@@ -103,6 +89,7 @@ export class ExecutionController {
vars,
session,
weboutPath,
headersPath,
tokenFile,
runTime,
logPath,
@@ -114,10 +101,7 @@ export class ExecutionController {
? await readFile(headersPath)
: ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
const fileResponse: boolean =
httpHeaders.hasOwnProperty('content-type') &&
!returnJson && // not a POST Request
!isDebugOn(vars) // Debug is not enabled
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
const webout = (await fileExists(weboutPath))
? fileResponse
@@ -128,19 +112,11 @@ export class ExecutionController {
// it should be deleted by scheduleSessionDestroy
session.inUse = false
if (returnJson) {
return {
httpHeaders,
webout,
log: isDebugOn(vars) || session.crashed ? log : undefined
}
}
return {
httpHeaders,
result:
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
}
}
@@ -150,6 +126,7 @@ export class ExecutionController {
name: 'files',
relativePath: '',
absolutePath: getFilesFolder(),
isFolder: true,
children: []
}
@@ -159,15 +136,22 @@ export class ExecutionController {
const currentNode = stack.pop()
if (currentNode) {
currentNode.isFolder = fs
.statSync(currentNode.absolutePath)
.isDirectory()
const children = fs.readdirSync(currentNode.absolutePath)
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 childNode: TreeNode = {
name: child,
relativePath: relativeChildPath,
absolutePath: absoluteChildPath,
isFolder: false,
children: []
}
currentNode.children.push(childNode)

View File

@@ -1,7 +1,7 @@
import { Request, RequestHandler } from 'express'
import multer from 'multer'
import { uuidv4 } from '@sasjs/utils'
import { getSASSessionController, getJSSessionController } from '.'
import { getSessionController } from '.'
import {
executeProgramRawValidation,
getRunTimeAndFilePath,
@@ -37,17 +37,23 @@ export class FileUploadController {
try {
;({ runTime } = await getRunTimeAndFilePath(programPath))
} catch (err: any) {
res.status(400).send({
return res.status(400).send({
status: 'failure',
message: 'Job execution failed',
error: typeof err === 'object' ? err.toString() : err
})
}
const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
let sessionController
try {
sessionController = getSessionController(runTime)
} catch (err: any) {
return res.status(400).send({
status: 'failure',
message: err.message,
error: typeof err === 'object' ? err.toString() : err
})
}
const session = await sessionController.getSession()
// marking consumed true, so that it's not available

View File

@@ -3,27 +3,58 @@ import { Session } from '../../types'
import { promisify } from 'util'
import { execFile } from 'child_process'
import {
getPackagesFolder,
getSessionsFolder,
generateUniqueFileName,
sysInitCompiledPath
sysInitCompiledPath,
RunTimeType
} from '../../utils'
import {
deleteFolder,
createFile,
fileExists,
generateTimestamp,
readFile
readFile,
isWindows
} from '@sasjs/utils'
const execFilePromise = promisify(execFile)
abstract class SessionController {
export class SessionController {
protected sessions: Session[] = []
protected getReadySessions = (): Session[] =>
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() {
const readySessions = this.getReadySessions()
@@ -62,6 +93,9 @@ export class SASSessionController extends SessionController {
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 clean them up after a predefined period, if unused
this.scheduleSessionDestroy(session)
@@ -71,7 +105,8 @@ export class SASSessionController extends SessionController {
// the autoexec file is executed on SAS startup
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
const contentForAutoExec = `/* compiled systemInit */
const contentForAutoExec = `filename packages "${getPackagesFolder()}";
/* compiled systemInit */
${compiledSystemInitContent}
/* autoexec */
${autoExecContent}`
@@ -88,7 +123,7 @@ ${autoExecContent}`
// Additional windows specific options to avoid the desktop popups.
execFilePromise(process.sasLoc, [
execFilePromise(process.sasLoc!, [
'-SYSIN',
codePath,
'-LOG',
@@ -99,11 +134,14 @@ ${autoExecContent}`
session.path,
'-AUTOEXEC',
autoExecPath,
'-ENCODING',
'UTF-8',
process.platform === 'win32' ? '-nosplash' : '',
process.platform === 'win32' ? '-icon' : '',
process.platform === 'win32' ? '-nologo' : ''
isWindows() ? '-nologo' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
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(() => {
session.completed = true
@@ -137,7 +175,7 @@ ${autoExecContent}`
session.ready = true
}
public async deleteSession(session: Session) {
private async deleteSession(session: Session) {
// remove the temporary files, to avoid buildup
await deleteFolder(session.path)
@@ -162,52 +200,17 @@ ${autoExecContent}`
}
}
export class JSSessionController extends SessionController {
protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
export const getSessionController = (
runTime: RunTimeType
): SessionController => {
if (process.sessionController) return process.sessionController
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()
process.sessionController =
runTime === RunTimeType.SAS
? new SASSessionController()
: new SessionController()
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 getSASSessionController = (): SASSessionController => {
if (process.sasSessionController) return process.sasSessionController
process.sasSessionController = new SASSessionController()
return process.sasSessionController
}
export const getJSSessionController = (): JSSessionController => {
if (process.jsSessionController) return process.jsSessionController
process.jsSessionController = new JSSessionController()
return process.jsSessionController
return process.sessionController
}
const autoExecContent = `

View File

@@ -1,3 +1,4 @@
import { escapeWinSlashes } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadJSCode } from '../../utils'
import { ExecutionVars } from './'
@@ -8,6 +9,7 @@ export const createJSProgram = async (
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
@@ -19,18 +21,15 @@ export const createJSProgram = async (
const preProgramVarStatments = `
let _webout = '';
const weboutPath = '${
process.platform === 'win32'
? weboutPath.replace(/\\/g, '\\\\')
: weboutPath
}';
const _sasjs_tokenfile = '${tokenFile}';
const _sasjs_username = '${preProgramVariables?.username}';
const _sasjs_userid = '${preProgramVariables?.userId}';
const _sasjs_displayname = '${preProgramVariables?.displayName}';
const _metaperson = _sasjs_displayname;
const _metauser = _sasjs_username;
const sasjsprocessmode = 'Stored Program';
const weboutPath = '${escapeWinSlashes(weboutPath)}';
const _SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
const _SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
const _SASJS_USERNAME = '${preProgramVariables?.username}';
const _SASJS_USERID = '${preProgramVariables?.userId}';
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
const _METAPERSON = _SASJS_DISPLAYNAME;
const _METAUSER = _SASJS_USERNAME;
const SASJSPROCESSMODE = 'Stored Program';
`
const requiredModules = `const fs = require('fs')`
@@ -54,14 +53,15 @@ if (_webout) {
`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadJSCode = await generateFileUploadJSCode(
const uploadJsCode = await generateFileUploadJSCode(
otherArgs.filesNamesMap,
session.path
)
//If js code for the file is generated it will be appended to the top of jsCode
if (uploadJSCode.length > 0) {
program = `${uploadJSCode}\n` + program
// If any files are uploaded, the program needs to be updated with some
// dynamically generated variables (pointers) for ease of ingestion
if (uploadJsCode.length > 0) {
program = `${uploadJsCode}\n` + program
}
}
return requiredModules + program

View 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
}

View 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
}

View File

@@ -8,6 +8,7 @@ export const createSASProgram = async (
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
@@ -23,10 +24,14 @@ export const createSASProgram = async (
%let _sasjs_displayname=${preProgramVariables?.displayName};
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
%let _sasjs_apipath=/SASjsApi/stp/execute;
%let _sasjs_webout_headers=${headersPath};
%let _metaperson=&_sasjs_displayname;
%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 sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
%macro _sasjs_server_init();
@@ -34,6 +39,9 @@ export const createSASProgram = async (
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
%mend;
%_sasjs_server_init()
proc printto print="%sysfunc(getoption(log))";
run;
`
program = `
@@ -60,7 +68,8 @@ ${program}`
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) {
program = `${uploadSasCode}` + program
}

View File

@@ -4,4 +4,6 @@ export * from './Execution'
export * from './FileUploadController'
export * from './createSASProgram'
export * from './createJSProgram'
export * from './createPythonProgram'
export * from './createRProgram'
export * from './processProgram'

View File

@@ -5,7 +5,13 @@ import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { RunTimeType } from '../../utils'
import { ExecutionVars, createSASProgram, createJSProgram } from './'
import {
ExecutionVars,
createSASProgram,
createJSProgram,
createPythonProgram,
createRProgram
} from './'
export const processProgram = async (
program: string,
@@ -13,54 +19,20 @@ export const processProgram = async (
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
runTime: RunTimeType,
logPath: string,
otherArgs?: any
) => {
if (runTime === RunTimeType.JS) {
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 {
if (runTime === RunTimeType.SAS) {
program = await createSASProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
@@ -80,6 +52,82 @@ export const processProgram = async (
while (!session.completed) {
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)
}
}
}

View 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
}
}

View 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 })
}

View File

@@ -13,7 +13,8 @@ export class SessionController {
@Example<UserResponse>({
id: 123,
username: 'johnusername',
displayName: 'John'
displayName: 'John',
isAdmin: false
})
@Get('/')
public async session(
@@ -26,5 +27,6 @@ export class SessionController {
const session = (req: express.Request) => ({
id: req.user!.userId,
username: req.user!.username,
displayName: req.user!.displayName
displayName: req.user!.displayName,
isAdmin: req.user!.isAdmin
})

View File

@@ -1,33 +1,16 @@
import express from 'express'
import {
Request,
Security,
Route,
Tags,
Post,
Body,
Get,
Query,
Example
} from 'tsoa'
import {
ExecuteReturnJson,
ExecuteReturnRaw,
ExecutionController,
ExecutionVars
} from './internal'
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
import { ExecutionController, ExecutionVars } from './internal'
import {
getPreProgramVariables,
HTTPHeaders,
isDebugOn,
LogLine,
makeFilesNamesMap,
parseLogToArray,
getRunTimeAndFilePath
} from '../utils'
import { MulterFile } from '../types/Upload'
interface ExecuteReturnJsonPayload {
interface ExecutePostRequestPayload {
/**
* Location of SAS program
* @example "/Public/somefolder/some.file"
@@ -35,102 +18,78 @@ interface ExecuteReturnJsonPayload {
_program?: string
}
interface IRecordOfAny {
[key: string]: any
}
export interface ExecuteReturnJsonResponse {
status: string
_webout: string | IRecordOfAny
log: LogLine[]
message?: string
httpHeaders: HTTPHeaders
}
@Security('bearerAuth')
@Route('SASjsApi/stp')
@Tags('STP')
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:
*
* https://server.sasjs.io/storedprograms
*
* @summary Execute a Stored Program, returns raw _webout content.
* @param _program Location of SAS or JS code
* @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of code in SASjs Drive
* @example _program "/Projects/myApp/some/program"
*/
@Get('/execute')
public async executeReturnRaw(
public async executeGetRequest(
@Request() request: express.Request,
@Query() _program: string
): 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:
*
* 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
* contains a content-type of application/json AND it is valid JSON.
* 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
* @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of code in SASjs Drive
* @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')
public async executeReturnJson(
public async executePostRequest(
@Request() request: express.Request,
@Body() body?: ExecuteReturnJsonPayload,
@Body() body?: ExecutePostRequestPayload,
@Query() _program?: string
): Promise<ExecuteReturnJsonResponse> {
): Promise<string | Buffer> {
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,
_program: string
_program: string,
vars: ExecutionVars,
otherArgs?: any
): Promise<string | Buffer> => {
const query = req.query as ExecutionVars
try {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { result, httpHeaders } =
(await new ExecutionController().executeFile({
const { result, httpHeaders } = await new ExecutionController().executeFile(
{
programPath: codePath,
runTime,
preProgramVariables: getPreProgramVariables(req),
vars: query,
runTime
})) as ExecuteReturnRaw
// 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)
vars,
otherArgs,
session: req.sasjsSession
}
)
if (result instanceof Buffer) {
;(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
}
}
}

View File

@@ -17,16 +17,22 @@ import {
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
import {
getUserAutoExec,
updateUserAutoExec,
ModeType,
AuthProviderType
} from '../utils'
import { GroupResponse } from './group'
export interface UserResponse {
id: number
username: string
displayName: string
isAdmin: boolean
}
interface UserDetailsResponse {
export interface UserDetailsResponse {
id: number
displayName: string
username: string
@@ -48,12 +54,14 @@ export class UserController {
{
id: 123,
username: 'johnusername',
displayName: 'John'
displayName: 'John',
isAdmin: false
},
{
id: 456,
username: 'starkusername',
displayName: 'Stark'
displayName: 'Stark',
isAdmin: true
}
])
@Get('/')
@@ -200,7 +208,7 @@ export class UserController {
const getAllUsers = async (): Promise<UserResponse[]> =>
await User.find({})
.select({ _id: 0, id: 1, username: 1, displayName: 1 })
.select({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 })
.exec()
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
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
const hashPassword = User.hashPassword(password)
@@ -252,7 +264,11 @@ const getUser = async (
'groupId name description -_id'
)) as unknown as UserDetailsResponse
if (!user) throw new Error('User is not found.')
if (!user)
throw {
code: 404,
message: 'User is not found.'
}
return {
id: user.id,
@@ -281,6 +297,24 @@ const updateUser = async (
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) {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
@@ -289,7 +323,10 @@ const updateUser = async (
(findBy.id && usernameExist.id != findBy.id) ||
(findBy.username && usernameExist.username != findBy.username)
)
throw new Error('Username already exists.')
throw {
code: 409,
message: 'Username already exists.'
}
}
params.username = username
}
@@ -302,7 +339,10 @@ const updateUser = async (
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
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 {
id: updatedUser.id,
@@ -329,11 +369,19 @@ const deleteUser = async (
{ password }: { password?: string }
) => {
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) {
const validPass = user.comparePassword(password!)
if (!validPass) throw new Error('Invalid password.')
if (!validPass)
throw {
code: 401,
message: 'Invalid password.'
}
}
await User.deleteOne(findBy)

View File

@@ -5,7 +5,12 @@ import { readFile } from '@sasjs/utils'
import User from '../model/User'
import Client from '../model/Client'
import { getWebBuildFolder, generateAuthCode } from '../utils'
import {
getWebBuildFolder,
generateAuthCode,
AuthProviderType,
LDAPClient
} from '../utils'
import { InfoJWT } from '../types'
import { AuthController } from './auth'
@@ -49,10 +54,10 @@ export class WebController {
}
/**
* @summary Accept a valid username/password
* @summary Destroy the session stored in cookies
*
*/
@Get('/logout')
@Get('/SASLogon/logout')
public async logout(@Request() req: express.Request) {
return new Promise((resolve) => {
req.session.destroy(() => {
@@ -80,8 +85,16 @@ const login = async (
const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.')
const validPass = user.comparePassword(password)
if (!validPass) throw new Error('Invalid password.')
if (
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.user = {
@@ -99,7 +112,8 @@ const login = async (
user: {
id: user.id,
username: user.username,
displayName: user.displayName
displayName: user.displayName,
isAdmin: user.isAdmin
}
}
}

View File

@@ -1,8 +1,16 @@
import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app'
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
import { csrfProtection } from './'
import {
fetchLatestAutoExec,
ModeType,
verifyTokenInDB,
isAuthorizingRoute,
isPublicRoute,
publicUser
} from '../utils'
import { desktopUser } from './desktop'
import { authorize } from './authorize'
export const authenticateAccessToken: RequestHandler = async (
req,
@@ -15,6 +23,10 @@ export const authenticateAccessToken: RequestHandler = async (
return next()
}
const nextFunction = isAuthorizingRoute(req)
? () => authorize(req, res, next)
: next
// if request is coming from web and has valid session
// it can be validated.
if (req.session?.loggedIn) {
@@ -24,33 +36,37 @@ export const authenticateAccessToken: RequestHandler = async (
if (user) {
if (user.isActive) {
req.user = user
return csrfProtection(req, res, next)
return csrfProtection(req, res, nextFunction)
} else return res.sendStatus(401)
}
}
return res.sendStatus(401)
}
authenticateToken(
await authenticateToken(
req,
res,
next,
process.env.ACCESS_TOKEN_SECRET as string,
nextFunction,
process.secrets.ACCESS_TOKEN_SECRET,
'accessToken'
)
}
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
authenticateToken(
export const authenticateRefreshToken: RequestHandler = async (
req,
res,
next
) => {
await authenticateToken(
req,
res,
next,
process.env.REFRESH_TOKEN_SECRET as string,
process.secrets.REFRESH_TOKEN_SECRET,
'refreshToken'
)
}
const authenticateToken = (
const authenticateToken = async (
req: Request,
res: Response,
next: NextFunction,
@@ -58,7 +74,7 @@ const authenticateToken = (
tokenType: 'accessToken' | 'refreshToken'
) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
if (MODE === ModeType.Desktop) {
req.user = {
userId: 1234,
clientId: 'desktopModeClientId',
@@ -73,12 +89,12 @@ const authenticateToken = (
const authHeader = req.headers['authorization']
const token = authHeader?.split(' ')[1]
if (!token) return res.sendStatus(401)
jwt.verify(token, key, async (err: any, data: any) => {
if (err) return res.sendStatus(401)
try {
if (!token) throw 'Unauthorized'
const data: any = jwt.verify(token, key)
// verify this valid token's entry in DB
const user = await verifyTokenInDB(
data?.userId,
data?.clientId,
@@ -91,8 +107,16 @@ const authenticateToken = (
req.user = user
if (tokenType === 'accessToken') req.accessToken = token
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)
}
}

View 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)
}

View 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()
}

View File

@@ -1,4 +1,6 @@
export * from './authenticateToken'
export * from './authorize'
export * from './csrfProtection'
export * from './desktop'
export * from './verifyAdmin'
export * from './verifyAdminIfNeeded'

View File

@@ -1,8 +1,9 @@
import { RequestHandler } from 'express'
import { ModeType } from '../utils'
export const verifyAdmin: RequestHandler = (req, res, next) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server') return next()
if (MODE === ModeType.Desktop) return next()
const { user } = req
if (!user?.isAdmin) return res.status(401).send('Admin account required')

View 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)

View File

@@ -1,7 +1,11 @@
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)
export const PUBLIC_GROUP_NAME = 'Public'
export interface GroupPayload {
/**
* Name of the group
@@ -24,11 +28,13 @@ interface IGroupDocument extends GroupPayload, Document {
groupId: number
isActive: boolean
users: Schema.Types.ObjectId[]
authProvider?: AuthProviderType
}
interface IGroup extends IGroupDocument {
addUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
removeUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
addUser(user: IUser): Promise<GroupDetailsResponse>
removeUser(user: IUser): Promise<GroupDetailsResponse>
hasUser(user: IUser): boolean
}
interface IGroupModel extends Model<IGroup> {}
@@ -42,6 +48,10 @@ const groupSchema = new Schema<IGroupDocument>({
type: String,
default: 'Group description.'
},
authProvider: {
type: String,
enum: AuthProviderType
},
isActive: {
type: Boolean,
default: true
@@ -70,28 +80,31 @@ groupSchema.pre('remove', async function () {
})
// Instance Methods
groupSchema.method(
'addUser',
async function (userObjectId: Schema.Types.ObjectId) {
const userIdIndex = this.users.indexOf(userObjectId)
if (userIdIndex === -1) {
this.users.push(userObjectId)
}
this.markModified('users')
return this.save()
groupSchema.method('addUser', async function (user: IUser) {
const userObjectId = user._id
const userIdIndex = this.users.indexOf(userObjectId)
if (userIdIndex === -1) {
this.users.push(userObjectId)
user.addGroup(this._id)
}
)
groupSchema.method(
'removeUser',
async function (userObjectId: Schema.Types.ObjectId) {
const userIdIndex = this.users.indexOf(userObjectId)
if (userIdIndex > -1) {
this.users.splice(userIdIndex, 1)
}
this.markModified('users')
return this.save()
this.markModified('users')
return this.save()
})
groupSchema.method('removeUser', async function (user: IUser) {
const userObjectId = user._id
const userIdIndex = this.users.indexOf(userObjectId)
if (userIdIndex > -1) {
this.users.splice(userIdIndex, 1)
user.removeGroup(this._id)
}
)
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>(
'Group',

View 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

View File

@@ -1,6 +1,7 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import bcrypt from 'bcryptjs'
import { AuthProviderType } from '../utils'
export interface UserPayload {
/**
@@ -35,15 +36,17 @@ export interface UserPayload {
}
interface IUserDocument extends UserPayload, Document {
_id: Schema.Types.ObjectId
id: number
isAdmin: boolean
isActive: boolean
autoExec: string
groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }]
authProvider?: AuthProviderType
}
interface IUser extends IUserDocument {
export interface IUser extends IUserDocument {
comparePassword(password: string): boolean
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
@@ -66,6 +69,10 @@ const userSchema = new Schema<IUserDocument>({
type: String,
required: true
},
authProvider: {
type: String,
enum: AuthProviderType
},
isAdmin: {
type: Boolean,
default: false

View File

@@ -7,7 +7,7 @@ import {
authenticateRefreshToken
} from '../../middlewares'
import { authorizeValidation, tokenValidation } from '../../utils'
import { tokenValidation } from '../../utils'
import { InfoJWT } from '../../types'
const authRouter = express.Router()

View 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

View File

@@ -11,8 +11,10 @@ import {
extractName,
fileBodyValidation,
fileParamValidation,
folderBodyValidation,
folderParamValidation,
isZipFile
isZipFile,
renameBodyValidation
} from '../../utils'
const controller = new DriveController()
@@ -119,7 +121,11 @@ driveRouter.get('/file', async (req, res) => {
try {
await controller.getFile(req, query._filePath)
} 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)
res.send(response)
} 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)
res.send(response)
} 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)
} catch (err: any) {
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(
'/file',
(...arg) => multerSingle('file', arg),
@@ -200,11 +253,33 @@ driveRouter.patch(
res.send(response)
} catch (err: any) {
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) => {
try {
const response = await controller.getFileTree()

View File

@@ -18,11 +18,7 @@ groupRouter.post(
const response = await controller.createGroup(body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -33,11 +29,7 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllGroups()
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
})
@@ -49,11 +41,7 @@ groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const response = await controller.getGroup(parseInt(groupId))
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
})
@@ -71,11 +59,7 @@ groupRouter.get(
const response = await controller.getGroupByGroupName(name)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -95,11 +79,7 @@ groupRouter.post(
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -119,11 +99,7 @@ groupRouter.delete(
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)
@@ -140,11 +116,7 @@ groupRouter.delete(
await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!')
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
res.status(err.code).send(err.message)
}
}
)

View File

@@ -17,6 +17,8 @@ import groupRouter from './group'
import clientRouter from './client'
import authRouter from './auth'
import sessionRouter from './session'
import permissionRouter from './permission'
import authConfigRouter from './authConfig'
const router = express.Router()
@@ -35,6 +37,20 @@ router.use('/group', desktopRestrict, groupRouter)
router.use('/stp', authenticateAccessToken, stpRouter)
router.use('/code', authenticateAccessToken, codeRouter)
router.use('/user', desktopRestrict, userRouter)
router.use(
'/permission',
desktopRestrict,
authenticateAccessToken,
permissionRouter
)
router.use(
'/authConfig',
desktopRestrict,
authenticateAccessToken,
verifyAdmin,
authConfigRouter
)
router.use(
'/',

View File

@@ -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

View 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

View File

@@ -29,7 +29,13 @@ jest
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import {
UserController,
PermissionController,
PermissionType,
PermissionSettingForRoute,
PrincipalType
} from '../../../controllers/'
import { getTreeExample } from '../../../controllers/internal'
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
const { getFilesFolder } = fileUtilModules
@@ -43,11 +49,18 @@ const user = {
isActive: true
}
const permission = {
type: PermissionType.route,
principalType: PrincipalType.user,
setting: PermissionSettingForRoute.grant
}
describe('drive', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
const controller = new UserController()
const permissionController = new PermissionController()
let accessToken: string
@@ -58,11 +71,32 @@ describe('drive', () => {
con = await mongoose.connect(mongoServer.getUri())
const dbUser = await controller.createUser(user)
accessToken = generateAccessToken({
clientId,
userId: dbUser.id
accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
...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 () => {
@@ -517,29 +551,29 @@ describe('drive', () => {
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)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.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({})
})
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)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.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({})
})
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 filePath = '/my/path/code.sas'
@@ -550,12 +584,96 @@ describe('drive', () => {
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.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({})
})
})
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', () => {
@@ -601,7 +719,7 @@ describe('drive', () => {
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 pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
@@ -616,13 +734,13 @@ describe('drive', () => {
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.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({})
})
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 pathToUpload = '/../path/code.sas'
@@ -631,9 +749,9 @@ describe('drive', () => {
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.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({})
})
@@ -768,19 +886,19 @@ describe('drive', () => {
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)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', `/my/path/code-3.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({})
})
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 pathToUpload = '/../path/code.sas'
@@ -789,9 +907,9 @@ describe('drive', () => {
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.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({})
})
@@ -896,25 +1014,25 @@ describe('drive', () => {
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)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.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({})
})
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)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.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({})
})
@@ -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 =>
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
.members[0] as ServiceMember
const generateAndSaveToken = async (userId: number) => {
const adminAccessToken = generateAccessToken({
clientId,
userId
})
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
return adminAccessToken
}

View File

@@ -4,7 +4,13 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
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 adminUser = {
@@ -27,6 +33,12 @@ const group = {
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 groupController = new GroupController()
@@ -535,6 +547,64 @@ describe('group', () => {
expect(res.text).toEqual('User not found.')
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', () => {
@@ -586,6 +656,46 @@ describe('group', () => {
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 () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')

View 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({})
}

View File

@@ -4,7 +4,13 @@ import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import {
UserController,
PermissionController,
PermissionType,
PermissionSettingForRoute,
PrincipalType
} from '../../../controllers/'
import {
generateAccessToken,
saveTokensInDB,
@@ -15,8 +21,8 @@ import {
} from '../../../utils'
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
import {
SASSessionController,
JSSessionController
SessionController,
SASSessionController
} from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session } from '../../../types'
@@ -33,20 +39,33 @@ const user = {
const sampleSasProgram = '%put hello world!;'
const sampleJsProgram = `console.log('hello world!/')`
const samplePyProgram = `print('hello world!/')`
const filesFolder = getFilesFolder()
const testFilesFolder = `test-stp-${generateTimestamp()}`
let app: Express
let accessToken: string
describe('stp', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let accessToken: string
const userController = new UserController()
const permissionController = new PermissionController()
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
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 () => {
@@ -56,8 +75,6 @@ describe('stp', () => {
})
describe('execute', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
describe('get', () => {
describe('with runtime js', () => {
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 () => {
const programPath = path.join(testFilesFolder, 'program')
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.JS,
expect.anything(),
undefined
await makeRequestAndAssert(
[RunTimeType.JS, RunTimeType.SAS],
200,
RunTimeType.JS
)
})
it('should throw error when js program is not present but sas program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
await createFile(sasProgramPath, sampleSasProgram)
await makeRequestAndAssert([], 400)
})
})
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
describe('with runtime py', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
beforeAll(() => {
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 () => {
const programPath = path.join(testFilesFolder, 'program')
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
)
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
})
it('should throw error when sas program do not exit but js exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
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)
await makeRequestAndAssert([], 400)
})
})
@@ -185,63 +176,51 @@ describe('stp', () => {
})
it('should execute js program when both js and sas program are present', async () => {
const programPath = path.join(testFilesFolder, 'program')
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.JS,
expect.anything(),
undefined
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.JS
)
})
it('should execute sas program when js program is not present but sas program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
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
)
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
})
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)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
describe('with runtime py and sas', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.PY, RunTimeType.SAS]
})
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 () => {
const programPath = path.join(testFilesFolder, 'program')
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
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.SAS
)
})
it('should execute js program when sas program is not present but js program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
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
)
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
})
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)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
describe('with runtime sas and py', () => {
beforeAll(() => {
process.runTimes = [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 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 (
someUser: any
): Promise<string> => {
const userController = new UserController()
const dbUser = await userController.createUser(someUser)
const makeRequestAndAssert = async (
programTypes: RunTimeType[],
expectedStatusCode: number,
expectedRuntime?: RunTimeType
) => {
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) => {
@@ -348,7 +471,7 @@ const setupMocks = async () => {
.mockImplementation(mockedGetSession)
jest
.spyOn(JSSessionController.prototype, 'getSession')
.spyOn(SASSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest

View File

@@ -4,7 +4,12 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
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 adminUser = {
@@ -110,16 +115,16 @@ describe('user', () => {
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)
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.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({})
})
@@ -226,6 +231,36 @@ describe('user', () => {
.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 () => {
const res = await request(app)
.patch('/SASjsApi/user/1234')
@@ -254,7 +289,7 @@ describe('user', () => {
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 dbUser2 = await controller.createUser({
...user,
@@ -265,9 +300,9 @@ describe('user', () => {
.patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.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({})
})
@@ -349,7 +384,7 @@ describe('user', () => {
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 dbUser2 = await controller.createUser({
...user,
@@ -360,9 +395,9 @@ describe('user', () => {
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.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({})
})
})
@@ -446,7 +481,7 @@ describe('user', () => {
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 accessToken = await generateAndSaveToken(dbUser.id)
@@ -454,9 +489,9 @@ describe('user', () => {
.delete(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
.expect(401)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.text).toEqual('Invalid password.')
expect(res.body).toEqual({})
})
@@ -528,7 +563,7 @@ describe('user', () => {
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 accessToken = await generateAndSaveToken(dbUser.id)
@@ -536,9 +571,9 @@ describe('user', () => {
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
.expect(401)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.text).toEqual('Invalid password.')
expect(res.body).toEqual({})
})
})
@@ -652,16 +687,16 @@ describe('user', () => {
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)
const res = await request(app)
.get('/SASjsApi/user/1234')
.auth(adminAccessToken, { type: 'bearer' })
.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({})
})
@@ -731,16 +766,16 @@ describe('user', () => {
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)
const res = await request(app)
.get('/SASjsApi/user/by/username/randomUsername')
.auth(adminAccessToken, { type: 'bearer' })
.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({})
})
})
@@ -770,12 +805,14 @@ describe('user', () => {
{
id: expect.anything(),
username: adminUser.username,
displayName: adminUser.displayName
displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin
},
{
id: expect.anything(),
username: user.username,
displayName: user.displayName
displayName: user.displayName,
isAdmin: user.isAdmin
}
])
})
@@ -796,12 +833,14 @@ describe('user', () => {
{
id: expect.anything(),
username: adminUser.username,
displayName: adminUser.displayName
displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin
},
{
id: expect.anything(),
username: 'randomUser',
displayName: user.displayName
displayName: user.displayName,
isAdmin: user.isAdmin
}
])
})

View File

@@ -39,21 +39,19 @@ describe('web', () => {
describe('home', () => {
it('should respond with CSRF Token', async () => {
await request(app)
.get('/')
.expect(
'set-cookie',
/_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\//
)
const res = await request(app).get('/').expect(200)
expect(res.text).toMatch(
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/
)
})
})
describe('SASLogon/login', () => {
let csrfToken: string
let cookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
;({ csrfToken } = await getCSRF(app))
})
afterEach(async () => {
@@ -67,7 +65,6 @@ describe('web', () => {
const res = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
@@ -79,18 +76,49 @@ describe('web', () => {
expect(res.body.user).toEqual({
id: expect.any(Number),
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', () => {
let csrfToken: string
let cookies: string
let authCookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
;({ csrfToken } = await getCSRF(app))
await userController.createUser(user)
@@ -99,12 +127,7 @@ describe('web', () => {
password: user.password
}
;({ cookies: authCookies } = await performLogin(
app,
credentials,
cookies,
csrfToken
))
;({ authCookies } = await performLogin(app, credentials, csrfToken))
})
afterAll(async () => {
@@ -116,17 +139,28 @@ describe('web', () => {
it('should respond with authorization code', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({ clientId })
expect(res.body).toHaveProperty('code')
})
it('should respond with Bad Request if CSRF Token is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.send({ clientId })
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({})
.expect(400)
@@ -138,7 +172,7 @@ describe('web', () => {
it('should respond with Forbidden if clientId is incorrect', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({
clientId: 'WrongClientID'
@@ -153,30 +187,25 @@ describe('web', () => {
const getCSRF = async (app: Express) => {
// make request to get CSRF
const { header } = await request(app).get('/')
const cookies = header['set-cookie'].join()
const { text } = await request(app).get('/')
const csrfToken = extractCSRF(cookies)
return { csrfToken, cookies }
return { csrfToken: extractCSRF(text) }
}
const performLogin = async (
app: Express,
credentials: { username: string; password: string },
cookies: string,
csrfToken: string
) => {
const { header } = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send(credentials)
const newCookies: string = header['set-cookie'].join()
return { cookies: newCookies }
return { authCookies: header['set-cookie'].join() }
}
const extractCSRF = (cookies: string) =>
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
cookies
)![2]
const extractCSRF = (text: string) =>
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
text
)![1]

View File

@@ -13,7 +13,7 @@ stpRouter.get('/execute', async (req, res) => {
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.executeReturnRaw(req, query._program)
const response = await controller.executeGetRequest(req, query._program)
if (response instanceof Buffer) {
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)
try {
const response = await controller.executeReturnJson(
const response = await controller.executePostRequest(
req,
req.body,
req.query?._program as string

View File

@@ -23,7 +23,7 @@ userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
const response = await controller.createUser(body)
res.send(response)
} 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()
res.send(response)
} 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)
res.send(response)
} 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))
res.send(response)
} 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)
res.send(response)
} 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)
res.send(response)
} 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)
res.status(200).send('Account Deleted!')
} 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)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
res.status(err.code).send(err.message)
}
}
)

View File

@@ -1,5 +1,6 @@
import path from 'path'
import express, { Request } from 'express'
import { authenticateAccessToken, generateCSRFToken } from '../../middlewares'
import { folderExists } from '@sasjs/utils'
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
@@ -9,10 +10,10 @@ const appStreams: { [key: string]: string } = {}
const router = express.Router()
router.get('/', async (req, res) => {
router.get('/', authenticateAccessToken, async (req, res) => {
const content = appStreamHtml(process.appStreamConfig)
res.cookie('XSRF-TOKEN', req.csrfToken())
res.cookie('XSRF-TOKEN', generateCSRFToken())
return res.send(content)
})
@@ -66,7 +67,7 @@ export const publishAppStream = async (
return {}
}
router.get(`/*`, function (req: Request, res, next) {
router.get(`/*`, authenticateAccessToken, function (req: Request, res, next) {
const reqPath = req.path.replace(/^\//, '')
// Redirecting to url with trailing slash for appStream base URL only

View File

@@ -26,6 +26,7 @@ export const style = `<style>
}
.app-container .app img{
width: 100%;
height: calc(100% - 30px);
margin-bottom: 10px;
border-radius: 10px;
}

View File

@@ -4,7 +4,7 @@ import webRouter from './web'
import apiRouter from './api'
import appStreamRouter from './appStream'
import { csrfProtection } from '../app'
import { csrfProtection } from '../middlewares'
export const setupRoutes = (app: Express) => {
app.use('/SASjsApi', apiRouter)

View File

@@ -1,8 +1,25 @@
import express from 'express'
import sas9WebRouter from './sas9-web'
import sasViyaWebRouter from './sasviya-web'
import webRouter from './web'
import { MOCK_SERVERTYPEType } from '../../utils'
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

View 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

View 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

View File

@@ -1,4 +1,5 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers/web'
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
import { authorizeValidation, loginWebValidation } from '../../utils'
@@ -11,11 +12,15 @@ webRouter.get('/', async (req, res) => {
try {
response = await controller.home()
} catch (_) {
response = 'Web Build is not present'
response = '<html><head></head><body>Web Build is not present</body></html>'
} 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)
}
})
@@ -48,7 +53,7 @@ webRouter.post(
}
)
webRouter.get('/logout', desktopRestrict, async (req, res) => {
webRouter.get('/SASLogon/logout', desktopRestrict, async (req, res) => {
try {
await controller.logout(req)
res.status(200).send('OK!')

View File

@@ -16,9 +16,9 @@ appPromise.then(async (app) => {
)
})
} else {
const { key, cert } = await getCertificates()
const { key, cert, ca } = await getCertificates()
const httpsServer = createServer({ key, cert }, app)
const httpsServer = createServer({ key, cert, ca }, app)
httpsServer.listen(sasJsPort, () => {
console.log(
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`

View File

@@ -2,5 +2,6 @@ export interface TreeNode {
name: string
relativePath: string
absolutePath: string
isFolder: boolean
children: Array<TreeNode>
}

View File

@@ -1,12 +1,16 @@
declare namespace NodeJS {
export interface Process {
sasLoc: string
nodeLoc: string
sasLoc?: string
nodeLoc?: string
pythonLoc?: string
rLoc?: string
driveLoc: string
sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController
logsLoc: string
logsUUID: string
sessionController?: import('../../controllers/internal').SessionController
appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[]
secrets: import('../../model/Configuration').ConfigurationType
}
}

View File

@@ -5,6 +5,8 @@ import { AppStreamConfig } from '../types'
import { getAppStreamConfigPath } from './file'
export const loadAppStreamConfig = async () => {
process.appStreamConfig = {}
if (process.env.NODE_ENV === 'test') return
const appStreamConfigPath = getAppStreamConfigPath()
@@ -21,7 +23,6 @@ export const loadAppStreamConfig = async () => {
} catch (_) {
appStreamConfig = {}
}
process.appStreamConfig = {}
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
const { appLoc, streamWebFolder, streamLogo } = entry

View File

@@ -9,7 +9,5 @@ export const connectDB = async () => {
}
console.log('Connected to DB!')
await seedDB()
return mongoose.connection
return seedDB()
}

View File

@@ -10,7 +10,7 @@ export const sysInitCompiledPath = path.join(
'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 getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
@@ -22,18 +22,21 @@ export const getDesktopUserAutoExecPath = () =>
export const getSasjsRootFolder = () => process.driveLoc
export const getLogFolder = () => process.logsLoc
export const getAppStreamConfigPath = () =>
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
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 getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
export const getSessionsFolder = () =>

View File

@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
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'
})

View File

@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
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'
})

View File

@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
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'
})

View 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))

View File

@@ -2,22 +2,32 @@ import path from 'path'
import { fileExists, getString, readFile } from '@sasjs/utils'
export const getCertificates = async () => {
const { PRIVATE_KEY, FULL_CHAIN } = process.env
const { PRIVATE_KEY, CERT_CHAIN, CA_ROOT } = process.env
let ca
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
const certPath = FULL_CHAIN ?? (await getFileInput('Full Chain (PEM)'))
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
const caPath = CA_ROOT
console.log('keyPath: ', keyPath)
console.log('certPath: ', certPath)
if (caPath) console.log('caPath: ', caPath)
const key = await readFile(keyPath)
const cert = await readFile(certPath)
if (caPath) ca = await readFile(caPath)
return { key, cert }
return { key, cert, ca }
}
const getFileInput = async (filename: string): Promise<string> => {
const getFileInput = async (
filename: string,
required: boolean = true
): Promise<string> => {
const validator = async (filePath: string) => {
if (!required) return true
if (!filePath) return `Path to ${filename} is required.`
if (!(await fileExists(path.join(process.cwd(), filePath)))) {

View File

@@ -1,17 +1,30 @@
import path from 'path'
import { getString } from '@sasjs/utils/input'
import { createFolder, fileExists, folderExists } from '@sasjs/utils'
const isWindows = () => process.platform === 'win32'
import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
import { RunTimeType } from './verifyEnvVariables'
export const getDesktopFields = async () => {
const { SAS_PATH, NODE_PATH } = process.env
const { SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH } = process.env
const sasLoc = SAS_PATH ?? (await getSASLocation())
const nodeLoc = NODE_PATH ?? (await getNodeLocation())
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
let sasLoc, nodeLoc, pythonLoc, rLoc
return { sasLoc, nodeLoc }
if (process.runTimes.includes(RunTimeType.SAS)) {
sasLoc = SAS_PATH ?? (await getSASLocation())
}
if (process.runTimes.includes(RunTimeType.JS)) {
nodeLoc = NODE_PATH ?? (await getNodeLocation())
}
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> => {
@@ -55,7 +68,7 @@ const getSASLocation = async (): Promise<string> => {
: '/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas'
const targetName = await getString(
'Please enter path to SAS executable (absolute path): ',
'Please enter full path to a SAS executable with UTF-8 encoding: ',
validator,
defaultLocation
)
@@ -75,11 +88,55 @@ const getNodeLocation = async (): Promise<string> => {
}
const defaultLocation = isWindows()
? 'C:\\Program Files\\nodejs\\'
: '/usr/local/nodejs/bin'
? 'C:\\Program Files\\nodejs\\node.exe'
: '/usr/local/nodejs/bin/node.sh'
const targetName = await getString(
'Please enter path to nodejs executable (absolute path): ',
'Please enter full path to a NodeJS executable: ',
validator,
defaultLocation
)
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
)

View File

@@ -7,7 +7,6 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
const { user, accessToken } = req
const csrfToken = req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN']
const sessionId = req.cookies['connect.sid']
const { _csrf } = req.cookies
const httpHeaders: string[] = []
@@ -16,7 +15,6 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
const cookies: string[] = []
if (sessionId) cookies.push(`connect.sid=${sessionId}`)
if (_csrf) cookies.push(`_csrf=${_csrf}`)
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)

View File

@@ -4,15 +4,11 @@ import { getFilesFolder } from './file'
import { RunTimeType } from '.'
export const getRunTimeAndFilePath = async (programPath: string) => {
const ext = path.extname(programPath)
// if program path is provided with extension we should split that into code path and ext as run time
if (ext) {
const ext = path.extname(programPath).toLowerCase()
// If programPath (_program) is provided with a ".sas", ".js", ".py" or ".r" extension
// we should use that extension to determine the appropriate runTime
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
const runTime = ext.slice(1)
const runTimeTypes = Object.values(RunTimeType)
if (!runTimeTypes.includes(runTime as RunTimeType)) {
throw `The '${runTime}' runtime is not supported.`
}
const codePath = path
.join(getFilesFolder(), programPath)

View 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 }
}
}
}

View File

@@ -8,13 +8,17 @@ export * from './file'
export * from './generateAccessToken'
export * from './generateAuthCode'
export * from './generateRefreshToken'
export * from './getAuthorizedRoutes'
export * from './getCertificates'
export * from './getDesktopFields'
export * from './getPreProgramVariables'
export * from './getRunTimeAndFilePath'
export * from './getServerUrl'
export * from './getTokensFromDB'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './isPublicRoute'
export * from './ldapClient'
export * from './zipped'
export * from './parseLogToArray'
export * from './removeTokensInDB'

View 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
View 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.')
})
}
}

View File

@@ -1,5 +1,88 @@
import Client from '../model/Client'
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
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 = {
clientId: 'clientID1',
@@ -13,23 +96,3 @@ const ADMIN_USER = {
isAdmin: 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}`)
}
}

View File

@@ -1,24 +1,42 @@
import path from 'path'
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
import { getDesktopFields, ModeType, RunTimeType } from '.'
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
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') {
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
return
}
const { MODE } = process.env
process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? []
if (MODE === ModeType.Server) {
process.sasLoc = process.env.SAS_PATH as string
process.nodeLoc = process.env.NODE_PATH as string
process.sasLoc = process.env.SAS_PATH
process.nodeLoc = process.env.NODE_PATH
process.pythonLoc = process.env.PYTHON_PATH
process.rLoc = process.env.R_PATH
} else {
const { sasLoc, nodeLoc } = await getDesktopFields()
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
process.sasLoc = sasLoc
process.nodeLoc = nodeLoc
process.pythonLoc = pythonLoc
process.rLoc = rLoc
}
const { SASJS_ROOT } = process.env
@@ -26,10 +44,18 @@ export const setProcessVariables = async () => {
await createFolder(absPath)
process.driveLoc = getRealPath(absPath)
const { RUN_TIMES } = process.env
process.runTimes = (RUN_TIMES as string).split(',') as RunTimeType[]
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('sasDrive: ', process.driveLoc)
console.log('sasLogs: ', process.logsLoc)
console.log('runTimes: ', process.runTimes)
}

View File

@@ -1,10 +1,15 @@
import { createFile, createFolder, fileExists } from '@sasjs/utils'
import { getDesktopUserAutoExecPath, getFilesFolder } from './file'
import {
getDesktopUserAutoExecPath,
getFilesFolder,
getPackagesFolder
} from './file'
import { ModeType } from './verifyEnvVariables'
export const setupFolders = async () => {
const drivePath = getFilesFolder()
await createFolder(drivePath)
await createFolder(getPackagesFolder())
if (process.env.MODE === ModeType.Desktop) {
if (!(await fileExists(getDesktopUserAutoExecPath()))) {

View File

@@ -1,4 +1,4 @@
import { extractHeaders } from '..'
import { extractHeaders } from '../extractHeaders'
describe('extractHeaders', () => {
it('should return valid http headers', () => {

View File

@@ -1,4 +1,4 @@
import { parseLogToArray } from '..'
import { parseLogToArray } from '../parseLogToArray'
describe('parseLogToArray', () => {
it('should parse log to array type', () => {

Some files were not shown because too many files have changed in this diff Show More