mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 11:24:35 +00:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1a182698e | ||
|
|
4be692b24b | ||
|
|
d2ddd8aaca | ||
|
|
3a45e8f525 | ||
|
|
c0e2f55a7b | ||
|
|
aa027414ed | ||
|
|
8c4c52b1a9 | ||
|
|
ff420434ae | ||
|
|
65e6de9663 | ||
|
|
2e53d43e11 | ||
|
|
3795f748a7 | ||
|
|
e024a92f16 | ||
|
|
92fda183f3 | ||
|
|
6f2e6efd03 | ||
|
|
3b4e9d20d4 | ||
|
|
4a67d0c63a | ||
|
|
dea204e3c5 | ||
|
|
5f9e83759c | ||
|
|
fefe63deb1 | ||
| ddd179bbee | |||
| a10b87930c | |||
| 496247d0b9 | |||
| eeb63b330c | |||
|
|
1108d3dd7b | ||
|
|
7edb47a4cb | ||
|
|
451cb4f6dd | ||
|
|
0b759a5594 | ||
|
|
5338ffb211 | ||
| e42fdd3575 | |||
| b10e932605 | |||
| e54a09db19 | |||
| 4c35e04802 | |||
| b5f595a25c | |||
|
|
a131adbae7 | ||
|
|
a20c3b9719 | ||
|
|
eee3a7b084 | ||
|
|
9c3da56901 | ||
|
|
7e6524d7e4 | ||
|
|
0ea2690616 | ||
|
|
b369759f0f | ||
|
|
ac9a835c5a | ||
|
|
e290751c87 | ||
| e516b7716d | |||
| f3dfc7083f | |||
| 7d916ec3e9 | |||
| 70f279a49c | |||
| 66a3537271 | |||
| ca64c13909 | |||
| 0a73a35547 | |||
| a75edbaa32 | |||
| 4ddfec0403 | |||
| 35439d7d51 | |||
| 907aa485fd | |||
| 888627e1c8 | |||
| 9cb9e2dd33 | |||
| 54d4bf835d | |||
| 67fe298fd5 | |||
| 97ecfdc955 | |||
| 5b319f9ad1 | |||
| be8635ccc5 | |||
| f863b81a7d | |||
| bdf63df1d9 | |||
| 4c6b9c5e93 | |||
|
|
a2d1396057 | ||
|
|
b2f21eb3ac | ||
|
|
71bcbb9134 | ||
|
|
c86f0feff8 | ||
|
|
d3d2ab9a36 | ||
| 5cc85b57f8 | |||
|
|
ae0fc0c48c | ||
|
|
555c5d54e2 | ||
| 1b5859ee37 | |||
| 65380be2f3 | |||
|
|
1933be15c2 | ||
|
|
56b20beb8c | ||
|
|
bfc5ac6a4f | ||
|
|
6376173de0 | ||
|
|
3130fbeff0 | ||
|
|
01e9a1d9e9 | ||
|
|
2119e9de9a | ||
|
|
87dbab98f6 | ||
|
|
1bf122a0a2 | ||
|
|
5d5d6ce326 | ||
|
|
620eddb713 | ||
|
|
3c92034da3 | ||
|
|
f6dc74f16b | ||
|
|
8c48d00d21 | ||
|
|
48ff8d73d4 | ||
| eb397b15c2 | |||
| eb569c7b82 | |||
| 99a1107364 | |||
| 91d29cb127 | |||
| fa63dc071b | |||
| e8c21a43b2 | |||
| 1413b18508 | |||
| dfbd155711 | |||
| 4fcc191ce9 | |||
| d000f7508f | |||
| 5652325452 | |||
| 0781ddd64e | |||
| 7be77cc38a | |||
| 98b8a75148 | |||
| 72a3197a06 | |||
| fce05d6959 | |||
| 1aec3abd28 | |||
| 9136c95013 | |||
|
|
89b32e70ff | ||
| 01713440a4 | |||
| 540f54fb77 | |||
| bf906aa544 | |||
| 797c2bcc39 | |||
| 1103ffe07b | |||
| e5200c1000 | |||
| 38a7db8514 | |||
| 39fc908de1 | |||
| be009d5b02 | |||
| 6bea1f7666 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [sasjs]
|
||||
125
CHANGELOG.md
125
CHANGELOG.md
@@ -1,3 +1,128 @@
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default runtime should be sas ([91d29cb](https://github.com/sasjs/server/commit/91d29cb1272c28afbceaf39d1e0a87e17fbfdcd6))
|
||||
* **Studio:** default selection of runtime fixed ([eb569c7](https://github.com/sasjs/server/commit/eb569c7b827c872ed2c4bc114559b97d87fd2aa0))
|
||||
* webout path fixed in code.js when running on windows ([99a1107](https://github.com/sasjs/server/commit/99a110736448f66f99a512396b268fc31a3feef0))
|
||||
|
||||
# [0.7.0](https://github.com/sasjs/server/compare/v0.6.1...v0.7.0) (2022-06-19)
|
||||
|
||||
|
||||
|
||||
19
PULL_REQUEST_TEMPLATE.md
Normal file
19
PULL_REQUEST_TEMPLATE.md
Normal 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.
|
||||
18
README.md
18
README.md
@@ -99,15 +99,12 @@ 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>
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||
@@ -139,11 +136,14 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json
|
||||
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
||||
LOG_FORMAT_MORGAN=
|
||||
|
||||
# This location is for server logs with classical UNIX logrotate behavior
|
||||
LOG_LOCATION=./sasjs_root/logs
|
||||
|
||||
# A comma separated string that defines the available runTimes.
|
||||
# Priority is given to the runtime that cSAS_PATHomes first in string.
|
||||
# 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,js
|
||||
# options: [sas,js|js,sas|sas|js] default:sas
|
||||
RUN_TIMES=
|
||||
|
||||
```
|
||||
|
||||
@@ -4,23 +4,21 @@ 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,js
|
||||
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
|
||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||
|
||||
SASJS_ROOT=./sasjs_root
|
||||
|
||||
LOG_FORMAT_MORGAN=common
|
||||
LOG_FORMAT_MORGAN=common
|
||||
LOG_LOCATION=./sasjs_root/logs
|
||||
1266
api/package-lock.json
generated
1266
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
|
||||
"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",
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "^4.27.3",
|
||||
"@sasjs/core": "^4.31.3",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
@@ -63,6 +63,7 @@
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.3",
|
||||
"rotating-file-stream": "^3.0.4",
|
||||
"swagger-ui-express": "4.3.0",
|
||||
"unzipper": "^0.10.11",
|
||||
"url": "^0.10.3"
|
||||
|
||||
@@ -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:
|
||||
@@ -144,7 +109,7 @@ components:
|
||||
- sas
|
||||
- js
|
||||
type: string
|
||||
ExecuteSASCodePayload:
|
||||
ExecuteCodePayload:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
@@ -314,10 +279,13 @@ components:
|
||||
type: string
|
||||
displayName:
|
||||
type: string
|
||||
isAdmin:
|
||||
type: boolean
|
||||
required:
|
||||
- id
|
||||
- username
|
||||
- displayName
|
||||
- isAdmin
|
||||
type: object
|
||||
additionalProperties: false
|
||||
GroupResponse:
|
||||
@@ -440,13 +408,13 @@ components:
|
||||
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.'
|
||||
id:
|
||||
description: 'The string version of this documents _id.'
|
||||
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_:
|
||||
@@ -480,6 +448,16 @@ components:
|
||||
- runTimes
|
||||
type: object
|
||||
additionalProperties: false
|
||||
AuthorizedRoutesResponse:
|
||||
properties:
|
||||
URIs:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- URIs
|
||||
type: object
|
||||
additionalProperties: false
|
||||
ExecuteReturnJsonPayload:
|
||||
properties:
|
||||
_program:
|
||||
@@ -488,6 +466,106 @@ 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
|
||||
PermissionDetailsResponse:
|
||||
properties:
|
||||
permissionId:
|
||||
type: number
|
||||
format: double
|
||||
uri:
|
||||
type: string
|
||||
setting:
|
||||
type: string
|
||||
user:
|
||||
$ref: '#/components/schemas/UserResponse'
|
||||
group:
|
||||
$ref: '#/components/schemas/GroupDetailsResponse'
|
||||
required:
|
||||
- permissionId
|
||||
- uri
|
||||
- setting
|
||||
type: object
|
||||
additionalProperties: false
|
||||
PermissionSetting:
|
||||
enum:
|
||||
- Grant
|
||||
- Deny
|
||||
type: string
|
||||
PrincipalType:
|
||||
enum:
|
||||
- user
|
||||
- group
|
||||
type: string
|
||||
RegisterPermissionPayload:
|
||||
properties:
|
||||
uri:
|
||||
type: string
|
||||
description: 'Name of affected resource'
|
||||
example: /SASjsApi/code/execute
|
||||
setting:
|
||||
$ref: '#/components/schemas/PermissionSetting'
|
||||
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:
|
||||
- uri
|
||||
- setting
|
||||
- principalType
|
||||
- principalId
|
||||
type: object
|
||||
additionalProperties: false
|
||||
UpdatePermissionPayload:
|
||||
properties:
|
||||
setting:
|
||||
$ref: '#/components/schemas/PermissionSetting'
|
||||
description: 'The indication of whether (and to what extent) access is provided'
|
||||
example: Grant
|
||||
required:
|
||||
- setting
|
||||
type: object
|
||||
additionalProperties: false
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
@@ -558,86 +636,6 @@ paths:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters: []
|
||||
/:
|
||||
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: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [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'
|
||||
/logout:
|
||||
get:
|
||||
operationId: Logout
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
summary: 'Accept a valid username/password'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
parameters: []
|
||||
/SASjsApi/client:
|
||||
post:
|
||||
operationId: CreateClient
|
||||
@@ -666,7 +664,7 @@ paths:
|
||||
$ref: '#/components/schemas/ClientPayload'
|
||||
/SASjsApi/code/execute:
|
||||
post:
|
||||
operationId: ExecuteSASCode
|
||||
operationId: ExecuteCode
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
@@ -687,7 +685,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExecuteSASCodePayload'
|
||||
$ref: '#/components/schemas/ExecuteCodePayload'
|
||||
/SASjsApi/drive/deploy:
|
||||
post:
|
||||
operationId: Deploy
|
||||
@@ -993,7 +991,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
|
||||
@@ -1422,6 +1420,24 @@ 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: {URIs: [/AppStream, /SASjsApi/stp/execute]}
|
||||
summary: 'Get authorized routes.'
|
||||
tags:
|
||||
- Info
|
||||
security: []
|
||||
parameters: []
|
||||
/SASjsApi/session:
|
||||
get:
|
||||
operationId: Session
|
||||
@@ -1434,7 +1450,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
|
||||
@@ -1504,19 +1520,205 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
||||
/:
|
||||
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: []
|
||||
/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, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}, {permissionId: 124, uri: /SASjsApi/code/execute, setting: Grant, group: {groupId: 1, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}}]
|
||||
summary: 'Get list of all permissions (uri, setting and userDetail).'
|
||||
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, uri: /SASjsApi/code/execute, 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, uri: /SASjsApi/code/execute, 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
|
||||
servers:
|
||||
-
|
||||
url: /
|
||||
tags:
|
||||
-
|
||||
name: Info
|
||||
description: 'Get Server Info'
|
||||
description: 'Get Server Information'
|
||||
-
|
||||
name: Session
|
||||
description: 'Get Session information'
|
||||
-
|
||||
name: User
|
||||
description: 'Operations about users'
|
||||
description: 'Operations with users'
|
||||
-
|
||||
name: Permission
|
||||
description: 'Operations about permissions'
|
||||
-
|
||||
name: Client
|
||||
description: 'Operations about clients'
|
||||
@@ -1525,16 +1727,16 @@ tags:
|
||||
description: 'Operations about auth'
|
||||
-
|
||||
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: STP
|
||||
description: 'Operations about STP'
|
||||
description: 'Execution of Stored Programs'
|
||||
-
|
||||
name: CODE
|
||||
description: 'Operations on SAS code'
|
||||
description: 'Execution of code (various runtimes are supported)'
|
||||
-
|
||||
name: Web
|
||||
description: 'Operations on Web'
|
||||
|
||||
@@ -6,7 +6,7 @@ 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')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
21
api/src/app-modules/configureCors.ts
Normal file
21
api/src/app-modules/configureCors.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Express } from 'express'
|
||||
import cors from 'cors'
|
||||
import { CorsType } from '../utils'
|
||||
|
||||
export const configureCors = (app: Express) => {
|
||||
const { CORS, WHITELIST } = process.env
|
||||
|
||||
if (CORS === CorsType.ENABLED) {
|
||||
const whiteList: string[] = []
|
||||
WHITELIST?.split(' ')
|
||||
?.filter((url) => !!url)
|
||||
.forEach((url) => {
|
||||
if (url.startsWith('http'))
|
||||
// removing trailing slash of URLs listing for CORS
|
||||
whiteList.push(url.replace(/\/$/, ''))
|
||||
})
|
||||
|
||||
console.log('All CORS Requests are enabled for:', whiteList)
|
||||
app.use(cors({ credentials: true, origin: whiteList }))
|
||||
}
|
||||
}
|
||||
32
api/src/app-modules/configureExpressSession.ts
Normal file
32
api/src/app-modules/configureExpressSession.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Express } from 'express'
|
||||
import mongoose from 'mongoose'
|
||||
import session from 'express-session'
|
||||
import MongoStore from 'connect-mongo'
|
||||
|
||||
import { ModeType } from '../utils'
|
||||
import { cookieOptions } from '../app'
|
||||
|
||||
export const configureExpressSession = (app: Express) => {
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE === ModeType.Server) {
|
||||
let store: MongoStore | undefined
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
store = MongoStore.create({
|
||||
client: mongoose.connection!.getClient() as any,
|
||||
collectionName: 'sessions'
|
||||
})
|
||||
}
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: process.secrets.SESSION_SECRET,
|
||||
saveUninitialized: false, // don't create session until something stored
|
||||
resave: false, //don't save session if unmodified
|
||||
store,
|
||||
cookie: cookieOptions
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
33
api/src/app-modules/configureLogger.ts
Normal file
33
api/src/app-modules/configureLogger.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import path from 'path'
|
||||
import { Express } from 'express'
|
||||
import morgan from 'morgan'
|
||||
import { createStream } from 'rotating-file-stream'
|
||||
import { generateTimestamp } from '@sasjs/utils'
|
||||
import { getLogFolder } from '../utils'
|
||||
|
||||
export const configureLogger = (app: Express) => {
|
||||
const { LOG_FORMAT_MORGAN } = process.env
|
||||
|
||||
let options
|
||||
if (
|
||||
process.env.NODE_ENV !== 'development' &&
|
||||
process.env.NODE_ENV !== 'test'
|
||||
) {
|
||||
const timestamp = generateTimestamp()
|
||||
const filename = `${timestamp}.log`
|
||||
const logsFolder = getLogFolder()
|
||||
|
||||
// create a rotating write stream
|
||||
var accessLogStream = createStream(filename, {
|
||||
interval: '1d', // rotate daily
|
||||
path: logsFolder
|
||||
})
|
||||
|
||||
console.log('Writing Logs to :', path.join(logsFolder, filename))
|
||||
|
||||
options = { stream: accessLogStream }
|
||||
}
|
||||
|
||||
// setup the logger
|
||||
app.use(morgan(LOG_FORMAT_MORGAN as string, options))
|
||||
}
|
||||
26
api/src/app-modules/configureSecurity.ts
Normal file
26
api/src/app-modules/configureSecurity.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Express } from 'express'
|
||||
import { getEnvCSPDirectives } from '../utils/parseHelmetConfig'
|
||||
import { HelmetCoepType, ProtocolType } from '../utils'
|
||||
import helmet from 'helmet'
|
||||
|
||||
export const configureSecurity = (app: Express) => {
|
||||
const { PROTOCOL, HELMET_CSP_CONFIG_PATH, HELMET_COEP } = process.env
|
||||
|
||||
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
||||
HELMET_CSP_CONFIG_PATH
|
||||
)
|
||||
if (PROTOCOL === ProtocolType.HTTP)
|
||||
cspConfigJson['upgrade-insecure-requests'] = null
|
||||
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
||||
...cspConfigJson
|
||||
}
|
||||
},
|
||||
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
||||
})
|
||||
)
|
||||
}
|
||||
4
api/src/app-modules/index.ts
Normal file
4
api/src/app-modules/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './configureCors'
|
||||
export * from './configureExpressSession'
|
||||
export * from './configureLogger'
|
||||
export * from './configureSecurity'
|
||||
122
api/src/app.ts
122
api/src/app.ts
@@ -1,30 +1,26 @@
|
||||
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 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,19 +30,7 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(cookieParser())
|
||||
|
||||
const {
|
||||
MODE,
|
||||
CORS,
|
||||
WHITELIST,
|
||||
PROTOCOL,
|
||||
HELMET_CSP_CONFIG_PATH,
|
||||
HELMET_COEP,
|
||||
LOG_FORMAT_MORGAN
|
||||
} = process.env
|
||||
|
||||
app.use(morgan(LOG_FORMAT_MORGAN as string))
|
||||
const { PROTOCOL } = process.env
|
||||
|
||||
export const cookieOptions = {
|
||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||
@@ -54,79 +38,11 @@ export const cookieOptions = {
|
||||
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!')
|
||||
@@ -136,6 +52,30 @@ const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
}
|
||||
|
||||
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')))
|
||||
|
||||
await setupFolders()
|
||||
await copySASjsCore()
|
||||
|
||||
|
||||
@@ -129,8 +129,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 = {
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface GroupResponse {
|
||||
description: string
|
||||
}
|
||||
|
||||
interface GroupDetailsResponse {
|
||||
export interface GroupDetailsResponse {
|
||||
groupId: number
|
||||
name: string
|
||||
description: string
|
||||
@@ -198,7 +198,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 {
|
||||
@@ -249,9 +249,10 @@ 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
|
||||
const updatedGroup =
|
||||
action === 'addUser'
|
||||
? await group.addUser(user)
|
||||
: await group.removeUser(user)
|
||||
|
||||
if (!updatedGroup)
|
||||
throw {
|
||||
@@ -260,9 +261,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,
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './code'
|
||||
export * from './drive'
|
||||
export * from './group'
|
||||
export * from './info'
|
||||
export * from './permission'
|
||||
export * from './session'
|
||||
export * from './stp'
|
||||
export * from './user'
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Route, Tags, Example, Get } from 'tsoa'
|
||||
import { getAuthorizedRoutes } from '../utils'
|
||||
export interface AuthorizedRoutesResponse {
|
||||
URIs: string[]
|
||||
}
|
||||
|
||||
export interface InfoResponse {
|
||||
mode: string
|
||||
@@ -36,4 +40,19 @@ export class InfoController {
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get authorized routes.
|
||||
*
|
||||
*/
|
||||
@Example<AuthorizedRoutesResponse>({
|
||||
URIs: ['/AppStream', '/SASjsApi/stp/execute']
|
||||
})
|
||||
@Get('/authorizedRoutes')
|
||||
public authorizedRoutes(): AuthorizedRoutesResponse {
|
||||
const response = {
|
||||
URIs: getAuthorizedRoutes()
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -76,10 +72,7 @@ export class ExecutionController {
|
||||
session: sessionByFileUpload,
|
||||
runTime
|
||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||
const sessionController =
|
||||
runTime === RunTimeType.SAS
|
||||
? getSASSessionController()
|
||||
: getJSSessionController()
|
||||
const sessionController = getSessionController(runTime)
|
||||
|
||||
const session =
|
||||
sessionByFileUpload ?? (await sessionController.getSession())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,14 +5,16 @@ import { execFile } from 'child_process'
|
||||
import {
|
||||
getSessionsFolder,
|
||||
generateUniqueFileName,
|
||||
sysInitCompiledPath
|
||||
sysInitCompiledPath,
|
||||
RunTimeType
|
||||
} from '../../utils'
|
||||
import {
|
||||
deleteFolder,
|
||||
createFile,
|
||||
fileExists,
|
||||
generateTimestamp,
|
||||
readFile
|
||||
readFile,
|
||||
isWindows
|
||||
} from '@sasjs/utils'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
@@ -88,7 +90,7 @@ ${autoExecContent}`
|
||||
|
||||
// Additional windows specific options to avoid the desktop popups.
|
||||
|
||||
execFilePromise(process.sasLoc, [
|
||||
execFilePromise(process.sasLoc!, [
|
||||
'-SYSIN',
|
||||
codePath,
|
||||
'-LOG',
|
||||
@@ -99,11 +101,9 @@ ${autoExecContent}`
|
||||
session.path,
|
||||
'-AUTOEXEC',
|
||||
autoExecPath,
|
||||
'-ENCODING',
|
||||
'UTF-8',
|
||||
process.platform === 'win32' ? '-nosplash' : '',
|
||||
process.platform === 'win32' ? '-icon' : '',
|
||||
process.platform === 'win32' ? '-nologo' : ''
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||
isWindows() ? '-nologo' : ''
|
||||
])
|
||||
.then(() => {
|
||||
session.completed = true
|
||||
@@ -194,7 +194,21 @@ export class JSSessionController extends SessionController {
|
||||
}
|
||||
}
|
||||
|
||||
export const getSASSessionController = (): SASSessionController => {
|
||||
export const getSessionController = (
|
||||
runTime: RunTimeType
|
||||
): SASSessionController | JSSessionController => {
|
||||
if (runTime === RunTimeType.SAS) {
|
||||
return getSASSessionController()
|
||||
}
|
||||
|
||||
if (runTime === RunTimeType.JS) {
|
||||
return getJSSessionController()
|
||||
}
|
||||
|
||||
throw new Error('No Runtime is configured')
|
||||
}
|
||||
|
||||
const getSASSessionController = (): SASSessionController => {
|
||||
if (process.sasSessionController) return process.sasSessionController
|
||||
|
||||
process.sasSessionController = new SASSessionController()
|
||||
@@ -202,7 +216,7 @@ export const getSASSessionController = (): SASSessionController => {
|
||||
return process.sasSessionController
|
||||
}
|
||||
|
||||
export const getJSSessionController = (): JSSessionController => {
|
||||
const getJSSessionController = (): JSSessionController => {
|
||||
if (process.jsSessionController) return process.jsSessionController
|
||||
|
||||
process.jsSessionController = new JSSessionController()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isWindows } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { generateFileUploadJSCode } from '../../utils'
|
||||
import { ExecutionVars } from './'
|
||||
@@ -19,7 +20,9 @@ export const createJSProgram = async (
|
||||
|
||||
const preProgramVarStatments = `
|
||||
let _webout = '';
|
||||
const weboutPath = '${weboutPath}';
|
||||
const weboutPath = '${
|
||||
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
||||
}';
|
||||
const _sasjs_tokenfile = '${tokenFile}';
|
||||
const _sasjs_username = '${preProgramVariables?.username}';
|
||||
const _sasjs_userid = '${preProgramVariables?.userId}';
|
||||
|
||||
@@ -40,7 +40,7 @@ export const processProgram = async (
|
||||
// waiting for the open event so that we can have underlying file descriptor
|
||||
await once(writeStream, 'open')
|
||||
|
||||
execFileSync(process.nodeLoc, [codePath], {
|
||||
execFileSync(process.nodeLoc!, [codePath], {
|
||||
stdio: ['ignore', writeStream, writeStream]
|
||||
})
|
||||
|
||||
|
||||
331
api/src/controllers/permission.ts
Normal file
331
api/src/controllers/permission.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import {
|
||||
Security,
|
||||
Route,
|
||||
Tags,
|
||||
Path,
|
||||
Example,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body
|
||||
} 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 PrincipalType {
|
||||
user = 'user',
|
||||
group = 'group'
|
||||
}
|
||||
|
||||
export enum PermissionSetting {
|
||||
grant = 'Grant',
|
||||
deny = 'Deny'
|
||||
}
|
||||
|
||||
interface RegisterPermissionPayload {
|
||||
/**
|
||||
* Name of affected resource
|
||||
* @example "/SASjsApi/code/execute"
|
||||
*/
|
||||
uri: string
|
||||
/**
|
||||
* The indication of whether (and to what extent) access is provided
|
||||
* @example "Grant"
|
||||
*/
|
||||
setting: PermissionSetting
|
||||
/**
|
||||
* 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: PermissionSetting
|
||||
}
|
||||
|
||||
export interface PermissionDetailsResponse {
|
||||
permissionId: number
|
||||
uri: string
|
||||
setting: string
|
||||
user?: UserResponse
|
||||
group?: GroupDetailsResponse
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/permission')
|
||||
@Tags('Permission')
|
||||
export class PermissionController {
|
||||
/**
|
||||
* @summary Get list of all permissions (uri, setting and userDetail).
|
||||
*
|
||||
*/
|
||||
@Example<PermissionDetailsResponse[]>([
|
||||
{
|
||||
permissionId: 123,
|
||||
uri: '/SASjsApi/code/execute',
|
||||
setting: 'Grant',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'johnSnow01',
|
||||
displayName: 'John Snow',
|
||||
isAdmin: false
|
||||
}
|
||||
},
|
||||
{
|
||||
permissionId: 124,
|
||||
uri: '/SASjsApi/code/execute',
|
||||
setting: 'Grant',
|
||||
group: {
|
||||
groupId: 1,
|
||||
name: 'DCGroup',
|
||||
description: 'This group represents Data Controller Users',
|
||||
isActive: true,
|
||||
users: []
|
||||
}
|
||||
}
|
||||
])
|
||||
@Get('/')
|
||||
public async getAllPermissions(): Promise<PermissionDetailsResponse[]> {
|
||||
return getAllPermissions()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create a new permission. Admin only.
|
||||
*
|
||||
*/
|
||||
@Example<PermissionDetailsResponse>({
|
||||
permissionId: 123,
|
||||
uri: '/SASjsApi/code/execute',
|
||||
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,
|
||||
uri: '/SASjsApi/code/execute',
|
||||
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 (): Promise<PermissionDetailsResponse[]> =>
|
||||
(await Permission.find({})
|
||||
.select({
|
||||
_id: 0,
|
||||
permissionId: 1,
|
||||
uri: 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[]
|
||||
|
||||
const createPermission = async ({
|
||||
uri,
|
||||
setting,
|
||||
principalType,
|
||||
principalId
|
||||
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
|
||||
const permission = new Permission({
|
||||
uri,
|
||||
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({
|
||||
uri,
|
||||
user: userInDB._id
|
||||
})
|
||||
|
||||
if (alreadyExists)
|
||||
throw {
|
||||
code: 409,
|
||||
status: 'Conflict',
|
||||
message: 'Permission already exists with provided URI 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({
|
||||
uri,
|
||||
group: groupInDB._id
|
||||
})
|
||||
if (alreadyExists)
|
||||
throw {
|
||||
code: 409,
|
||||
status: 'Conflict',
|
||||
message: 'Permission already exists with provided URI 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,
|
||||
uri: savedPermission.uri,
|
||||
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,
|
||||
uri: 1,
|
||||
setting: 1
|
||||
})
|
||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
||||
.populate({
|
||||
path: 'group',
|
||||
select: 'groupId name description -_id'
|
||||
})) as unknown as PermissionDetailsResponse
|
||||
if (!updatedPermission)
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'Permission not found.'
|
||||
}
|
||||
|
||||
return updatedPermission
|
||||
}
|
||||
|
||||
const deletePermission = async (id: number) => {
|
||||
const permission = await Permission.findOne({ permissionId: id })
|
||||
if (!permission)
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'Permission not found.'
|
||||
}
|
||||
await Permission.deleteOne({ permissionId: id })
|
||||
}
|
||||
@@ -13,7 +13,8 @@ export class SessionController {
|
||||
@Example<UserResponse>({
|
||||
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
|
||||
})
|
||||
|
||||
@@ -24,9 +24,10 @@ export interface UserResponse {
|
||||
id: number
|
||||
username: string
|
||||
displayName: string
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
interface UserDetailsResponse {
|
||||
export interface UserDetailsResponse {
|
||||
id: number
|
||||
displayName: string
|
||||
username: string
|
||||
@@ -48,12 +49,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 +203,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> => {
|
||||
|
||||
@@ -49,10 +49,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(() => {
|
||||
@@ -99,7 +99,8 @@ const login = async (
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName
|
||||
displayName: user.displayName,
|
||||
isAdmin: user.isAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { csrfProtection } from '../app'
|
||||
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
|
||||
import {
|
||||
fetchLatestAutoExec,
|
||||
ModeType,
|
||||
verifyTokenInDB,
|
||||
isAuthorizingRoute
|
||||
} from '../utils'
|
||||
import { desktopUser } from './desktop'
|
||||
import { authorize } from './authorize'
|
||||
|
||||
export const authenticateAccessToken: RequestHandler = async (
|
||||
req,
|
||||
@@ -15,6 +21,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,7 +34,7 @@ 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)
|
||||
}
|
||||
}
|
||||
@@ -34,8 +44,8 @@ export const authenticateAccessToken: RequestHandler = async (
|
||||
authenticateToken(
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
process.env.ACCESS_TOKEN_SECRET as string,
|
||||
nextFunction,
|
||||
process.secrets.ACCESS_TOKEN_SECRET,
|
||||
'accessToken'
|
||||
)
|
||||
}
|
||||
@@ -45,7 +55,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
process.env.REFRESH_TOKEN_SECRET as string,
|
||||
process.secrets.REFRESH_TOKEN_SECRET,
|
||||
'refreshToken'
|
||||
)
|
||||
}
|
||||
@@ -58,7 +68,7 @@ const authenticateToken = (
|
||||
tokenType: 'accessToken' | 'refreshToken'
|
||||
) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') {
|
||||
if (MODE === ModeType.Desktop) {
|
||||
req.user = {
|
||||
userId: 1234,
|
||||
clientId: 'desktopModeClientId',
|
||||
|
||||
36
api/src/middlewares/authorize.ts
Normal file
36
api/src/middlewares/authorize.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { RequestHandler } from 'express'
|
||||
import User from '../model/User'
|
||||
import Permission from '../model/Permission'
|
||||
import { PermissionSetting } from '../controllers/permission'
|
||||
import { getUri } 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()
|
||||
|
||||
const dbUser = await User.findOne({ id: user.userId })
|
||||
if (!dbUser) return res.sendStatus(401)
|
||||
|
||||
const uri = getUri(req)
|
||||
|
||||
// find permission w.r.t user
|
||||
const permission = await Permission.findOne({ uri, user: dbUser._id })
|
||||
|
||||
if (permission) {
|
||||
if (permission.setting === PermissionSetting.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({ uri, group })
|
||||
if (groupPermission?.setting === PermissionSetting.grant) return next()
|
||||
}
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export * from './authenticateToken'
|
||||
export * from './desktop'
|
||||
export * from './verifyAdmin'
|
||||
export * from './verifyAdminIfNeeded'
|
||||
export * from './authorize'
|
||||
|
||||
@@ -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')
|
||||
|
||||
45
api/src/model/Configuration.ts
Normal file
45
api/src/model/Configuration.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import mongoose, { Schema } from 'mongoose'
|
||||
|
||||
export interface ConfigurationType {
|
||||
/**
|
||||
* SecretOrPrivateKey to sign Access Token
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
ACCESS_TOKEN_SECRET: string
|
||||
/**
|
||||
* SecretOrPrivateKey to sign Refresh Token
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
REFRESH_TOKEN_SECRET: string
|
||||
/**
|
||||
* SecretOrPrivateKey to sign Auth Code
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
AUTH_CODE_SECRET: string
|
||||
/**
|
||||
* Secret used to sign the session cookie
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
SESSION_SECRET: string
|
||||
}
|
||||
|
||||
const ConfigurationSchema = new Schema<ConfigurationType>({
|
||||
ACCESS_TOKEN_SECRET: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
REFRESH_TOKEN_SECRET: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
AUTH_CODE_SECRET: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
SESSION_SECRET: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
export default mongoose.model('Configuration', ConfigurationSchema)
|
||||
@@ -1,5 +1,6 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
import User from './User'
|
||||
import { GroupDetailsResponse } from '../controllers'
|
||||
import User, { IUser } from './User'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
|
||||
export interface GroupPayload {
|
||||
@@ -27,8 +28,9 @@ interface IGroupDocument extends GroupPayload, Document {
|
||||
}
|
||||
|
||||
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> {}
|
||||
|
||||
@@ -70,28 +72,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',
|
||||
|
||||
36
api/src/model/Permission.ts
Normal file
36
api/src/model/Permission.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
|
||||
interface IPermissionDocument extends Document {
|
||||
uri: string
|
||||
setting: string
|
||||
permissionId: number
|
||||
user: Schema.Types.ObjectId
|
||||
group: Schema.Types.ObjectId
|
||||
}
|
||||
|
||||
interface IPermission extends IPermissionDocument {}
|
||||
|
||||
interface IPermissionModel extends Model<IPermission> {}
|
||||
|
||||
const permissionSchema = new Schema<IPermissionDocument>({
|
||||
uri: {
|
||||
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' })
|
||||
|
||||
export const Permission: IPermissionModel = model<
|
||||
IPermission,
|
||||
IPermissionModel
|
||||
>('Permission', permissionSchema)
|
||||
|
||||
export default Permission
|
||||
@@ -35,6 +35,7 @@ export interface UserPayload {
|
||||
}
|
||||
|
||||
interface IUserDocument extends UserPayload, Document {
|
||||
_id: Schema.Types.ObjectId
|
||||
id: number
|
||||
isAdmin: boolean
|
||||
isActive: boolean
|
||||
@@ -43,7 +44,7 @@ interface IUserDocument extends UserPayload, Document {
|
||||
tokens: [{ [key: string]: string }]
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -17,6 +17,7 @@ import groupRouter from './group'
|
||||
import clientRouter from './client'
|
||||
import authRouter from './auth'
|
||||
import sessionRouter from './session'
|
||||
import permissionRouter from './permission'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@@ -35,6 +36,12 @@ 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(
|
||||
'/',
|
||||
|
||||
@@ -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
|
||||
|
||||
69
api/src/routes/api/permission.ts
Normal file
69
api/src/routes/api/permission.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import express from 'express'
|
||||
import { PermissionController } from '../../controllers/'
|
||||
import { verifyAdmin } from '../../middlewares'
|
||||
import {
|
||||
registerPermissionValidation,
|
||||
updatePermissionValidation
|
||||
} from '../../utils'
|
||||
|
||||
const permissionRouter = express.Router()
|
||||
const controller = new PermissionController()
|
||||
|
||||
permissionRouter.get('/', async (req, res) => {
|
||||
try {
|
||||
const response = await controller.getAllPermissions()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
delete err.code
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
permissionRouter.post('/', verifyAdmin, async (req, res) => {
|
||||
const { error, value: body } = registerPermissionValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.createPermission(body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
delete err.code
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => {
|
||||
const { permissionId } = req.params
|
||||
|
||||
const { error, value: body } = updatePermissionValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.updatePermission(permissionId, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
delete err.code
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
permissionRouter.delete(
|
||||
'/:permissionId',
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
const { permissionId } = req.params
|
||||
|
||||
try {
|
||||
await controller.deletePermission(permissionId)
|
||||
res.status(200).send('Permission Deleted!')
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
delete err.code
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
export default permissionRouter
|
||||
@@ -29,7 +29,12 @@ jest
|
||||
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||
|
||||
import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import {
|
||||
UserController,
|
||||
PermissionController,
|
||||
PermissionSetting,
|
||||
PrincipalType
|
||||
} from '../../../controllers/'
|
||||
import { getTreeExample } from '../../../controllers/internal'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||
const { getFilesFolder } = fileUtilModules
|
||||
@@ -48,6 +53,7 @@ describe('drive', () => {
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const controller = new UserController()
|
||||
const permissionController = new PermissionController()
|
||||
|
||||
let accessToken: string
|
||||
|
||||
@@ -58,11 +64,31 @@ 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({
|
||||
uri: '/SASjsApi/drive/deploy',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/drive/deploy/upload',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/drive/file',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/drive/folder',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
})
|
||||
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -945,3 +971,12 @@ describe('drive', () => {
|
||||
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
|
||||
}
|
||||
|
||||
571
api/src/routes/api/spec/permission.spec.ts
Normal file
571
api/src/routes/api/spec/permission.spec.ts
Normal file
@@ -0,0 +1,571 @@
|
||||
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,
|
||||
ClientController,
|
||||
PermissionController,
|
||||
PrincipalType,
|
||||
PermissionSetting
|
||||
} 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 = {
|
||||
uri: '/SASjsApi/code/execute',
|
||||
setting: PermissionSetting.grant,
|
||||
principalType: PrincipalType.user,
|
||||
principalId: 123
|
||||
}
|
||||
|
||||
const group = {
|
||||
name: 'DCGroup1',
|
||||
description: 'DC group for testing purposes.'
|
||||
}
|
||||
|
||||
const userController = new UserController()
|
||||
const groupController = new GroupController()
|
||||
const clientController = new ClientController()
|
||||
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.uri).toEqual(permission.uri)
|
||||
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.uri).toEqual(permission.uri)
|
||||
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 even if user has permission', async () => {
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/permission',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.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 uri is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
uri: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"uri" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if uri is not valid', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
uri: '/some/random/api/endpoint'
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
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 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 principalId is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
principalId: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"principalId" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if principal type is not valid', async () => {
|
||||
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 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 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'
|
||||
})
|
||||
.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 URI 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: '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(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 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 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 not found (404) if permission with provided id does not exists', async () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/permission/123')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
setting: PermissionSetting.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,
|
||||
uri: '/test-1',
|
||||
principalId: dbUser.id
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
uri: '/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 all permissions when user is not admin', async () => {
|
||||
const dbUser = await userController.createUser({
|
||||
...user,
|
||||
username: 'get' + user.username
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/permission',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/permission/')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe.only('verify', () => {
|
||||
beforeAll(async () => {
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
uri: '/SASjsApi/drive/deploy',
|
||||
principalId: dbUser.id
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(DriveController.prototype, 'deploy')
|
||||
.mockImplementation((deployPayload) =>
|
||||
Promise.resolve({
|
||||
status: 'success',
|
||||
message: 'Files deployed successfully to @sasjs/server.'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('should create files in SASJS drive', async () => {
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
|
||||
await request(app)
|
||||
.get('/SASjsApi/drive/deploy')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send(deployPayload)
|
||||
.expect(200)
|
||||
})
|
||||
|
||||
it('should respond unauthorized', async () => {
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
|
||||
await request(app)
|
||||
.get('/SASjsApi/drive/deploy/upload')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(401)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const generateSaveTokenAndCreateUser = async (
|
||||
someUser?: any
|
||||
): Promise<string> => {
|
||||
const dbUser = await userController.createUser(someUser ?? adminUser)
|
||||
|
||||
return generateAndSaveToken(dbUser.id)
|
||||
}
|
||||
|
||||
const generateAndSaveToken = async (userId: number) => {
|
||||
const adminAccessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId
|
||||
})
|
||||
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
|
||||
return adminAccessToken
|
||||
}
|
||||
|
||||
const deleteAllPermissions = async () => {
|
||||
const { collections } = mongoose.connection
|
||||
const collection = collections['permissions']
|
||||
await collection.deleteMany({})
|
||||
}
|
||||
@@ -4,7 +4,12 @@ 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,
|
||||
PermissionSetting,
|
||||
PrincipalType
|
||||
} from '../../../controllers/'
|
||||
import {
|
||||
generateAccessToken,
|
||||
saveTokensInDB,
|
||||
@@ -41,12 +46,21 @@ describe('stp', () => {
|
||||
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({
|
||||
uri: '/SASjsApi/stp/execute',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -770,12 +770,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 +798,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
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
@@ -79,7 +79,8 @@ describe('web', () => {
|
||||
expect(res.body.user).toEqual({
|
||||
id: expect.any(Number),
|
||||
username: user.username,
|
||||
displayName: user.displayName
|
||||
displayName: user.displayName,
|
||||
isAdmin: user.isAdmin
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'path'
|
||||
import express, { Request } from 'express'
|
||||
import { authenticateAccessToken } from '../../middlewares'
|
||||
import { folderExists } from '@sasjs/utils'
|
||||
|
||||
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
||||
@@ -9,7 +10,7 @@ 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())
|
||||
@@ -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
|
||||
|
||||
@@ -48,7 +48,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!')
|
||||
|
||||
@@ -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}`
|
||||
|
||||
6
api/src/types/system/process.d.ts
vendored
6
api/src/types/system/process.d.ts
vendored
@@ -1,12 +1,14 @@
|
||||
declare namespace NodeJS {
|
||||
export interface Process {
|
||||
sasLoc: string
|
||||
nodeLoc: string
|
||||
sasLoc?: string
|
||||
nodeLoc?: string
|
||||
driveLoc: string
|
||||
logsLoc: string
|
||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
||||
appStreamConfig: import('../').AppStreamConfig
|
||||
logger: import('@sasjs/utils/logger').Logger
|
||||
runTimes: import('../../utils').RunTimeType[]
|
||||
secrets: import('../../model/Configuration').ConfigurationType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,5 @@ export const connectDB = async () => {
|
||||
}
|
||||
|
||||
console.log('Connected to DB!')
|
||||
await seedDB()
|
||||
|
||||
return mongoose.connection
|
||||
return seedDB()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ export const getDesktopUserAutoExecPath = () =>
|
||||
|
||||
export const getSasjsRootFolder = () => process.driveLoc
|
||||
|
||||
export const getLogFolder = () => process.logsLoc
|
||||
|
||||
export const getAppStreamConfigPath = () =>
|
||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
||||
|
||||
@@ -32,8 +34,6 @@ 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 = () =>
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
35
api/src/utils/getAuthorizedRoutes.ts
Normal file
35
api/src/utils/getAuthorizedRoutes.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Request } from 'express'
|
||||
|
||||
const StaticAuthorizedRoutes = [
|
||||
'/AppStream',
|
||||
'/SASjsApi/code/execute',
|
||||
'/SASjsApi/stp/execute',
|
||||
'/SASjsApi/drive/deploy',
|
||||
'/SASjsApi/drive/deploy/upload',
|
||||
'/SASjsApi/drive/file',
|
||||
'/SASjsApi/drive/folder',
|
||||
'/SASjsApi/drive/fileTree',
|
||||
'/SASjsApi/permission'
|
||||
]
|
||||
|
||||
export const getAuthorizedRoutes = () => {
|
||||
const streamingApps = Object.keys(process.appStreamConfig)
|
||||
const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`)
|
||||
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
|
||||
}
|
||||
|
||||
export const getUri = (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(getUri(req))
|
||||
@@ -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)))) {
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
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 sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||
const nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
||||
let sasLoc, nodeLoc
|
||||
|
||||
if (process.runTimes.includes(RunTimeType.SAS)) {
|
||||
sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||
}
|
||||
|
||||
if (process.runTimes.includes(RunTimeType.JS)) {
|
||||
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||
}
|
||||
|
||||
return { sasLoc, nodeLoc }
|
||||
}
|
||||
@@ -55,7 +60,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 +80,11 @@ 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
|
||||
)
|
||||
|
||||
@@ -5,14 +5,10 @@ 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) {
|
||||
// If programPath (_program) is provided with a ".sas" or ".js" 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)
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from './file'
|
||||
export * from './generateAccessToken'
|
||||
export * from './generateAuthCode'
|
||||
export * from './generateRefreshToken'
|
||||
export * from './getAuthorizedRoutes'
|
||||
export * from './getCertificates'
|
||||
export * from './getDesktopFields'
|
||||
export * from './getPreProgramVariables'
|
||||
|
||||
@@ -1,6 +1,73 @@
|
||||
import Client from '../model/Client'
|
||||
import Group 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 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 CLIENT = {
|
||||
clientId: 'clientID1',
|
||||
clientSecret: 'clientSecret'
|
||||
@@ -13,23 +80,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}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
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
|
||||
} else {
|
||||
const { sasLoc, nodeLoc } = await getDesktopFields()
|
||||
|
||||
@@ -26,10 +40,16 @@ 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)
|
||||
|
||||
console.log('sasLoc: ', process.sasLoc)
|
||||
console.log('sasDrive: ', process.driveLoc)
|
||||
console.log('sasLogs: ', process.logsLoc)
|
||||
console.log('runTimes: ', process.runTimes)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extractHeaders } from '..'
|
||||
import { extractHeaders } from '../extractHeaders'
|
||||
|
||||
describe('extractHeaders', () => {
|
||||
it('should return valid http headers', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseLogToArray } from '..'
|
||||
import { parseLogToArray } from '../parseLogToArray'
|
||||
|
||||
describe('parseLogToArray', () => {
|
||||
it('should parse log to array type', () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { RunTimeType } from '.'
|
||||
import { PermissionSetting, PrincipalType } from '../controllers/permission'
|
||||
import { getAuthorizedRoutes } from './getAuthorizedRoutes'
|
||||
|
||||
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||
const passwordSchema = Joi.string().min(6).max(1024)
|
||||
@@ -86,6 +87,27 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
||||
clientSecret: Joi.string().required()
|
||||
}).validate(data)
|
||||
|
||||
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
uri: Joi.string()
|
||||
.required()
|
||||
.valid(...getAuthorizedRoutes()),
|
||||
setting: Joi.string()
|
||||
.required()
|
||||
.valid(...Object.values(PermissionSetting)),
|
||||
principalType: Joi.string()
|
||||
.required()
|
||||
.valid(...Object.values(PrincipalType)),
|
||||
principalId: Joi.number().required()
|
||||
}).validate(data)
|
||||
|
||||
export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
setting: Joi.string()
|
||||
.required()
|
||||
.valid(...Object.values(PermissionSetting))
|
||||
}).validate(data)
|
||||
|
||||
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
||||
@@ -124,7 +146,7 @@ export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
||||
export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
code: Joi.string().required(),
|
||||
runTime: Joi.string().valid(...Object.values(RunTimeType))
|
||||
runTime: Joi.string().valid(...process.runTimes)
|
||||
}).validate(data)
|
||||
|
||||
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
@@ -78,33 +78,7 @@ const verifyMODE = (): string[] => {
|
||||
}
|
||||
|
||||
if (process.env.MODE === ModeType.Server) {
|
||||
const {
|
||||
ACCESS_TOKEN_SECRET,
|
||||
REFRESH_TOKEN_SECRET,
|
||||
AUTH_CODE_SECRET,
|
||||
SESSION_SECRET,
|
||||
DB_CONNECT
|
||||
} = process.env
|
||||
|
||||
if (!ACCESS_TOKEN_SECRET)
|
||||
errors.push(
|
||||
`- ACCESS_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||
)
|
||||
|
||||
if (!REFRESH_TOKEN_SECRET)
|
||||
errors.push(
|
||||
`- REFRESH_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||
)
|
||||
|
||||
if (!AUTH_CODE_SECRET)
|
||||
errors.push(
|
||||
`- AUTH_CODE_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||
)
|
||||
|
||||
if (!SESSION_SECRET)
|
||||
errors.push(
|
||||
`- SESSION_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||
)
|
||||
const { DB_CONNECT } = process.env
|
||||
|
||||
if (process.env.NODE_ENV !== 'test')
|
||||
if (!DB_CONNECT)
|
||||
@@ -129,16 +103,16 @@ const verifyPROTOCOL = (): string[] => {
|
||||
}
|
||||
|
||||
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
|
||||
const { PRIVATE_KEY, FULL_CHAIN } = process.env
|
||||
const { PRIVATE_KEY, CERT_CHAIN } = process.env
|
||||
|
||||
if (!PRIVATE_KEY)
|
||||
errors.push(
|
||||
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||
)
|
||||
|
||||
if (!FULL_CHAIN)
|
||||
if (!CERT_CHAIN)
|
||||
errors.push(
|
||||
`- FULL_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||
`- CERT_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,5 +232,5 @@ const DEFAULTS = {
|
||||
PORT: '5000',
|
||||
HELMET_COEP: HelmetCoepType.TRUE,
|
||||
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
||||
RUN_TIMES: `${RunTimeType.SAS},${RunTimeType.JS}`
|
||||
RUN_TIMES: RunTimeType.SAS
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ export const extractJSONFromZip = async (zipFile: Express.Multer.File) => {
|
||||
|
||||
for await (const entry of zip) {
|
||||
const fileName = entry.path as string
|
||||
if (fileName.toUpperCase().endsWith('.JSON') && fileName === fileInZip) {
|
||||
// grab the first json found in .zip
|
||||
if (fileName.toUpperCase().endsWith('.JSON')) {
|
||||
fileContent = await entry.buffer()
|
||||
break
|
||||
} else {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"tags": [
|
||||
{
|
||||
"name": "Info",
|
||||
"description": "Get Server Info"
|
||||
"description": "Get Server Information"
|
||||
},
|
||||
{
|
||||
"name": "Session",
|
||||
@@ -21,7 +21,11 @@
|
||||
},
|
||||
{
|
||||
"name": "User",
|
||||
"description": "Operations about users"
|
||||
"description": "Operations with users"
|
||||
},
|
||||
{
|
||||
"name": "Permission",
|
||||
"description": "Operations about permissions"
|
||||
},
|
||||
{
|
||||
"name": "Client",
|
||||
@@ -33,19 +37,19 @@
|
||||
},
|
||||
{
|
||||
"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": "STP",
|
||||
"description": "Operations about STP"
|
||||
"description": "Execution of Stored Programs"
|
||||
},
|
||||
{
|
||||
"name": "CODE",
|
||||
"description": "Operations on SAS code"
|
||||
"description": "Execution of code (various runtimes are supported)"
|
||||
},
|
||||
{
|
||||
"name": "Web",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "npx webpack-dev-server --config webpack.dev.ts --hot",
|
||||
"build": "npx webpack --config webpack.prod.ts"
|
||||
"start": "webpack-dev-server --config webpack.dev.ts --hot",
|
||||
"build": "webpack --config webpack.prod.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.4.1",
|
||||
|
||||
35
web/src/components/dialogTitle.tsx
Normal file
35
web/src/components/dialogTitle.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
|
||||
export interface DialogTitleProps {
|
||||
id: string
|
||||
children?: React.ReactNode
|
||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export const BootstrapDialogTitle = (props: DialogTitleProps) => {
|
||||
const { children, handleOpen, ...other } = props
|
||||
|
||||
return (
|
||||
<DialogTitle sx={{ m: 0, p: 2 }} {...other}>
|
||||
{children}
|
||||
{handleOpen ? (
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={() => handleOpen(false)}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: 8,
|
||||
color: (theme) => theme.palette.grey[500]
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
) : null}
|
||||
</DialogTitle>
|
||||
)
|
||||
}
|
||||
@@ -144,6 +144,18 @@ const Header = (props: any) => {
|
||||
open={!!anchorEl}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
href={'https://server.sasjs.io'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
>
|
||||
Documentation
|
||||
</Button>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
component={Link}
|
||||
|
||||
@@ -22,7 +22,7 @@ const Login = () => {
|
||||
username,
|
||||
password
|
||||
}).catch((err: any) => {
|
||||
setErrorMessage(err.response.data)
|
||||
setErrorMessage(err.response?.data || err.toString())
|
||||
return {}
|
||||
})
|
||||
|
||||
@@ -30,6 +30,7 @@ const Login = () => {
|
||||
appContext.setUserId?.(user.id)
|
||||
appContext.setUsername?.(user.username)
|
||||
appContext.setDisplayName?.(user.displayName)
|
||||
appContext.setIsAdmin?.(user.isAdmin)
|
||||
appContext.setLoggedIn?.(loggedIn)
|
||||
}
|
||||
}
|
||||
|
||||
43
web/src/components/modal.tsx
Normal file
43
web/src/components/modal.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Typography, Dialog, DialogContent } from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
import { BootstrapDialogTitle } from './dialogTitle'
|
||||
|
||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
padding: theme.spacing(1)
|
||||
}
|
||||
}))
|
||||
|
||||
export interface ModalProps {
|
||||
open: boolean
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
title: string
|
||||
payload: string
|
||||
}
|
||||
|
||||
const Modal = (props: ModalProps) => {
|
||||
const { open, setOpen, title, payload } = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
||||
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||
{title}
|
||||
</BootstrapDialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography gutterBottom>
|
||||
<span style={{ fontFamily: 'monospace' }}>{payload}</span>
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
</BootstrapDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
||||
62
web/src/components/snackbar.tsx
Normal file
62
web/src/components/snackbar.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { Dispatch, SetStateAction } from 'react'
|
||||
import Snackbar from '@mui/material/Snackbar'
|
||||
import MuiAlert, { AlertProps } from '@mui/material/Alert'
|
||||
import Slide, { SlideProps } from '@mui/material/Slide'
|
||||
|
||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
|
||||
})
|
||||
|
||||
const Transition = (props: SlideProps) => {
|
||||
return <Slide {...props} direction="up" />
|
||||
}
|
||||
|
||||
export enum AlertSeverityType {
|
||||
Success = 'success',
|
||||
Warning = 'warning',
|
||||
Info = 'info',
|
||||
Error = 'error'
|
||||
}
|
||||
|
||||
type BootstrapSnackbarProps = {
|
||||
open: boolean
|
||||
setOpen: Dispatch<SetStateAction<boolean>>
|
||||
message: string
|
||||
severity: AlertSeverityType
|
||||
}
|
||||
|
||||
const BootstrapSnackbar = ({
|
||||
open,
|
||||
setOpen,
|
||||
message,
|
||||
severity
|
||||
}: BootstrapSnackbarProps) => {
|
||||
const handleClose = (
|
||||
event: React.SyntheticEvent | Event,
|
||||
reason?: string
|
||||
) => {
|
||||
if (reason === 'clickaway') {
|
||||
return
|
||||
}
|
||||
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={3000}
|
||||
onClose={handleClose}
|
||||
TransitionComponent={Transition}
|
||||
>
|
||||
<Alert onClose={handleClose} severity={severity} sx={{ width: '100%' }}>
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
)
|
||||
}
|
||||
|
||||
export default BootstrapSnackbar
|
||||
214
web/src/containers/Settings/addPermissionModal.tsx
Normal file
214
web/src/containers/Settings/addPermissionModal.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import React, { useState, useEffect, Dispatch, SetStateAction } from 'react'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
Autocomplete
|
||||
} from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
||||
|
||||
import {
|
||||
UserResponse,
|
||||
GroupResponse,
|
||||
RegisterPermissionPayload
|
||||
} from '../../utils/types'
|
||||
|
||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
padding: theme.spacing(1)
|
||||
}
|
||||
}))
|
||||
|
||||
type AddPermissionModalProps = {
|
||||
open: boolean
|
||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||
addPermission: (addPermissionPayload: RegisterPermissionPayload) => void
|
||||
}
|
||||
|
||||
const AddPermissionModal = ({
|
||||
open,
|
||||
handleOpen,
|
||||
addPermission
|
||||
}: AddPermissionModalProps) => {
|
||||
const [URIs, setURIs] = useState<string[]>([])
|
||||
const [loadingURIs, setLoadingURIs] = useState(false)
|
||||
const [uri, setUri] = useState<string>()
|
||||
const [principalType, setPrincipalType] = useState('user')
|
||||
const [userPrincipal, setUserPrincipal] = useState<UserResponse>()
|
||||
const [groupPrincipal, setGroupPrincipal] = useState<GroupResponse>()
|
||||
const [permissionSetting, setPermissionSetting] = useState('Grant')
|
||||
const [loadingPrincipals, setLoadingPrincipals] = useState(false)
|
||||
const [userPrincipals, setUserPrincipals] = useState<UserResponse[]>([])
|
||||
const [groupPrincipals, setGroupPrincipals] = useState<GroupResponse[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingURIs(true)
|
||||
axios
|
||||
.get('/SASjsApi/info/authorizedRoutes')
|
||||
.then((res: any) => {
|
||||
if (res.data) {
|
||||
setURIs(res.data.URIs)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingURIs(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingPrincipals(true)
|
||||
axios
|
||||
.get(`/SASjsApi/${principalType}`)
|
||||
.then((res: any) => {
|
||||
if (res.data) {
|
||||
if (principalType === 'user') {
|
||||
const users: UserResponse[] = res.data
|
||||
const nonAdminUsers = users.filter((user) => !user.isAdmin)
|
||||
setUserPrincipals(nonAdminUsers)
|
||||
} else {
|
||||
setGroupPrincipals(res.data)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingPrincipals(false)
|
||||
})
|
||||
}, [principalType])
|
||||
|
||||
const handleAddPermission = () => {
|
||||
const addPermissionPayload: any = {
|
||||
uri,
|
||||
setting: permissionSetting,
|
||||
principalType
|
||||
}
|
||||
if (principalType === 'user' && userPrincipal) {
|
||||
addPermissionPayload.principalId = userPrincipal.id
|
||||
} else if (principalType === 'group' && groupPrincipal) {
|
||||
addPermissionPayload.principalId = groupPrincipal.groupId
|
||||
}
|
||||
addPermission(addPermissionPayload)
|
||||
}
|
||||
|
||||
const addButtonDisabled =
|
||||
!uri || (principalType === 'user' ? !userPrincipal : !groupPrincipal)
|
||||
|
||||
return (
|
||||
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
||||
<BootstrapDialogTitle
|
||||
id="add-permission-dialog-title"
|
||||
handleOpen={handleOpen}
|
||||
>
|
||||
Add Permission
|
||||
</BootstrapDialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
options={URIs}
|
||||
disableClearable
|
||||
value={uri}
|
||||
onChange={(event: any, newValue: string) => setUri(newValue)}
|
||||
renderInput={(params) =>
|
||||
loadingURIs ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<TextField {...params} label="Principal" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
options={['user', 'group']}
|
||||
disableClearable
|
||||
value={principalType}
|
||||
onChange={(event: any, newValue: string) =>
|
||||
setPrincipalType(newValue)
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Principal Type" />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{principalType === 'user' ? (
|
||||
<Autocomplete
|
||||
options={userPrincipals}
|
||||
getOptionLabel={(option) => option.displayName}
|
||||
disableClearable
|
||||
value={userPrincipal}
|
||||
onChange={(event: any, newValue: UserResponse) =>
|
||||
setUserPrincipal(newValue)
|
||||
}
|
||||
renderInput={(params) =>
|
||||
loadingPrincipals ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<TextField {...params} label="Principal" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Autocomplete
|
||||
options={groupPrincipals}
|
||||
getOptionLabel={(option) => option.name}
|
||||
disableClearable
|
||||
value={groupPrincipal}
|
||||
onChange={(event: any, newValue: GroupResponse) =>
|
||||
setGroupPrincipal(newValue)
|
||||
}
|
||||
renderInput={(params) =>
|
||||
loadingPrincipals ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<TextField {...params} label="Principal" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
options={['Grant', 'Deny']}
|
||||
disableClearable
|
||||
value={permissionSetting}
|
||||
onChange={(event: any, newValue: string) =>
|
||||
setPermissionSetting(newValue)
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Settings" />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleAddPermission}
|
||||
disabled={addButtonDisabled}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</BootstrapDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddPermissionModal
|
||||
44
web/src/containers/Settings/deletePermissionModal.tsx
Normal file
44
web/src/containers/Settings/deletePermissionModal.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
padding: theme.spacing(1)
|
||||
}
|
||||
}))
|
||||
|
||||
type DeleteModalProps = {
|
||||
open: boolean
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
deletePermission: () => void
|
||||
}
|
||||
|
||||
const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => {
|
||||
return (
|
||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
||||
<DialogContent dividers>
|
||||
<Typography gutterBottom>
|
||||
Are you sure you want to delete this permission?
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button color="error" onClick={() => deletePermission()}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</BootstrapDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteModal
|
||||
@@ -1,12 +1,15 @@
|
||||
import * as React from 'react'
|
||||
import React, { useState, useContext } from 'react'
|
||||
|
||||
import { Box, Paper, Tab, styled } from '@mui/material'
|
||||
import TabContext from '@mui/lab/TabContext'
|
||||
import TabList from '@mui/lab/TabList'
|
||||
import TabPanel from '@mui/lab/TabPanel'
|
||||
|
||||
import Permission from './permission'
|
||||
import Profile from './profile'
|
||||
|
||||
import { AppContext, ModeType } from '../../context/appContext'
|
||||
|
||||
const StyledTab = styled(Tab)({
|
||||
background: 'black',
|
||||
margin: '0 5px 5px 0'
|
||||
@@ -17,7 +20,8 @@ const StyledTabpanel = styled(TabPanel)({
|
||||
})
|
||||
|
||||
const Settings = () => {
|
||||
const [value, setValue] = React.useState('profile')
|
||||
const appContext = useContext(AppContext)
|
||||
const [value, setValue] = useState('profile')
|
||||
|
||||
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||
setValue(newValue)
|
||||
@@ -42,11 +46,17 @@ const Settings = () => {
|
||||
onChange={handleChange}
|
||||
>
|
||||
<StyledTab label="Profile" value="profile" />
|
||||
{appContext.mode === ModeType.Server && (
|
||||
<StyledTab label="Uri Access" value="permission" />
|
||||
)}
|
||||
</TabList>
|
||||
</Box>
|
||||
<StyledTabpanel value="profile">
|
||||
<Profile />
|
||||
</StyledTabpanel>
|
||||
<StyledTabpanel value="permission">
|
||||
<Permission />
|
||||
</StyledTabpanel>
|
||||
</TabContext>
|
||||
</Box>
|
||||
)
|
||||
|
||||
483
web/src/containers/Settings/permission.tsx
Normal file
483
web/src/containers/Settings/permission.tsx
Normal file
@@ -0,0 +1,483 @@
|
||||
import React, { useState, useEffect, useContext, useCallback } from 'react'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Box,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Grid,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Popover
|
||||
} from '@mui/material'
|
||||
|
||||
import FilterListIcon from '@mui/icons-material/FilterList'
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
||||
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
import Modal from '../../components/modal'
|
||||
import PermissionFilterModal from './permissionFilterModal'
|
||||
import AddPermissionModal from './addPermissionModal'
|
||||
import UpdatePermissionModal from './updatePermissionModal'
|
||||
import DeleteModal from './deletePermissionModal'
|
||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||
|
||||
import {
|
||||
GroupDetailsResponse,
|
||||
PermissionResponse,
|
||||
RegisterPermissionPayload
|
||||
} from '../../utils/types'
|
||||
import { AppContext } from '../../context/appContext'
|
||||
|
||||
const BootstrapTableCell = styled(TableCell)({
|
||||
textAlign: 'left'
|
||||
})
|
||||
|
||||
export enum PrincipalType {
|
||||
User = 'User',
|
||||
Group = 'Group'
|
||||
}
|
||||
|
||||
const Permission = () => {
|
||||
const appContext = useContext(AppContext)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [openModal, setOpenModal] = useState(false)
|
||||
const [modalTitle, setModalTitle] = useState('')
|
||||
const [modalPayload, setModalPayload] = useState('')
|
||||
const [openSnackbar, setOpenSnackbar] = useState(false)
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||
AlertSeverityType.Success
|
||||
)
|
||||
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
||||
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
||||
useState(false)
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||
const [selectedPermission, setSelectedPermission] =
|
||||
useState<PermissionResponse>()
|
||||
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||
const [uriFilter, setUriFilter] = useState<string[]>([])
|
||||
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
||||
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
||||
PrincipalType[]
|
||||
>([])
|
||||
const [settingFilter, setSettingFilter] = useState<string[]>([])
|
||||
const [permissions, setPermissions] = useState<PermissionResponse[]>([])
|
||||
const [filteredPermissions, setFilteredPermissions] = useState<
|
||||
PermissionResponse[]
|
||||
>([])
|
||||
const [filterApplied, setFilterApplied] = useState(false)
|
||||
|
||||
const fetchPermissions = useCallback(() => {
|
||||
axios
|
||||
.get(`/SASjsApi/permission`)
|
||||
.then((res: any) => {
|
||||
if (res.data?.length > 0) {
|
||||
setPermissions(res.data)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchPermissions()
|
||||
}, [fetchPermissions])
|
||||
|
||||
/**
|
||||
* first find the permissions w.r.t each filter type
|
||||
* take intersection of resultant arrays
|
||||
*/
|
||||
const applyFilter = () => {
|
||||
setFilterModalOpen(false)
|
||||
|
||||
const uriFilteredPermissions =
|
||||
uriFilter.length > 0
|
||||
? permissions.filter((permission) => uriFilter.includes(permission.uri))
|
||||
: permissions
|
||||
|
||||
const principalFilteredPermissions =
|
||||
principalFilter.length > 0
|
||||
? permissions.filter((permission) => {
|
||||
if (permission.user) {
|
||||
return principalFilter.includes(permission.user.username)
|
||||
}
|
||||
if (permission.group) {
|
||||
return principalFilter.includes(permission.group.name)
|
||||
}
|
||||
return false
|
||||
})
|
||||
: permissions
|
||||
|
||||
const principalTypeFilteredPermissions =
|
||||
principalTypeFilter.length > 0
|
||||
? permissions.filter((permission) => {
|
||||
if (permission.user) {
|
||||
return principalTypeFilter.includes(PrincipalType.User)
|
||||
}
|
||||
if (permission.group) {
|
||||
return principalTypeFilter.includes(PrincipalType.Group)
|
||||
}
|
||||
return false
|
||||
})
|
||||
: permissions
|
||||
|
||||
const settingFilteredPermissions =
|
||||
settingFilter.length > 0
|
||||
? permissions.filter((permission) =>
|
||||
settingFilter.includes(permission.setting)
|
||||
)
|
||||
: permissions
|
||||
|
||||
let filteredArray = uriFilteredPermissions.filter((permission) =>
|
||||
principalFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
)
|
||||
|
||||
filteredArray = filteredArray.filter((permission) =>
|
||||
principalTypeFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
)
|
||||
|
||||
filteredArray = filteredArray.filter((permission) =>
|
||||
settingFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
)
|
||||
|
||||
setFilteredPermissions(filteredArray)
|
||||
setFilterApplied(true)
|
||||
}
|
||||
|
||||
const resetFilter = () => {
|
||||
setFilterModalOpen(false)
|
||||
setUriFilter([])
|
||||
setPrincipalFilter([])
|
||||
setSettingFilter([])
|
||||
setFilteredPermissions([])
|
||||
setFilterApplied(false)
|
||||
}
|
||||
|
||||
const addPermission = (addPermissionPayload: RegisterPermissionPayload) => {
|
||||
setAddPermissionModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post('/SASjsApi/permission', addPermissionPayload)
|
||||
.then((res: any) => {
|
||||
fetchPermissions()
|
||||
setSnackbarMessage('Permission added!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdatePermissionClick = (permission: PermissionResponse) => {
|
||||
setSelectedPermission(permission)
|
||||
setUpdatePermissionModalOpen(true)
|
||||
}
|
||||
|
||||
const updatePermission = (setting: string) => {
|
||||
setUpdatePermissionModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, {
|
||||
setting
|
||||
})
|
||||
.then((res: any) => {
|
||||
fetchPermissions()
|
||||
setSnackbarMessage('Permission updated!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setSelectedPermission(undefined)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
||||
setSelectedPermission(permission)
|
||||
setDeleteModalOpen(true)
|
||||
}
|
||||
|
||||
const deletePermission = () => {
|
||||
setDeleteModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
||||
.then((res: any) => {
|
||||
fetchPermissions()
|
||||
setSnackbarMessage('Permission deleted!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setSelectedPermission(undefined)
|
||||
})
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<CircularProgress
|
||||
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||
/>
|
||||
) : (
|
||||
<Box className="permissions-page">
|
||||
<Grid container direction="column" spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<Paper elevation={3} sx={{ display: 'flex' }}>
|
||||
<Tooltip title="Filter Permissions">
|
||||
<IconButton>
|
||||
<FilterListIcon onClick={() => setFilterModalOpen(true)} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{appContext.isAdmin && (
|
||||
<Tooltip
|
||||
sx={{ marginLeft: 'auto' }}
|
||||
title="Add Permission"
|
||||
placement="bottom-end"
|
||||
>
|
||||
<IconButton onClick={() => setAddPermissionModalOpen(true)}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<PermissionTable
|
||||
permissions={filterApplied ? filteredPermissions : permissions}
|
||||
handleUpdatePermissionClick={handleUpdatePermissionClick}
|
||||
handleDeletePermissionClick={handleDeletePermissionClick}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<BootstrapSnackbar
|
||||
open={openSnackbar}
|
||||
setOpen={setOpenSnackbar}
|
||||
message={snackbarMessage}
|
||||
severity={snackbarSeverity}
|
||||
/>
|
||||
<Modal
|
||||
open={openModal}
|
||||
setOpen={setOpenModal}
|
||||
title={modalTitle}
|
||||
payload={modalPayload}
|
||||
/>
|
||||
<PermissionFilterModal
|
||||
open={filterModalOpen}
|
||||
handleOpen={setFilterModalOpen}
|
||||
permissions={permissions}
|
||||
uriFilter={uriFilter}
|
||||
setUriFilter={setUriFilter}
|
||||
principalFilter={principalFilter}
|
||||
setPrincipalFilter={setPrincipalFilter}
|
||||
principalTypeFilter={principalTypeFilter}
|
||||
setPrincipalTypeFilter={setPrincipalTypeFilter}
|
||||
settingFilter={settingFilter}
|
||||
setSettingFilter={setSettingFilter}
|
||||
applyFilter={applyFilter}
|
||||
resetFilter={resetFilter}
|
||||
/>
|
||||
<AddPermissionModal
|
||||
open={addPermissionModalOpen}
|
||||
handleOpen={setAddPermissionModalOpen}
|
||||
addPermission={addPermission}
|
||||
/>
|
||||
<UpdatePermissionModal
|
||||
open={updatePermissionModalOpen}
|
||||
handleOpen={setUpdatePermissionModalOpen}
|
||||
permission={selectedPermission}
|
||||
updatePermission={updatePermission}
|
||||
/>
|
||||
<DeleteModal
|
||||
open={deleteModalOpen}
|
||||
setOpen={setDeleteModalOpen}
|
||||
deletePermission={deletePermission}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Permission
|
||||
|
||||
type PermissionTableProps = {
|
||||
permissions: PermissionResponse[]
|
||||
handleUpdatePermissionClick: (permission: PermissionResponse) => void
|
||||
handleDeletePermissionClick: (permission: PermissionResponse) => void
|
||||
}
|
||||
|
||||
const PermissionTable = ({
|
||||
permissions,
|
||||
handleUpdatePermissionClick,
|
||||
handleDeletePermissionClick
|
||||
}: PermissionTableProps) => {
|
||||
const appContext = useContext(AppContext)
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }}>
|
||||
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
||||
<TableRow>
|
||||
<BootstrapTableCell>Uri</BootstrapTableCell>
|
||||
<BootstrapTableCell>Principal</BootstrapTableCell>
|
||||
<BootstrapTableCell>Type</BootstrapTableCell>
|
||||
<BootstrapTableCell>Setting</BootstrapTableCell>
|
||||
{appContext.isAdmin && (
|
||||
<BootstrapTableCell>Action</BootstrapTableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{permissions.map((permission) => (
|
||||
<TableRow key={permission.permissionId}>
|
||||
<BootstrapTableCell>{permission.uri}</BootstrapTableCell>
|
||||
<BootstrapTableCell>
|
||||
{displayPrincipal(permission)}
|
||||
</BootstrapTableCell>
|
||||
<BootstrapTableCell>
|
||||
{displayPrincipalType(permission)}
|
||||
</BootstrapTableCell>
|
||||
<BootstrapTableCell>{permission.setting}</BootstrapTableCell>
|
||||
{appContext.isAdmin && (
|
||||
<BootstrapTableCell>
|
||||
<Tooltip title="Edit Permission">
|
||||
<IconButton
|
||||
onClick={() => handleUpdatePermissionClick(permission)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete Permission">
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={() => handleDeletePermissionClick(permission)}
|
||||
>
|
||||
<DeleteForeverIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</BootstrapTableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const displayPrincipal = (permission: PermissionResponse) => {
|
||||
if (permission.user) return permission.user.username
|
||||
if (permission.group) return <DisplayGroup group={permission.group} />
|
||||
}
|
||||
|
||||
type DisplayGroupProps = {
|
||||
group: GroupDetailsResponse
|
||||
}
|
||||
|
||||
const DisplayGroup = ({ group }: DisplayGroupProps) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
|
||||
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handlePopoverClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const open = Boolean(anchorEl)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography
|
||||
aria-owns={open ? 'mouse-over-popover' : undefined}
|
||||
aria-haspopup="true"
|
||||
onMouseEnter={handlePopoverOpen}
|
||||
onMouseLeave={handlePopoverClose}
|
||||
>
|
||||
{group.name}
|
||||
</Typography>
|
||||
<Popover
|
||||
id="mouse-over-popover"
|
||||
sx={{
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
onClose={handlePopoverClose}
|
||||
disableRestoreFocus
|
||||
>
|
||||
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
||||
Group Members
|
||||
</Typography>
|
||||
{group.users.map((user) => (
|
||||
<Typography sx={{ p: 1 }} component="li">
|
||||
{user.username}
|
||||
</Typography>
|
||||
))}
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const displayPrincipalType = (permission: PermissionResponse) => {
|
||||
if (permission.user) return PrincipalType.User
|
||||
if (permission.group) return PrincipalType.Group
|
||||
}
|
||||
154
web/src/containers/Settings/permissionFilterModal.tsx
Normal file
154
web/src/containers/Settings/permissionFilterModal.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, { Dispatch, SetStateAction } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField
|
||||
} from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import Autocomplete from '@mui/material/Autocomplete'
|
||||
|
||||
import { PermissionResponse } from '../../utils/types'
|
||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
||||
import { PrincipalType } from './permission'
|
||||
|
||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
padding: theme.spacing(1)
|
||||
}
|
||||
}))
|
||||
|
||||
type FilterModalProps = {
|
||||
open: boolean
|
||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||
permissions: PermissionResponse[]
|
||||
uriFilter: string[]
|
||||
setUriFilter: Dispatch<SetStateAction<string[]>>
|
||||
principalFilter: string[]
|
||||
setPrincipalFilter: Dispatch<SetStateAction<string[]>>
|
||||
principalTypeFilter: PrincipalType[]
|
||||
setPrincipalTypeFilter: Dispatch<SetStateAction<PrincipalType[]>>
|
||||
settingFilter: string[]
|
||||
setSettingFilter: Dispatch<SetStateAction<string[]>>
|
||||
applyFilter: () => void
|
||||
resetFilter: () => void
|
||||
}
|
||||
|
||||
const PermissionFilterModal = ({
|
||||
open,
|
||||
handleOpen,
|
||||
permissions,
|
||||
uriFilter,
|
||||
setUriFilter,
|
||||
principalFilter,
|
||||
setPrincipalFilter,
|
||||
principalTypeFilter,
|
||||
setPrincipalTypeFilter,
|
||||
settingFilter,
|
||||
setSettingFilter,
|
||||
applyFilter,
|
||||
resetFilter
|
||||
}: FilterModalProps) => {
|
||||
const URIs = permissions
|
||||
.map((permission) => permission.uri)
|
||||
.filter((uri, index, array) => array.indexOf(uri) === index)
|
||||
|
||||
// fetch all the principals from permissions array
|
||||
let principals = permissions.map((permission) => {
|
||||
if (permission.user) return permission.user.username
|
||||
if (permission.group) return permission.group.name
|
||||
return ''
|
||||
})
|
||||
|
||||
// removes empty strings
|
||||
principals = principals.filter((principal) => principal !== '')
|
||||
|
||||
// removes the duplicates
|
||||
principals = principals.filter(
|
||||
(principal, index, array) => array.indexOf(principal) === index
|
||||
)
|
||||
|
||||
return (
|
||||
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
||||
<BootstrapDialogTitle
|
||||
id="permission-filter-dialog-title"
|
||||
handleOpen={handleOpen}
|
||||
>
|
||||
Permission Filter
|
||||
</BootstrapDialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={URIs}
|
||||
filterSelectedOptions
|
||||
value={uriFilter}
|
||||
onChange={(event: any, newValue: string[]) => {
|
||||
setUriFilter(newValue)
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} label="URIs" />}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={principals}
|
||||
filterSelectedOptions
|
||||
value={principalFilter}
|
||||
onChange={(event: any, newValue: string[]) => {
|
||||
setPrincipalFilter(newValue)
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Principals" />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={Object.values(PrincipalType)}
|
||||
filterSelectedOptions
|
||||
value={principalTypeFilter}
|
||||
onChange={(event: any, newValue: PrincipalType[]) => {
|
||||
setPrincipalTypeFilter(newValue)
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Principal Type" />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={['Grant', 'Deny']}
|
||||
filterSelectedOptions
|
||||
value={settingFilter}
|
||||
onChange={(event: any, newValue: string[]) => {
|
||||
setSettingFilter(newValue)
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Settings" />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" color="error" onClick={resetFilter}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={applyFilter}>
|
||||
Apply
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</BootstrapDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default PermissionFilterModal
|
||||
84
web/src/containers/Settings/updatePermissionModal.tsx
Normal file
84
web/src/containers/Settings/updatePermissionModal.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useState, Dispatch, SetStateAction, useEffect } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField
|
||||
} from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import Autocomplete from '@mui/material/Autocomplete'
|
||||
|
||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
||||
|
||||
import { PermissionResponse } from '../../utils/types'
|
||||
|
||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
padding: theme.spacing(1)
|
||||
}
|
||||
}))
|
||||
|
||||
type UpdatePermissionModalProps = {
|
||||
open: boolean
|
||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||
permission: PermissionResponse | undefined
|
||||
updatePermission: (setting: string) => void
|
||||
}
|
||||
|
||||
const UpdatePermissionModal = ({
|
||||
open,
|
||||
handleOpen,
|
||||
permission,
|
||||
updatePermission
|
||||
}: UpdatePermissionModalProps) => {
|
||||
const [permissionSetting, setPermissionSetting] = useState('Grant')
|
||||
|
||||
useEffect(() => {
|
||||
if (permission) setPermissionSetting(permission.setting)
|
||||
}, [permission])
|
||||
|
||||
return (
|
||||
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
||||
<BootstrapDialogTitle
|
||||
id="add-permission-dialog-title"
|
||||
handleOpen={handleOpen}
|
||||
>
|
||||
Update Permission
|
||||
</BootstrapDialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
sx={{ width: 300 }}
|
||||
options={['Grant', 'Deny']}
|
||||
disableClearable
|
||||
value={permissionSetting}
|
||||
onChange={(event: any, newValue: string) =>
|
||||
setPermissionSetting(newValue)
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Settings" />
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => updatePermission(permissionSetting)}
|
||||
disabled={permission?.setting === permissionSetting}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</BootstrapDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdatePermissionModal
|
||||
@@ -2,13 +2,15 @@ import React, { useEffect, useRef, useState, useContext } from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
import {
|
||||
Backdrop,
|
||||
Box,
|
||||
MenuItem,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Button,
|
||||
Paper,
|
||||
Tab,
|
||||
Tooltip
|
||||
} from '@mui/material'
|
||||
@@ -48,7 +50,17 @@ const Studio = () => {
|
||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||
const [webout, setWebout] = useState('')
|
||||
const [tab, setTab] = useState('1')
|
||||
const [selectedRunTime, setSelectedRunTime] = useState(RunTimeType.SAS)
|
||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setRunTimes(Object.values(appContext.runTimes))
|
||||
}, [appContext.runTimes])
|
||||
|
||||
useEffect(() => {
|
||||
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
||||
}, [runTimes])
|
||||
|
||||
const handleTabChange = (_e: any, newValue: string) => {
|
||||
setTab(newValue)
|
||||
@@ -69,6 +81,7 @@ const Studio = () => {
|
||||
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
|
||||
|
||||
const runCode = (code: string) => {
|
||||
setIsRunning(true)
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||
.then((res: any) => {
|
||||
@@ -85,6 +98,7 @@ const Studio = () => {
|
||||
window.scrollTo(0, document.body.scrollHeight)
|
||||
})
|
||||
.catch((err) => console.log(err))
|
||||
.finally(() => setIsRunning(false))
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
@@ -153,7 +167,13 @@ const Studio = () => {
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
<TabPanel style={{ paddingBottom: 0 }} value="1">
|
||||
<TabPanel sx={{ paddingBottom: 0 }} value="1">
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={isRunning}
|
||||
>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
<div className={classes.subMenu}>
|
||||
<Tooltip title="CTRL+ENTER will also run SAS code">
|
||||
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
||||
@@ -174,8 +194,10 @@ const Studio = () => {
|
||||
value={selectedRunTime}
|
||||
onChange={handleChangeRunTime}
|
||||
>
|
||||
{appContext.runTimes.map((runTime) => (
|
||||
<MenuItem value={runTime}>{runTime}</MenuItem>
|
||||
{runTimes.map((runTime) => (
|
||||
<MenuItem key={runTime} value={runTime}>
|
||||
{runTime}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
@@ -29,6 +29,8 @@ interface AppContextProps {
|
||||
setUsername: Dispatch<SetStateAction<string>> | null
|
||||
displayName: string
|
||||
setDisplayName: Dispatch<SetStateAction<string>> | null
|
||||
isAdmin: boolean
|
||||
setIsAdmin: Dispatch<SetStateAction<boolean>> | null
|
||||
mode: ModeType
|
||||
runTimes: RunTimeType[]
|
||||
logout: (() => void) | null
|
||||
@@ -44,6 +46,8 @@ export const AppContext = createContext<AppContextProps>({
|
||||
setUsername: null,
|
||||
displayName: '',
|
||||
setDisplayName: null,
|
||||
isAdmin: false,
|
||||
setIsAdmin: null,
|
||||
mode: ModeType.Server,
|
||||
runTimes: [],
|
||||
logout: null
|
||||
@@ -56,6 +60,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
const [userId, setUserId] = useState(0)
|
||||
const [username, setUsername] = useState('')
|
||||
const [displayName, setDisplayName] = useState('')
|
||||
const [isAdmin, setIsAdmin] = useState(false)
|
||||
const [mode, setMode] = useState(ModeType.Server)
|
||||
const [runTimes, setRunTimes] = useState<RunTimeType[]>([])
|
||||
|
||||
@@ -70,6 +75,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
setUserId(data.id)
|
||||
setUsername(data.username)
|
||||
setDisplayName(data.displayName)
|
||||
setIsAdmin(data.isAdmin)
|
||||
setLoggedIn(true)
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -88,7 +94,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
}, [])
|
||||
|
||||
const logout = useCallback(() => {
|
||||
axios.get('/logout').then(() => {
|
||||
axios.get('/SASLogon/logout').then(() => {
|
||||
setLoggedIn(false)
|
||||
setUsername('')
|
||||
setDisplayName('')
|
||||
@@ -107,6 +113,8 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
setUsername,
|
||||
displayName,
|
||||
setDisplayName,
|
||||
isAdmin,
|
||||
setIsAdmin,
|
||||
mode,
|
||||
runTimes,
|
||||
logout
|
||||
|
||||
@@ -18,3 +18,10 @@ code {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.permissions-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: '5px 10px';
|
||||
margin-top: '10px';
|
||||
}
|
||||
|
||||
32
web/src/utils/types.ts
Normal file
32
web/src/utils/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export interface UserResponse {
|
||||
id: number
|
||||
username: string
|
||||
displayName: string
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
export interface GroupResponse {
|
||||
groupId: number
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface GroupDetailsResponse extends GroupResponse {
|
||||
isActive: boolean
|
||||
users: UserResponse[]
|
||||
}
|
||||
|
||||
export interface PermissionResponse {
|
||||
permissionId: number
|
||||
uri: string
|
||||
setting: string
|
||||
user?: UserResponse
|
||||
group?: GroupDetailsResponse
|
||||
}
|
||||
|
||||
export interface RegisterPermissionPayload {
|
||||
uri: string
|
||||
setting: string
|
||||
principalType: string
|
||||
principalId: number
|
||||
}
|
||||
Reference in New Issue
Block a user