mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b234eb2b1 | ||
|
|
ef25eec11f | ||
| 63dd6813c0 | |||
| 299512135d | |||
|
|
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 | ||
| e516b7716d | |||
| f3dfc7083f | |||
| 7d916ec3e9 | |||
| 70f279a49c | |||
| 66a3537271 | |||
| ca64c13909 | |||
| 0a73a35547 | |||
| a75edbaa32 | |||
| 4ddfec0403 | |||
| 35439d7d51 | |||
| 907aa485fd | |||
| 888627e1c8 | |||
| 9cb9e2dd33 | |||
| 54d4bf835d | |||
| 67fe298fd5 | |||
| 97ecfdc955 | |||
| 5b319f9ad1 | |||
| be8635ccc5 | |||
| f863b81a7d | |||
| bdf63df1d9 | |||
| 4c6b9c5e93 | |||
|
|
a2d1396057 | ||
|
|
b2f21eb3ac | ||
| fa63dc071b | |||
| e8c21a43b2 | |||
| 1413b18508 | |||
| dfbd155711 | |||
| 4fcc191ce9 | |||
| d000f7508f | |||
| 5652325452 | |||
| 0781ddd64e | |||
| 7be77cc38a | |||
| 98b8a75148 | |||
| 72a3197a06 | |||
| fce05d6959 | |||
| 1aec3abd28 | |||
| 9136c95013 | |||
|
|
89b32e70ff | ||
| 01713440a4 | |||
| 540f54fb77 | |||
| bf906aa544 | |||
| 797c2bcc39 | |||
| 1103ffe07b | |||
| e5200c1000 | |||
| 38a7db8514 | |||
| 39fc908de1 | |||
| be009d5b02 | |||
| 6bea1f7666 |
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [sasjs]
|
||||||
80
CHANGELOG.md
80
CHANGELOG.md
@@ -1,3 +1,83 @@
|
|||||||
|
## [0.11.3](https://github.com/sasjs/server/compare/v0.11.2...v0.11.3) (2022-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* filePath fix in code.js file for windows ([2995121](https://github.com/sasjs/server/commit/299512135d77c2ac9e34853cf35aee6f2e1d4da4))
|
||||||
|
|
||||||
|
## [0.11.2](https://github.com/sasjs/server/compare/v0.11.1...v0.11.2) (2022-07-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* apply icon option only for sas.exe ([d2ddd8a](https://github.com/sasjs/server/commit/d2ddd8aacadfdd143026881f2c6ae8c6b277610a))
|
||||||
|
|
||||||
|
## [0.11.1](https://github.com/sasjs/server/compare/v0.11.0...v0.11.1) (2022-07-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bank operator ([aa02741](https://github.com/sasjs/server/commit/aa027414ed3ce51f1014ef36c4191e064b2e963d))
|
||||||
|
* ensuring nosplash option only applies for sas.exe ([65e6de9](https://github.com/sasjs/server/commit/65e6de966383fe49a919b1f901d77c7f1e402c9b)), closes [#229](https://github.com/sasjs/server/issues/229)
|
||||||
|
|
||||||
|
# [0.11.0](https://github.com/sasjs/server/compare/v0.10.0...v0.11.0) (2022-07-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **logs:** logs location is configurable ([e024a92](https://github.com/sasjs/server/commit/e024a92f165990e08db8aa26ee326dbcb30e2e46))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **logs:** logs to file with rotating + code split into files ([92fda18](https://github.com/sasjs/server/commit/92fda183f3f0f3956b7c791669eb8dd52c389d1b))
|
||||||
|
|
||||||
|
# [0.10.0](https://github.com/sasjs/server/compare/v0.9.0...v0.10.0) (2022-07-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add authorize middleware for appStreams ([e54a09d](https://github.com/sasjs/server/commit/e54a09db19ec8690e54a40760531a4e06d250974))
|
||||||
|
* add isAdmin attribute to return response of get session and login requests ([bdf63df](https://github.com/sasjs/server/commit/bdf63df1d915892486005ec904807749786b1c0c))
|
||||||
|
* add permission authorization middleware to only specific routes ([f3dfc70](https://github.com/sasjs/server/commit/f3dfc7083fbfb4b447521341b1a86730fb90b4c0))
|
||||||
|
* bumping core and running lint ([a2d1396](https://github.com/sasjs/server/commit/a2d13960578014312d2cb5e03145bfd1829d99ec))
|
||||||
|
* controller fixed for deleting permission ([b5f595a](https://github.com/sasjs/server/commit/b5f595a25c50550d62482409353c7629c5a5c3e0))
|
||||||
|
* do not show admin users in add permission modal ([a75edba](https://github.com/sasjs/server/commit/a75edbaa327ec2af49523c13996ac283061da7d8))
|
||||||
|
* export GroupResponse interface ([38a7db8](https://github.com/sasjs/server/commit/38a7db8514de0acd94d74ba96bc1efb732add30c))
|
||||||
|
* move permission filter modal to separate file and icons for different actions ([d000f75](https://github.com/sasjs/server/commit/d000f7508f6d7384afffafee4179151fca802ca8))
|
||||||
|
* principalId type changed to number from any ([4fcc191](https://github.com/sasjs/server/commit/4fcc191ce9edc7e4dcd8821fb8019f4eea5db4ea))
|
||||||
|
* remove clientId from principal types ([0781ddd](https://github.com/sasjs/server/commit/0781ddd64e3b5e5ca39647bb4e4e1a9332a0f4f8))
|
||||||
|
* remove duplicates principals from permission filter modal ([5b319f9](https://github.com/sasjs/server/commit/5b319f9ad1f941b306db6b9473a2128b2e42bf76))
|
||||||
|
* show loading spinner in studio while executing code ([496247d](https://github.com/sasjs/server/commit/496247d0b9975097a008cf4d3a999d77648fd930))
|
||||||
|
* show permission component only in server mode ([f863b81](https://github.com/sasjs/server/commit/f863b81a7d40a1296a061ec93946f204382af2c3))
|
||||||
|
* update permission model ([39fc908](https://github.com/sasjs/server/commit/39fc908de1945f2aaea18d14e6bce703f6bf0c06))
|
||||||
|
* update permission response ([e516b77](https://github.com/sasjs/server/commit/e516b7716da5ff7e23350a5f77cfa073b1171175))
|
||||||
|
* **web:** only admin should be able to add, update or delete permission ([be8635c](https://github.com/sasjs/server/commit/be8635ccc5eb34c3f0a5951c8a0421292ef69c97))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add api endpoint for deleting permission ([0171344](https://github.com/sasjs/server/commit/01713440a4fa661b76368785c0ca731f096ac70a))
|
||||||
|
* add api endpoint for updating permission setting ([540f54f](https://github.com/sasjs/server/commit/540f54fb77b364822da7889dbe75c02242f48a59))
|
||||||
|
* add authorize middleware for validating permissions ([7d916ec](https://github.com/sasjs/server/commit/7d916ec3e9ef579dde1b73015715cd01098c2018))
|
||||||
|
* add basic UI for settings and permissions ([5652325](https://github.com/sasjs/server/commit/56523254525a66e756196e90b39a2b8cdadc1518))
|
||||||
|
* add documentation link under usename dropdown menu ([eeb63b3](https://github.com/sasjs/server/commit/eeb63b330c292afcdd5c8f006882b224c4235068))
|
||||||
|
* add permission model ([6bea1f7](https://github.com/sasjs/server/commit/6bea1f76668ddb070ad95b3e02c31238af67c346))
|
||||||
|
* add UI for updating permission ([e8c21a4](https://github.com/sasjs/server/commit/e8c21a43b215f5fced0463b70747cda1191a4e01))
|
||||||
|
* add validation for registering permission ([e5200c1](https://github.com/sasjs/server/commit/e5200c1000903185dfad9ee49c99583e473c4388))
|
||||||
|
* add, remove and update permissions from web component ([97ecfdc](https://github.com/sasjs/server/commit/97ecfdc95563c72dbdecaebcb504e5194250a763))
|
||||||
|
* added get authorizedRoutes api endpoint ([b10e932](https://github.com/sasjs/server/commit/b10e9326058193dd65a57fab2d2f05b7b06096e7))
|
||||||
|
* created modal for adding permission ([1413b18](https://github.com/sasjs/server/commit/1413b1850838ecc988ab289da4541bde36a9a346))
|
||||||
|
* defined register permission and get all permissions api endpoints ([1103ffe](https://github.com/sasjs/server/commit/1103ffe07b88496967cb03683b08f058ca3bbb9f))
|
||||||
|
* update swagger docs ([797c2bc](https://github.com/sasjs/server/commit/797c2bcc39005a05a995be15a150d584fecae259))
|
||||||
|
|
||||||
|
# [0.9.0](https://github.com/sasjs/server/compare/v0.8.3...v0.9.0) (2022-07-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* removed secrets from env variables ([9c3da56](https://github.com/sasjs/server/commit/9c3da56901672a818f54267f9defc9f4701ab7fb))
|
||||||
|
|
||||||
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)
|
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -105,10 +105,6 @@ CERT_CHAIN=certificate.pem (required)
|
|||||||
CA_ROOT=fullchain.pem (optional)
|
CA_ROOT=fullchain.pem (optional)
|
||||||
|
|
||||||
# ENV variables required for MODE: `server`
|
# ENV variables required for MODE: `server`
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
|
||||||
REFRESH_TOKEN_SECRET=<secret>
|
|
||||||
AUTH_CODE_SECRET=<secret>
|
|
||||||
SESSION_SECRET=<secret>
|
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||||
@@ -140,6 +136,9 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json
|
|||||||
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
||||||
LOG_FORMAT_MORGAN=
|
LOG_FORMAT_MORGAN=
|
||||||
|
|
||||||
|
# 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.
|
# A comma separated string that defines the available runTimes.
|
||||||
# Priority is given to the runtime that comes first in the string.
|
# Priority is given to the runtime that comes first in the string.
|
||||||
# Possible options at the moment are sas and js
|
# Possible options at the moment are sas and js
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ PORT=[5000] default value is 5000
|
|||||||
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
||||||
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
||||||
|
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
|
||||||
REFRESH_TOKEN_SECRET=<secret>
|
|
||||||
AUTH_CODE_SECRET=<secret>
|
|
||||||
SESSION_SECRET=<secret>
|
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
|
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
|
||||||
@@ -24,4 +20,5 @@ NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
|||||||
|
|
||||||
SASJS_ROOT=./sasjs_root
|
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",
|
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
|
||||||
"prestart": "npm run initial",
|
"prestart": "npm run initial",
|
||||||
"prebuild": "npm run initial",
|
"prebuild": "npm run initial",
|
||||||
"start": "nodemon ./src/server.ts",
|
"start": "NODE_ENV=development nodemon ./src/server.ts",
|
||||||
"start:prod": "node ./build/src/server.js",
|
"start:prod": "node ./build/src/server.js",
|
||||||
"build": "rimraf build && tsc",
|
"build": "rimraf build && tsc",
|
||||||
"postbuild": "npm run copy:files",
|
"postbuild": "npm run copy:files",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
},
|
},
|
||||||
"author": "4GL Ltd",
|
"author": "4GL Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^4.27.3",
|
"@sasjs/core": "^4.31.3",
|
||||||
"@sasjs/utils": "2.42.1",
|
"@sasjs/utils": "2.42.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"connect-mongo": "^4.6.0",
|
"connect-mongo": "^4.6.0",
|
||||||
@@ -63,6 +63,7 @@
|
|||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
|
"rotating-file-stream": "^3.0.4",
|
||||||
"swagger-ui-express": "4.3.0",
|
"swagger-ui-express": "4.3.0",
|
||||||
"unzipper": "^0.10.11",
|
"unzipper": "^0.10.11",
|
||||||
"url": "^0.10.3"
|
"url": "^0.10.3"
|
||||||
|
|||||||
@@ -47,41 +47,6 @@ components:
|
|||||||
- userId
|
- userId
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
LoginPayload:
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: 'Username for user'
|
|
||||||
example: secretuser
|
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
description: 'Password for user'
|
|
||||||
example: secretpassword
|
|
||||||
required:
|
|
||||||
- username
|
|
||||||
- password
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
AuthorizeResponse:
|
|
||||||
properties:
|
|
||||||
code:
|
|
||||||
type: string
|
|
||||||
description: 'Authorization code'
|
|
||||||
example: someRandomCryptoString
|
|
||||||
required:
|
|
||||||
- code
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
AuthorizePayload:
|
|
||||||
properties:
|
|
||||||
clientId:
|
|
||||||
type: string
|
|
||||||
description: 'Client ID'
|
|
||||||
example: clientID1
|
|
||||||
required:
|
|
||||||
- clientId
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
ClientPayload:
|
ClientPayload:
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
clientId:
|
||||||
@@ -314,10 +279,13 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
displayName:
|
displayName:
|
||||||
type: string
|
type: string
|
||||||
|
isAdmin:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- username
|
- username
|
||||||
- displayName
|
- displayName
|
||||||
|
- isAdmin
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
GroupResponse:
|
GroupResponse:
|
||||||
@@ -440,13 +408,13 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__:
|
Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__:
|
||||||
properties:
|
properties:
|
||||||
id:
|
|
||||||
description: 'The string version of this documents _id.'
|
|
||||||
_id:
|
_id:
|
||||||
$ref: '#/components/schemas/_LeanDocument__LeanDocument_T__'
|
$ref: '#/components/schemas/_LeanDocument__LeanDocument_T__'
|
||||||
description: 'This documents _id.'
|
description: 'This documents _id.'
|
||||||
__v:
|
__v:
|
||||||
description: 'This documents __v.'
|
description: 'This documents __v.'
|
||||||
|
id:
|
||||||
|
description: 'The string version of this documents _id.'
|
||||||
type: object
|
type: object
|
||||||
description: 'From T, pick a set of properties whose keys are in the union K'
|
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_:
|
Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_:
|
||||||
@@ -480,6 +448,16 @@ components:
|
|||||||
- runTimes
|
- runTimes
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
AuthorizedRoutesResponse:
|
||||||
|
properties:
|
||||||
|
URIs:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- URIs
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
ExecuteReturnJsonPayload:
|
ExecuteReturnJsonPayload:
|
||||||
properties:
|
properties:
|
||||||
_program:
|
_program:
|
||||||
@@ -488,6 +466,106 @@ components:
|
|||||||
example: /Public/somefolder/some.file
|
example: /Public/somefolder/some.file
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
LoginPayload:
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: 'Username for user'
|
||||||
|
example: secretuser
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
description: 'Password for user'
|
||||||
|
example: secretpassword
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
AuthorizeResponse:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: 'Authorization code'
|
||||||
|
example: someRandomCryptoString
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
AuthorizePayload:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
type: string
|
||||||
|
description: 'Client ID'
|
||||||
|
example: clientID1
|
||||||
|
required:
|
||||||
|
- clientId
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
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:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
type: http
|
type: http
|
||||||
@@ -558,86 +636,6 @@ paths:
|
|||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
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'
|
|
||||||
/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/client:
|
/SASjsApi/client:
|
||||||
post:
|
post:
|
||||||
operationId: CreateClient
|
operationId: CreateClient
|
||||||
@@ -993,7 +991,7 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: [{id: 123, username: johnusername, displayName: John}, {id: 456, username: starkusername, displayName: Stark}]
|
value: [{id: 123, username: johnusername, displayName: John, isAdmin: false}, {id: 456, username: starkusername, displayName: Stark, isAdmin: true}]
|
||||||
summary: 'Get list of all users (username, displayname). All users can request this.'
|
summary: 'Get list of all users (username, displayname). All users can request this.'
|
||||||
tags:
|
tags:
|
||||||
- User
|
- User
|
||||||
@@ -1422,6 +1420,24 @@ paths:
|
|||||||
- Info
|
- Info
|
||||||
security: []
|
security: []
|
||||||
parameters: []
|
parameters: []
|
||||||
|
/SASjsApi/info/authorizedRoutes:
|
||||||
|
get:
|
||||||
|
operationId: AuthorizedRoutes
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthorizedRoutesResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {URIs: [/AppStream, /SASjsApi/stp/execute]}
|
||||||
|
summary: 'Get authorized routes.'
|
||||||
|
tags:
|
||||||
|
- Info
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
/SASjsApi/session:
|
/SASjsApi/session:
|
||||||
get:
|
get:
|
||||||
operationId: Session
|
operationId: Session
|
||||||
@@ -1434,7 +1450,7 @@ paths:
|
|||||||
$ref: '#/components/schemas/UserResponse'
|
$ref: '#/components/schemas/UserResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {id: 123, username: johnusername, displayName: John}
|
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
|
||||||
summary: 'Get session info (username).'
|
summary: 'Get session info (username).'
|
||||||
tags:
|
tags:
|
||||||
- Session
|
- Session
|
||||||
@@ -1504,19 +1520,205 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
$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:
|
servers:
|
||||||
-
|
-
|
||||||
url: /
|
url: /
|
||||||
tags:
|
tags:
|
||||||
-
|
-
|
||||||
name: Info
|
name: Info
|
||||||
description: 'Get Server Info'
|
description: 'Get Server Information'
|
||||||
-
|
-
|
||||||
name: Session
|
name: Session
|
||||||
description: 'Get Session information'
|
description: 'Get Session information'
|
||||||
-
|
-
|
||||||
name: User
|
name: User
|
||||||
description: 'Operations about users'
|
description: 'Operations with users'
|
||||||
|
-
|
||||||
|
name: Permission
|
||||||
|
description: 'Operations about permissions'
|
||||||
-
|
-
|
||||||
name: Client
|
name: Client
|
||||||
description: 'Operations about clients'
|
description: 'Operations about clients'
|
||||||
@@ -1525,16 +1727,16 @@ tags:
|
|||||||
description: 'Operations about auth'
|
description: 'Operations about auth'
|
||||||
-
|
-
|
||||||
name: Drive
|
name: Drive
|
||||||
description: 'Operations about drive'
|
description: 'Operations on SASjs Drive'
|
||||||
-
|
-
|
||||||
name: Group
|
name: Group
|
||||||
description: 'Operations about group'
|
description: 'Operations on groups and group memberships'
|
||||||
-
|
-
|
||||||
name: STP
|
name: STP
|
||||||
description: 'Operations about STP'
|
description: 'Execution of Stored Programs'
|
||||||
-
|
-
|
||||||
name: CODE
|
name: CODE
|
||||||
description: 'Operations on SAS code'
|
description: 'Execution of code (various runtimes are supported)'
|
||||||
-
|
-
|
||||||
name: Web
|
name: Web
|
||||||
description: 'Operations on Web'
|
description: 'Operations on Web'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
readFile,
|
readFile,
|
||||||
SASJsFileType
|
SASJsFileType
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
import { apiRoot, sysInitCompiledPath } from '../src/utils'
|
import { apiRoot, sysInitCompiledPath } from '../src/utils/file'
|
||||||
|
|
||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import {
|
|||||||
listFilesInFolder
|
listFilesInFolder
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
|
import {
|
||||||
|
apiRoot,
|
||||||
|
sasJSCoreMacros,
|
||||||
|
sasJSCoreMacrosInfo
|
||||||
|
} from '../src/utils/file'
|
||||||
|
|
||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
|
|||||||
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 path from 'path'
|
||||||
import express, { ErrorRequestHandler } from 'express'
|
import express, { ErrorRequestHandler } from 'express'
|
||||||
import csrf from 'csurf'
|
import csrf from 'csurf'
|
||||||
import session from 'express-session'
|
|
||||||
import MongoStore from 'connect-mongo'
|
|
||||||
import morgan from 'morgan'
|
|
||||||
import cookieParser from 'cookie-parser'
|
import cookieParser from 'cookie-parser'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import cors from 'cors'
|
|
||||||
import helmet from 'helmet'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
connectDB,
|
|
||||||
copySASjsCore,
|
copySASjsCore,
|
||||||
CorsType,
|
|
||||||
getWebBuildFolder,
|
getWebBuildFolder,
|
||||||
HelmetCoepType,
|
|
||||||
instantiateLogger,
|
instantiateLogger,
|
||||||
loadAppStreamConfig,
|
loadAppStreamConfig,
|
||||||
ModeType,
|
|
||||||
ProtocolType,
|
ProtocolType,
|
||||||
ReturnCode,
|
ReturnCode,
|
||||||
setProcessVariables,
|
setProcessVariables,
|
||||||
setupFolders,
|
setupFolders,
|
||||||
verifyEnvVariables
|
verifyEnvVariables
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
|
import {
|
||||||
|
configureCors,
|
||||||
|
configureExpressSession,
|
||||||
|
configureLogger,
|
||||||
|
configureSecurity
|
||||||
|
} from './app-modules'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
@@ -34,19 +30,7 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
|||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.use(cookieParser())
|
const { PROTOCOL } = process.env
|
||||||
|
|
||||||
const {
|
|
||||||
MODE,
|
|
||||||
CORS,
|
|
||||||
WHITELIST,
|
|
||||||
PROTOCOL,
|
|
||||||
HELMET_CSP_CONFIG_PATH,
|
|
||||||
HELMET_COEP,
|
|
||||||
LOG_FORMAT_MORGAN
|
|
||||||
} = process.env
|
|
||||||
|
|
||||||
app.use(morgan(LOG_FORMAT_MORGAN as string))
|
|
||||||
|
|
||||||
export const cookieOptions = {
|
export const cookieOptions = {
|
||||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||||
@@ -54,79 +38,11 @@ export const cookieOptions = {
|
|||||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||||
}
|
}
|
||||||
|
|
||||||
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
|
||||||
HELMET_CSP_CONFIG_PATH
|
|
||||||
)
|
|
||||||
if (PROTOCOL === ProtocolType.HTTP)
|
|
||||||
cspConfigJson['upgrade-insecure-requests'] = null
|
|
||||||
|
|
||||||
/***********************************
|
/***********************************
|
||||||
* CSRF Protection *
|
* CSRF Protection *
|
||||||
***********************************/
|
***********************************/
|
||||||
export const csrfProtection = csrf({ cookie: cookieOptions })
|
export const csrfProtection = csrf({ cookie: cookieOptions })
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* Handle security and origin *
|
|
||||||
***********************************/
|
|
||||||
app.use(
|
|
||||||
helmet({
|
|
||||||
contentSecurityPolicy: {
|
|
||||||
directives: {
|
|
||||||
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
|
||||||
...cspConfigJson
|
|
||||||
}
|
|
||||||
},
|
|
||||||
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* Enabling CORS *
|
|
||||||
***********************************/
|
|
||||||
if (CORS === CorsType.ENABLED) {
|
|
||||||
const whiteList: string[] = []
|
|
||||||
WHITELIST?.split(' ')
|
|
||||||
?.filter((url) => !!url)
|
|
||||||
.forEach((url) => {
|
|
||||||
if (url.startsWith('http'))
|
|
||||||
// removing trailing slash of URLs listing for CORS
|
|
||||||
whiteList.push(url.replace(/\/$/, ''))
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('All CORS Requests are enabled for:', whiteList)
|
|
||||||
app.use(cors({ credentials: true, origin: whiteList }))
|
|
||||||
}
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* DB Connection & *
|
|
||||||
* Express Sessions *
|
|
||||||
* With Mongo Store *
|
|
||||||
***********************************/
|
|
||||||
if (MODE === ModeType.Server) {
|
|
||||||
let store: MongoStore | undefined
|
|
||||||
|
|
||||||
// NOTE: when exporting app.js as agent for supertest
|
|
||||||
// we should exclude connecting to the real database
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
|
||||||
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
|
|
||||||
|
|
||||||
store = MongoStore.create({ clientPromise, collectionName: 'sessions' })
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
session({
|
|
||||||
secret: process.env.SESSION_SECRET as string,
|
|
||||||
saveUninitialized: false, // don't create session until something stored
|
|
||||||
resave: false, //don't save session if unmodified
|
|
||||||
store,
|
|
||||||
cookie: cookieOptions
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(express.json({ limit: '100mb' }))
|
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
|
||||||
|
|
||||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
if (err.code === 'EBADCSRFTOKEN')
|
if (err.code === 'EBADCSRFTOKEN')
|
||||||
return res.status(400).send('Invalid CSRF token!')
|
return res.status(400).send('Invalid CSRF token!')
|
||||||
@@ -136,6 +52,30 @@ const onError: ErrorRequestHandler = (err, req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default setProcessVariables().then(async () => {
|
export default setProcessVariables().then(async () => {
|
||||||
|
app.use(cookieParser())
|
||||||
|
|
||||||
|
configureLogger(app)
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Handle security and origin *
|
||||||
|
***********************************/
|
||||||
|
configureSecurity(app)
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Enabling CORS *
|
||||||
|
***********************************/
|
||||||
|
configureCors(app)
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* DB Connection & *
|
||||||
|
* Express Sessions *
|
||||||
|
* With Mongo Store *
|
||||||
|
***********************************/
|
||||||
|
configureExpressSession(app)
|
||||||
|
|
||||||
|
app.use(express.json({ limit: '100mb' }))
|
||||||
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
await setupFolders()
|
await setupFolders()
|
||||||
await copySASjsCore()
|
await copySASjsCore()
|
||||||
|
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ const verifyAuthCode = async (
|
|||||||
clientId: string,
|
clientId: string,
|
||||||
code: string
|
code: string
|
||||||
): Promise<InfoJWT | undefined> => {
|
): Promise<InfoJWT | undefined> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
|
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
|
||||||
if (err) return resolve(undefined)
|
if (err) return resolve(undefined)
|
||||||
|
|
||||||
const clientInfo: InfoJWT = {
|
const clientInfo: InfoJWT = {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface GroupResponse {
|
|||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroupDetailsResponse {
|
export interface GroupDetailsResponse {
|
||||||
groupId: number
|
groupId: number
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
@@ -198,7 +198,7 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
|
|||||||
'groupId name description isActive users -_id'
|
'groupId name description isActive users -_id'
|
||||||
).populate(
|
).populate(
|
||||||
'users',
|
'users',
|
||||||
'id username displayName -_id'
|
'id username displayName isAdmin -_id'
|
||||||
)) as unknown as GroupDetailsResponse
|
)) as unknown as GroupDetailsResponse
|
||||||
if (!group)
|
if (!group)
|
||||||
throw {
|
throw {
|
||||||
@@ -249,9 +249,10 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'User not found.'
|
message: 'User not found.'
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedGroup = (action === 'addUser'
|
const updatedGroup =
|
||||||
? await group.addUser(user._id)
|
action === 'addUser'
|
||||||
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
|
? await group.addUser(user)
|
||||||
|
: await group.removeUser(user)
|
||||||
|
|
||||||
if (!updatedGroup)
|
if (!updatedGroup)
|
||||||
throw {
|
throw {
|
||||||
@@ -260,9 +261,6 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'Unable to update group.'
|
message: 'Unable to update group.'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'addUser') user.addGroup(group._id)
|
|
||||||
else user.removeGroup(group._id)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupId: updatedGroup.groupId,
|
groupId: updatedGroup.groupId,
|
||||||
name: updatedGroup.name,
|
name: updatedGroup.name,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export * from './code'
|
|||||||
export * from './drive'
|
export * from './drive'
|
||||||
export * from './group'
|
export * from './group'
|
||||||
export * from './info'
|
export * from './info'
|
||||||
|
export * from './permission'
|
||||||
export * from './session'
|
export * from './session'
|
||||||
export * from './stp'
|
export * from './stp'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Route, Tags, Example, Get } from 'tsoa'
|
import { Route, Tags, Example, Get } from 'tsoa'
|
||||||
|
import { getAuthorizedRoutes } from '../utils'
|
||||||
|
export interface AuthorizedRoutesResponse {
|
||||||
|
URIs: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface InfoResponse {
|
export interface InfoResponse {
|
||||||
mode: string
|
mode: string
|
||||||
@@ -36,4 +40,19 @@ export class InfoController {
|
|||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Get authorized routes.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<AuthorizedRoutesResponse>({
|
||||||
|
URIs: ['/AppStream', '/SASjsApi/stp/execute']
|
||||||
|
})
|
||||||
|
@Get('/authorizedRoutes')
|
||||||
|
public authorizedRoutes(): AuthorizedRoutesResponse {
|
||||||
|
const response = {
|
||||||
|
URIs: getAuthorizedRoutes()
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
isWindows() ? '-nosplash' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||||
isWindows() ? '-icon' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||||
isWindows() ? '-nologo' : ''
|
isWindows() ? '-nologo' : ''
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ let _webout = '';
|
|||||||
const weboutPath = '${
|
const weboutPath = '${
|
||||||
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
||||||
}';
|
}';
|
||||||
const _sasjs_tokenfile = '${tokenFile}';
|
const _sasjs_tokenfile = '${
|
||||||
|
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||||
|
}';
|
||||||
const _sasjs_username = '${preProgramVariables?.username}';
|
const _sasjs_username = '${preProgramVariables?.username}';
|
||||||
const _sasjs_userid = '${preProgramVariables?.userId}';
|
const _sasjs_userid = '${preProgramVariables?.userId}';
|
||||||
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
||||||
|
|||||||
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>({
|
@Example<UserResponse>({
|
||||||
id: 123,
|
id: 123,
|
||||||
username: 'johnusername',
|
username: 'johnusername',
|
||||||
displayName: 'John'
|
displayName: 'John',
|
||||||
|
isAdmin: false
|
||||||
})
|
})
|
||||||
@Get('/')
|
@Get('/')
|
||||||
public async session(
|
public async session(
|
||||||
@@ -26,5 +27,6 @@ export class SessionController {
|
|||||||
const session = (req: express.Request) => ({
|
const session = (req: express.Request) => ({
|
||||||
id: req.user!.userId,
|
id: req.user!.userId,
|
||||||
username: req.user!.username,
|
username: req.user!.username,
|
||||||
displayName: req.user!.displayName
|
displayName: req.user!.displayName,
|
||||||
|
isAdmin: req.user!.isAdmin
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ export interface UserResponse {
|
|||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
isAdmin: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserDetailsResponse {
|
export interface UserDetailsResponse {
|
||||||
id: number
|
id: number
|
||||||
displayName: string
|
displayName: string
|
||||||
username: string
|
username: string
|
||||||
@@ -48,12 +49,14 @@ export class UserController {
|
|||||||
{
|
{
|
||||||
id: 123,
|
id: 123,
|
||||||
username: 'johnusername',
|
username: 'johnusername',
|
||||||
displayName: 'John'
|
displayName: 'John',
|
||||||
|
isAdmin: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 456,
|
id: 456,
|
||||||
username: 'starkusername',
|
username: 'starkusername',
|
||||||
displayName: 'Stark'
|
displayName: 'Stark',
|
||||||
|
isAdmin: true
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@Get('/')
|
@Get('/')
|
||||||
@@ -200,7 +203,7 @@ export class UserController {
|
|||||||
|
|
||||||
const getAllUsers = async (): Promise<UserResponse[]> =>
|
const getAllUsers = async (): Promise<UserResponse[]> =>
|
||||||
await User.find({})
|
await User.find({})
|
||||||
.select({ _id: 0, id: 1, username: 1, displayName: 1 })
|
.select({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 })
|
||||||
.exec()
|
.exec()
|
||||||
|
|
||||||
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ const login = async (
|
|||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName
|
displayName: user.displayName,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { csrfProtection } from '../app'
|
import { csrfProtection } from '../app'
|
||||||
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
|
import {
|
||||||
|
fetchLatestAutoExec,
|
||||||
|
ModeType,
|
||||||
|
verifyTokenInDB,
|
||||||
|
isAuthorizingRoute
|
||||||
|
} from '../utils'
|
||||||
import { desktopUser } from './desktop'
|
import { desktopUser } from './desktop'
|
||||||
|
import { authorize } from './authorize'
|
||||||
|
|
||||||
export const authenticateAccessToken: RequestHandler = async (
|
export const authenticateAccessToken: RequestHandler = async (
|
||||||
req,
|
req,
|
||||||
@@ -15,6 +21,10 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextFunction = isAuthorizingRoute(req)
|
||||||
|
? () => authorize(req, res, next)
|
||||||
|
: next
|
||||||
|
|
||||||
// if request is coming from web and has valid session
|
// if request is coming from web and has valid session
|
||||||
// it can be validated.
|
// it can be validated.
|
||||||
if (req.session?.loggedIn) {
|
if (req.session?.loggedIn) {
|
||||||
@@ -24,7 +34,7 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
if (user) {
|
if (user) {
|
||||||
if (user.isActive) {
|
if (user.isActive) {
|
||||||
req.user = user
|
req.user = user
|
||||||
return csrfProtection(req, res, next)
|
return csrfProtection(req, res, nextFunction)
|
||||||
} else return res.sendStatus(401)
|
} else return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,8 +44,8 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
authenticateToken(
|
authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next,
|
nextFunction,
|
||||||
process.env.ACCESS_TOKEN_SECRET as string,
|
process.secrets.ACCESS_TOKEN_SECRET,
|
||||||
'accessToken'
|
'accessToken'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -45,7 +55,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
|||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next,
|
next,
|
||||||
process.env.REFRESH_TOKEN_SECRET as string,
|
process.secrets.REFRESH_TOKEN_SECRET,
|
||||||
'refreshToken'
|
'refreshToken'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -58,7 +68,7 @@ const authenticateToken = (
|
|||||||
tokenType: 'accessToken' | 'refreshToken'
|
tokenType: 'accessToken' | 'refreshToken'
|
||||||
) => {
|
) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server') {
|
if (MODE === ModeType.Desktop) {
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: 1234,
|
userId: 1234,
|
||||||
clientId: 'desktopModeClientId',
|
clientId: 'desktopModeClientId',
|
||||||
|
|||||||
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 './desktop'
|
||||||
export * from './verifyAdmin'
|
export * from './verifyAdmin'
|
||||||
export * from './verifyAdminIfNeeded'
|
export * from './verifyAdminIfNeeded'
|
||||||
|
export * from './authorize'
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { RequestHandler } from 'express'
|
import { RequestHandler } from 'express'
|
||||||
|
import { ModeType } from '../utils'
|
||||||
|
|
||||||
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server') return next()
|
if (MODE === ModeType.Desktop) return next()
|
||||||
|
|
||||||
const { user } = req
|
const { user } = req
|
||||||
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
||||||
|
|||||||
45
api/src/model/Configuration.ts
Normal file
45
api/src/model/Configuration.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import mongoose, { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
export interface ConfigurationType {
|
||||||
|
/**
|
||||||
|
* SecretOrPrivateKey to sign Access Token
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
ACCESS_TOKEN_SECRET: string
|
||||||
|
/**
|
||||||
|
* SecretOrPrivateKey to sign Refresh Token
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
REFRESH_TOKEN_SECRET: string
|
||||||
|
/**
|
||||||
|
* SecretOrPrivateKey to sign Auth Code
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
AUTH_CODE_SECRET: string
|
||||||
|
/**
|
||||||
|
* Secret used to sign the session cookie
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
SESSION_SECRET: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfigurationSchema = new Schema<ConfigurationType>({
|
||||||
|
ACCESS_TOKEN_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
REFRESH_TOKEN_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
AUTH_CODE_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
SESSION_SECRET: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mongoose.model('Configuration', ConfigurationSchema)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||||
import User from './User'
|
import { GroupDetailsResponse } from '../controllers'
|
||||||
|
import User, { IUser } from './User'
|
||||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||||
|
|
||||||
export interface GroupPayload {
|
export interface GroupPayload {
|
||||||
@@ -27,8 +28,9 @@ interface IGroupDocument extends GroupPayload, Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IGroup extends IGroupDocument {
|
interface IGroup extends IGroupDocument {
|
||||||
addUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
addUser(user: IUser): Promise<GroupDetailsResponse>
|
||||||
removeUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
removeUser(user: IUser): Promise<GroupDetailsResponse>
|
||||||
|
hasUser(user: IUser): boolean
|
||||||
}
|
}
|
||||||
interface IGroupModel extends Model<IGroup> {}
|
interface IGroupModel extends Model<IGroup> {}
|
||||||
|
|
||||||
@@ -70,28 +72,31 @@ groupSchema.pre('remove', async function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Instance Methods
|
// Instance Methods
|
||||||
groupSchema.method(
|
groupSchema.method('addUser', async function (user: IUser) {
|
||||||
'addUser',
|
const userObjectId = user._id
|
||||||
async function (userObjectId: Schema.Types.ObjectId) {
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
if (userIdIndex === -1) {
|
||||||
if (userIdIndex === -1) {
|
this.users.push(userObjectId)
|
||||||
this.users.push(userObjectId)
|
user.addGroup(this._id)
|
||||||
}
|
|
||||||
this.markModified('users')
|
|
||||||
return this.save()
|
|
||||||
}
|
}
|
||||||
)
|
this.markModified('users')
|
||||||
groupSchema.method(
|
return this.save()
|
||||||
'removeUser',
|
})
|
||||||
async function (userObjectId: Schema.Types.ObjectId) {
|
groupSchema.method('removeUser', async function (user: IUser) {
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
const userObjectId = user._id
|
||||||
if (userIdIndex > -1) {
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
this.users.splice(userIdIndex, 1)
|
if (userIdIndex > -1) {
|
||||||
}
|
this.users.splice(userIdIndex, 1)
|
||||||
this.markModified('users')
|
user.removeGroup(this._id)
|
||||||
return this.save()
|
|
||||||
}
|
}
|
||||||
)
|
this.markModified('users')
|
||||||
|
return this.save()
|
||||||
|
})
|
||||||
|
groupSchema.method('hasUser', function (user: IUser) {
|
||||||
|
const userObjectId = user._id
|
||||||
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
|
return userIdIndex > -1
|
||||||
|
})
|
||||||
|
|
||||||
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
||||||
'Group',
|
'Group',
|
||||||
|
|||||||
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 {
|
interface IUserDocument extends UserPayload, Document {
|
||||||
|
_id: Schema.Types.ObjectId
|
||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
@@ -43,7 +44,7 @@ interface IUserDocument extends UserPayload, Document {
|
|||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUser extends IUserDocument {
|
export interface IUser extends IUserDocument {
|
||||||
comparePassword(password: string): boolean
|
comparePassword(password: string): boolean
|
||||||
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||||
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import groupRouter from './group'
|
|||||||
import clientRouter from './client'
|
import clientRouter from './client'
|
||||||
import authRouter from './auth'
|
import authRouter from './auth'
|
||||||
import sessionRouter from './session'
|
import sessionRouter from './session'
|
||||||
|
import permissionRouter from './permission'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
@@ -35,6 +36,12 @@ router.use('/group', desktopRestrict, groupRouter)
|
|||||||
router.use('/stp', authenticateAccessToken, stpRouter)
|
router.use('/stp', authenticateAccessToken, stpRouter)
|
||||||
router.use('/code', authenticateAccessToken, codeRouter)
|
router.use('/code', authenticateAccessToken, codeRouter)
|
||||||
router.use('/user', desktopRestrict, userRouter)
|
router.use('/user', desktopRestrict, userRouter)
|
||||||
|
router.use(
|
||||||
|
'/permission',
|
||||||
|
desktopRestrict,
|
||||||
|
authenticateAccessToken,
|
||||||
|
permissionRouter
|
||||||
|
)
|
||||||
|
|
||||||
router.use(
|
router.use(
|
||||||
'/',
|
'/',
|
||||||
|
|||||||
@@ -13,4 +13,14 @@ infoRouter.get('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
infoRouter.get('/authorizedRoutes', async (req, res) => {
|
||||||
|
const controller = new InfoController()
|
||||||
|
try {
|
||||||
|
const response = controller.authorizedRoutes()
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default infoRouter
|
export default infoRouter
|
||||||
|
|||||||
69
api/src/routes/api/permission.ts
Normal file
69
api/src/routes/api/permission.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { PermissionController } from '../../controllers/'
|
||||||
|
import { verifyAdmin } from '../../middlewares'
|
||||||
|
import {
|
||||||
|
registerPermissionValidation,
|
||||||
|
updatePermissionValidation
|
||||||
|
} from '../../utils'
|
||||||
|
|
||||||
|
const permissionRouter = express.Router()
|
||||||
|
const controller = new PermissionController()
|
||||||
|
|
||||||
|
permissionRouter.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await controller.getAllPermissions()
|
||||||
|
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'))
|
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||||
|
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import {
|
||||||
|
UserController,
|
||||||
|
PermissionController,
|
||||||
|
PermissionSetting,
|
||||||
|
PrincipalType
|
||||||
|
} from '../../../controllers/'
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||||
const { getFilesFolder } = fileUtilModules
|
const { getFilesFolder } = fileUtilModules
|
||||||
@@ -48,6 +53,7 @@ describe('drive', () => {
|
|||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
|
const permissionController = new PermissionController()
|
||||||
|
|
||||||
let accessToken: string
|
let accessToken: string
|
||||||
|
|
||||||
@@ -58,11 +64,31 @@ describe('drive', () => {
|
|||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
accessToken = generateAccessToken({
|
accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
clientId,
|
await permissionController.createPermission({
|
||||||
userId: dbUser.id
|
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 () => {
|
afterAll(async () => {
|
||||||
@@ -945,3 +971,12 @@ describe('drive', () => {
|
|||||||
const getExampleService = (): ServiceMember =>
|
const getExampleService = (): ServiceMember =>
|
||||||
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
|
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
|
||||||
.members[0] as ServiceMember
|
.members[0] as ServiceMember
|
||||||
|
|
||||||
|
const generateAndSaveToken = async (userId: number) => {
|
||||||
|
const adminAccessToken = generateAccessToken({
|
||||||
|
clientId,
|
||||||
|
userId
|
||||||
|
})
|
||||||
|
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
|
||||||
|
return adminAccessToken
|
||||||
|
}
|
||||||
|
|||||||
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 { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import {
|
||||||
|
UserController,
|
||||||
|
PermissionController,
|
||||||
|
PermissionSetting,
|
||||||
|
PrincipalType
|
||||||
|
} from '../../../controllers/'
|
||||||
import {
|
import {
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
saveTokensInDB,
|
saveTokensInDB,
|
||||||
@@ -41,12 +46,21 @@ describe('stp', () => {
|
|||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
let accessToken: string
|
let accessToken: string
|
||||||
|
const userController = new UserController()
|
||||||
|
const permissionController = new PermissionController()
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await appPromise
|
app = await appPromise
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
accessToken = await generateSaveTokenAndCreateUser(user)
|
const dbUser = await userController.createUser(user)
|
||||||
|
accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
await permissionController.createPermission({
|
||||||
|
uri: '/SASjsApi/stp/execute',
|
||||||
|
principalType: PrincipalType.user,
|
||||||
|
principalId: dbUser.id,
|
||||||
|
setting: PermissionSetting.grant
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -770,12 +770,14 @@ describe('user', () => {
|
|||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: adminUser.username,
|
username: adminUser.username,
|
||||||
displayName: adminUser.displayName
|
displayName: adminUser.displayName,
|
||||||
|
isAdmin: adminUser.isAdmin
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName
|
displayName: user.displayName,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -796,12 +798,14 @@ describe('user', () => {
|
|||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: adminUser.username,
|
username: adminUser.username,
|
||||||
displayName: adminUser.displayName
|
displayName: adminUser.displayName,
|
||||||
|
isAdmin: adminUser.isAdmin
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: 'randomUser',
|
username: 'randomUser',
|
||||||
displayName: user.displayName
|
displayName: user.displayName,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ describe('web', () => {
|
|||||||
expect(res.body.user).toEqual({
|
expect(res.body.user).toEqual({
|
||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName
|
displayName: user.displayName,
|
||||||
|
isAdmin: user.isAdmin
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { Request } from 'express'
|
import express, { Request } from 'express'
|
||||||
|
import { authenticateAccessToken } from '../../middlewares'
|
||||||
import { folderExists } from '@sasjs/utils'
|
import { folderExists } from '@sasjs/utils'
|
||||||
|
|
||||||
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
||||||
@@ -9,7 +10,7 @@ const appStreams: { [key: string]: string } = {}
|
|||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', authenticateAccessToken, async (req, res) => {
|
||||||
const content = appStreamHtml(process.appStreamConfig)
|
const content = appStreamHtml(process.appStreamConfig)
|
||||||
|
|
||||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||||
@@ -66,7 +67,7 @@ export const publishAppStream = async (
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get(`/*`, function (req: Request, res, next) {
|
router.get(`/*`, authenticateAccessToken, function (req: Request, res, next) {
|
||||||
const reqPath = req.path.replace(/^\//, '')
|
const reqPath = req.path.replace(/^\//, '')
|
||||||
|
|
||||||
// Redirecting to url with trailing slash for appStream base URL only
|
// Redirecting to url with trailing slash for appStream base URL only
|
||||||
|
|||||||
2
api/src/types/system/process.d.ts
vendored
2
api/src/types/system/process.d.ts
vendored
@@ -3,10 +3,12 @@ declare namespace NodeJS {
|
|||||||
sasLoc?: string
|
sasLoc?: string
|
||||||
nodeLoc?: string
|
nodeLoc?: string
|
||||||
driveLoc: string
|
driveLoc: string
|
||||||
|
logsLoc: string
|
||||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
sasSessionController?: import('../../controllers/internal').SASSessionController
|
||||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
jsSessionController?: import('../../controllers/internal').JSSessionController
|
||||||
appStreamConfig: import('../').AppStreamConfig
|
appStreamConfig: import('../').AppStreamConfig
|
||||||
logger: import('@sasjs/utils/logger').Logger
|
logger: import('@sasjs/utils/logger').Logger
|
||||||
runTimes: import('../../utils').RunTimeType[]
|
runTimes: import('../../utils').RunTimeType[]
|
||||||
|
secrets: import('../../model/Configuration').ConfigurationType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { AppStreamConfig } from '../types'
|
|||||||
import { getAppStreamConfigPath } from './file'
|
import { getAppStreamConfigPath } from './file'
|
||||||
|
|
||||||
export const loadAppStreamConfig = async () => {
|
export const loadAppStreamConfig = async () => {
|
||||||
|
process.appStreamConfig = {}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') return
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
const appStreamConfigPath = getAppStreamConfigPath()
|
const appStreamConfigPath = getAppStreamConfigPath()
|
||||||
@@ -21,7 +23,6 @@ export const loadAppStreamConfig = async () => {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
appStreamConfig = {}
|
appStreamConfig = {}
|
||||||
}
|
}
|
||||||
process.appStreamConfig = {}
|
|
||||||
|
|
||||||
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
|
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
|
||||||
const { appLoc, streamWebFolder, streamLogo } = entry
|
const { appLoc, streamWebFolder, streamLogo } = entry
|
||||||
|
|||||||
@@ -9,7 +9,5 @@ export const connectDB = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Connected to DB!')
|
console.log('Connected to DB!')
|
||||||
await seedDB()
|
return seedDB()
|
||||||
|
|
||||||
return mongoose.connection
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export const getDesktopUserAutoExecPath = () =>
|
|||||||
|
|
||||||
export const getSasjsRootFolder = () => process.driveLoc
|
export const getSasjsRootFolder = () => process.driveLoc
|
||||||
|
|
||||||
|
export const getLogFolder = () => process.logsLoc
|
||||||
|
|
||||||
export const getAppStreamConfigPath = () =>
|
export const getAppStreamConfigPath = () =>
|
||||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
||||||
|
|
||||||
@@ -32,8 +34,6 @@ export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
|||||||
|
|
||||||
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
||||||
|
|
||||||
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
|
|
||||||
|
|
||||||
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
||||||
|
|
||||||
export const getSessionsFolder = () =>
|
export const getSessionsFolder = () =>
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateAccessToken = (data: InfoJWT) =>
|
export const generateAccessToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
|
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
|
||||||
expiresIn: '1day'
|
expiresIn: '1day'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateAuthCode = (data: InfoJWT) =>
|
export const generateAuthCode = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.AUTH_CODE_SECRET as string, {
|
jwt.sign(data, process.secrets.AUTH_CODE_SECRET, {
|
||||||
expiresIn: '30s'
|
expiresIn: '30s'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateRefreshToken = (data: InfoJWT) =>
|
export const generateRefreshToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
|
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
|
||||||
expiresIn: '30 days'
|
expiresIn: '30 days'
|
||||||
})
|
})
|
||||||
|
|||||||
35
api/src/utils/getAuthorizedRoutes.ts
Normal file
35
api/src/utils/getAuthorizedRoutes.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Request } from 'express'
|
||||||
|
|
||||||
|
const StaticAuthorizedRoutes = [
|
||||||
|
'/AppStream',
|
||||||
|
'/SASjsApi/code/execute',
|
||||||
|
'/SASjsApi/stp/execute',
|
||||||
|
'/SASjsApi/drive/deploy',
|
||||||
|
'/SASjsApi/drive/deploy/upload',
|
||||||
|
'/SASjsApi/drive/file',
|
||||||
|
'/SASjsApi/drive/folder',
|
||||||
|
'/SASjsApi/drive/fileTree',
|
||||||
|
'/SASjsApi/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))
|
||||||
@@ -5,7 +5,7 @@ import { RunTimeType } from '.'
|
|||||||
|
|
||||||
export const getRunTimeAndFilePath = async (programPath: string) => {
|
export const getRunTimeAndFilePath = async (programPath: string) => {
|
||||||
const ext = path.extname(programPath)
|
const ext = path.extname(programPath)
|
||||||
// If programPath (_program) is provided with a ".sas" or ".js" extension
|
// If programPath (_program) is provided with a ".sas" or ".js" extension
|
||||||
// we should use that extension to determine the appropriate runTime
|
// we should use that extension to determine the appropriate runTime
|
||||||
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
||||||
const runTime = ext.slice(1)
|
const runTime = ext.slice(1)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export * from './file'
|
|||||||
export * from './generateAccessToken'
|
export * from './generateAccessToken'
|
||||||
export * from './generateAuthCode'
|
export * from './generateAuthCode'
|
||||||
export * from './generateRefreshToken'
|
export * from './generateRefreshToken'
|
||||||
|
export * from './getAuthorizedRoutes'
|
||||||
export * from './getCertificates'
|
export * from './getCertificates'
|
||||||
export * from './getDesktopFields'
|
export * from './getDesktopFields'
|
||||||
export * from './getPreProgramVariables'
|
export * from './getPreProgramVariables'
|
||||||
|
|||||||
@@ -1,6 +1,73 @@
|
|||||||
import Client from '../model/Client'
|
import Client from '../model/Client'
|
||||||
|
import Group from '../model/Group'
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
|
import Configuration, { ConfigurationType } from '../model/Configuration'
|
||||||
|
|
||||||
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
|
export const SECRETS: ConfigurationType = {
|
||||||
|
ACCESS_TOKEN_SECRET: randomBytes(64).toString('hex'),
|
||||||
|
REFRESH_TOKEN_SECRET: randomBytes(64).toString('hex'),
|
||||||
|
AUTH_CODE_SECRET: randomBytes(64).toString('hex'),
|
||||||
|
SESSION_SECRET: randomBytes(64).toString('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const seedDB = async (): Promise<ConfigurationType> => {
|
||||||
|
// Checking if client is already in the database
|
||||||
|
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
||||||
|
if (!clientExist) {
|
||||||
|
const client = new Client(CLIENT)
|
||||||
|
await client.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking if 'AllUsers' Group is already in the database
|
||||||
|
let groupExist = await Group.findOne({ name: GROUP.name })
|
||||||
|
if (!groupExist) {
|
||||||
|
const group = new Group(GROUP)
|
||||||
|
groupExist = await group.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - Group created: ${GROUP.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking if 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 = {
|
const CLIENT = {
|
||||||
clientId: 'clientID1',
|
clientId: 'clientID1',
|
||||||
clientSecret: 'clientSecret'
|
clientSecret: 'clientSecret'
|
||||||
@@ -13,23 +80,3 @@ const ADMIN_USER = {
|
|||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export const seedDB = async () => {
|
|
||||||
// Checking if client is already in the database
|
|
||||||
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
|
||||||
if (!clientExist) {
|
|
||||||
const client = new Client(CLIENT)
|
|
||||||
await client.save()
|
|
||||||
|
|
||||||
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checking if user is already in the database
|
|
||||||
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
|
||||||
if (!usernameExist) {
|
|
||||||
const user = new User(ADMIN_USER)
|
|
||||||
await user.save()
|
|
||||||
|
|
||||||
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||||
|
|
||||||
import { getDesktopFields, ModeType, RunTimeType } from '.'
|
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
|
||||||
|
|
||||||
export const setProcessVariables = async () => {
|
export const setProcessVariables = async () => {
|
||||||
|
const { MODE, RUN_TIMES } = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
// NOTE: when exporting app.js as agent for supertest
|
||||||
|
// it should prevent connecting to the real database
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
const secrets = await connectDB()
|
||||||
|
|
||||||
|
process.secrets = secrets
|
||||||
|
} else {
|
||||||
|
process.secrets = SECRETS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { MODE, RUN_TIMES } = process.env
|
|
||||||
|
|
||||||
process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? []
|
process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? []
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
@@ -28,7 +40,16 @@ export const setProcessVariables = async () => {
|
|||||||
await createFolder(absPath)
|
await createFolder(absPath)
|
||||||
process.driveLoc = getRealPath(absPath)
|
process.driveLoc = getRealPath(absPath)
|
||||||
|
|
||||||
|
const { LOG_LOCATION } = process.env
|
||||||
|
const absLogsPath = getAbsolutePath(
|
||||||
|
LOG_LOCATION ?? `sasjs_root${path.sep}logs`,
|
||||||
|
process.cwd()
|
||||||
|
)
|
||||||
|
await createFolder(absLogsPath)
|
||||||
|
process.logsLoc = getRealPath(absLogsPath)
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
|
console.log('sasLogs: ', process.logsLoc)
|
||||||
console.log('runTimes: ', process.runTimes)
|
console.log('runTimes: ', process.runTimes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { extractHeaders } from '..'
|
import { extractHeaders } from '../extractHeaders'
|
||||||
|
|
||||||
describe('extractHeaders', () => {
|
describe('extractHeaders', () => {
|
||||||
it('should return valid http headers', () => {
|
it('should return valid http headers', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseLogToArray } from '..'
|
import { parseLogToArray } from '../parseLogToArray'
|
||||||
|
|
||||||
describe('parseLogToArray', () => {
|
describe('parseLogToArray', () => {
|
||||||
it('should parse log to array type', () => {
|
it('should parse log to array type', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { MulterFile } from '../types/Upload'
|
import { MulterFile } from '../types/Upload'
|
||||||
import { listFilesInFolder, readFileBinary } from '@sasjs/utils'
|
import { listFilesInFolder, readFileBinary, isWindows } from '@sasjs/utils'
|
||||||
|
|
||||||
interface FilenameMapSingle {
|
interface FilenameMapSingle {
|
||||||
fieldName: string
|
fieldName: string
|
||||||
@@ -118,7 +118,9 @@ export const generateFileUploadJSCode = async (
|
|||||||
if (fileName.includes('req_file')) {
|
if (fileName.includes('req_file')) {
|
||||||
fileCount++
|
fileCount++
|
||||||
const filePath = path.join(sessionFolder, fileName)
|
const filePath = path.join(sessionFolder, fileName)
|
||||||
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${filePath}')`
|
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${
|
||||||
|
isWindows() ? filePath.replace(/\\/g, '\\\\') : filePath
|
||||||
|
}')`
|
||||||
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
||||||
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Joi from 'joi'
|
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 usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||||
const passwordSchema = Joi.string().min(6).max(1024)
|
const passwordSchema = Joi.string().min(6).max(1024)
|
||||||
@@ -86,6 +87,27 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
|||||||
clientSecret: Joi.string().required()
|
clientSecret: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
|
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
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 =>
|
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
||||||
|
|||||||
@@ -78,33 +78,7 @@ const verifyMODE = (): string[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Server) {
|
if (process.env.MODE === ModeType.Server) {
|
||||||
const {
|
const { DB_CONNECT } = process.env
|
||||||
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}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test')
|
if (process.env.NODE_ENV !== 'test')
|
||||||
if (!DB_CONNECT)
|
if (!DB_CONNECT)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
"name": "Info",
|
"name": "Info",
|
||||||
"description": "Get Server Info"
|
"description": "Get Server Information"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Session",
|
"name": "Session",
|
||||||
@@ -21,7 +21,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "User",
|
"name": "User",
|
||||||
"description": "Operations about users"
|
"description": "Operations with users"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Permission",
|
||||||
|
"description": "Operations about permissions"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Client",
|
"name": "Client",
|
||||||
@@ -33,19 +37,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Drive",
|
"name": "Drive",
|
||||||
"description": "Operations about drive"
|
"description": "Operations on SASjs Drive"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Group",
|
"name": "Group",
|
||||||
"description": "Operations about group"
|
"description": "Operations on groups and group memberships"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "STP",
|
"name": "STP",
|
||||||
"description": "Operations about STP"
|
"description": "Execution of Stored Programs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CODE",
|
"name": "CODE",
|
||||||
"description": "Operations on SAS code"
|
"description": "Execution of code (various runtimes are supported)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Web",
|
"name": "Web",
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx webpack-dev-server --config webpack.dev.ts --hot",
|
"start": "webpack-dev-server --config webpack.dev.ts --hot",
|
||||||
"build": "npx webpack --config webpack.prod.ts"
|
"build": "webpack --config webpack.prod.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@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}
|
open={!!anchorEl}
|
||||||
onClose={handleClose}
|
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' }}>
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const Login = () => {
|
|||||||
username,
|
username,
|
||||||
password
|
password
|
||||||
}).catch((err: any) => {
|
}).catch((err: any) => {
|
||||||
setErrorMessage(err.response.data)
|
setErrorMessage(err.response?.data || err.toString())
|
||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ const Login = () => {
|
|||||||
appContext.setUserId?.(user.id)
|
appContext.setUserId?.(user.id)
|
||||||
appContext.setUsername?.(user.username)
|
appContext.setUsername?.(user.username)
|
||||||
appContext.setDisplayName?.(user.displayName)
|
appContext.setDisplayName?.(user.displayName)
|
||||||
|
appContext.setIsAdmin?.(user.isAdmin)
|
||||||
appContext.setLoggedIn?.(loggedIn)
|
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 { Box, Paper, Tab, styled } from '@mui/material'
|
||||||
import TabContext from '@mui/lab/TabContext'
|
import TabContext from '@mui/lab/TabContext'
|
||||||
import TabList from '@mui/lab/TabList'
|
import TabList from '@mui/lab/TabList'
|
||||||
import TabPanel from '@mui/lab/TabPanel'
|
import TabPanel from '@mui/lab/TabPanel'
|
||||||
|
|
||||||
|
import Permission from './permission'
|
||||||
import Profile from './profile'
|
import Profile from './profile'
|
||||||
|
|
||||||
|
import { AppContext, ModeType } from '../../context/appContext'
|
||||||
|
|
||||||
const StyledTab = styled(Tab)({
|
const StyledTab = styled(Tab)({
|
||||||
background: 'black',
|
background: 'black',
|
||||||
margin: '0 5px 5px 0'
|
margin: '0 5px 5px 0'
|
||||||
@@ -17,7 +20,8 @@ const StyledTabpanel = styled(TabPanel)({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const [value, setValue] = React.useState('profile')
|
const appContext = useContext(AppContext)
|
||||||
|
const [value, setValue] = useState('profile')
|
||||||
|
|
||||||
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||||
setValue(newValue)
|
setValue(newValue)
|
||||||
@@ -42,11 +46,17 @@ const Settings = () => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
<StyledTab label="Profile" value="profile" />
|
<StyledTab label="Profile" value="profile" />
|
||||||
|
{appContext.mode === ModeType.Server && (
|
||||||
|
<StyledTab label="Uri Access" value="permission" />
|
||||||
|
)}
|
||||||
</TabList>
|
</TabList>
|
||||||
</Box>
|
</Box>
|
||||||
<StyledTabpanel value="profile">
|
<StyledTabpanel value="profile">
|
||||||
<Profile />
|
<Profile />
|
||||||
</StyledTabpanel>
|
</StyledTabpanel>
|
||||||
|
<StyledTabpanel value="permission">
|
||||||
|
<Permission />
|
||||||
|
</StyledTabpanel>
|
||||||
</TabContext>
|
</TabContext>
|
||||||
</Box>
|
</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 axios from 'axios'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Backdrop,
|
||||||
Box,
|
Box,
|
||||||
MenuItem,
|
Button,
|
||||||
|
CircularProgress,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
MenuItem,
|
||||||
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
Button,
|
|
||||||
Paper,
|
|
||||||
Tab,
|
Tab,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
@@ -50,6 +52,7 @@ const Studio = () => {
|
|||||||
const [tab, setTab] = useState('1')
|
const [tab, setTab] = useState('1')
|
||||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||||
|
const [isRunning, setIsRunning] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRunTimes(Object.values(appContext.runTimes))
|
setRunTimes(Object.values(appContext.runTimes))
|
||||||
@@ -78,6 +81,7 @@ const Studio = () => {
|
|||||||
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
|
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
|
||||||
|
|
||||||
const runCode = (code: string) => {
|
const runCode = (code: string) => {
|
||||||
|
setIsRunning(true)
|
||||||
axios
|
axios
|
||||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
@@ -94,6 +98,7 @@ const Studio = () => {
|
|||||||
window.scrollTo(0, document.body.scrollHeight)
|
window.scrollTo(0, document.body.scrollHeight)
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err))
|
.catch((err) => console.log(err))
|
||||||
|
.finally(() => setIsRunning(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: any) => {
|
const handleKeyDown = (event: any) => {
|
||||||
@@ -163,6 +168,12 @@ const Studio = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TabPanel sx={{ 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}>
|
<div className={classes.subMenu}>
|
||||||
<Tooltip title="CTRL+ENTER will also run SAS code">
|
<Tooltip title="CTRL+ENTER will also run SAS code">
|
||||||
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ interface AppContextProps {
|
|||||||
setUsername: Dispatch<SetStateAction<string>> | null
|
setUsername: Dispatch<SetStateAction<string>> | null
|
||||||
displayName: string
|
displayName: string
|
||||||
setDisplayName: Dispatch<SetStateAction<string>> | null
|
setDisplayName: Dispatch<SetStateAction<string>> | null
|
||||||
|
isAdmin: boolean
|
||||||
|
setIsAdmin: Dispatch<SetStateAction<boolean>> | null
|
||||||
mode: ModeType
|
mode: ModeType
|
||||||
runTimes: RunTimeType[]
|
runTimes: RunTimeType[]
|
||||||
logout: (() => void) | null
|
logout: (() => void) | null
|
||||||
@@ -44,6 +46,8 @@ export const AppContext = createContext<AppContextProps>({
|
|||||||
setUsername: null,
|
setUsername: null,
|
||||||
displayName: '',
|
displayName: '',
|
||||||
setDisplayName: null,
|
setDisplayName: null,
|
||||||
|
isAdmin: false,
|
||||||
|
setIsAdmin: null,
|
||||||
mode: ModeType.Server,
|
mode: ModeType.Server,
|
||||||
runTimes: [],
|
runTimes: [],
|
||||||
logout: null
|
logout: null
|
||||||
@@ -56,6 +60,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
const [userId, setUserId] = useState(0)
|
const [userId, setUserId] = useState(0)
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [displayName, setDisplayName] = useState('')
|
const [displayName, setDisplayName] = useState('')
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false)
|
||||||
const [mode, setMode] = useState(ModeType.Server)
|
const [mode, setMode] = useState(ModeType.Server)
|
||||||
const [runTimes, setRunTimes] = useState<RunTimeType[]>([])
|
const [runTimes, setRunTimes] = useState<RunTimeType[]>([])
|
||||||
|
|
||||||
@@ -70,6 +75,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
setUserId(data.id)
|
setUserId(data.id)
|
||||||
setUsername(data.username)
|
setUsername(data.username)
|
||||||
setDisplayName(data.displayName)
|
setDisplayName(data.displayName)
|
||||||
|
setIsAdmin(data.isAdmin)
|
||||||
setLoggedIn(true)
|
setLoggedIn(true)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -107,6 +113,8 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
setUsername,
|
setUsername,
|
||||||
displayName,
|
displayName,
|
||||||
setDisplayName,
|
setDisplayName,
|
||||||
|
isAdmin,
|
||||||
|
setIsAdmin,
|
||||||
mode,
|
mode,
|
||||||
runTimes,
|
runTimes,
|
||||||
logout
|
logout
|
||||||
|
|||||||
@@ -18,3 +18,10 @@ code {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
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