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

Compare commits

..

1 Commits

Author SHA1 Message Date
Saad Jutt
efbfd3f392 chore(web): switched built tool, webpack to Vite 2022-10-02 03:23:09 +05:00
143 changed files with 3743 additions and 15499 deletions

View File

@@ -56,4 +56,4 @@ jobs:
- name: Release
run: |
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} semantic-release
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release

2
.gitignore vendored
View File

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

View File

@@ -1,354 +1,3 @@
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)
### Features
* **code:** added code/trigger API endpoint ([ffcf193](https://github.com/sasjs/server/commit/ffcf193b87d811b166d79af74013776a253b50b0))
## [0.35.4](https://github.com/sasjs/server/compare/v0.35.3...v0.35.4) (2024-01-15)
### Bug Fixes
* **api:** fixed env issue in MacOS executable ([73d965d](https://github.com/sasjs/server/commit/73d965daf54b16c0921e4b18d11a1e6f8650884d))
## [0.35.3](https://github.com/sasjs/server/compare/v0.35.2...v0.35.3) (2023-11-07)
### Bug Fixes
* enable embedded LFs in JS STP vars ([7e8cbbf](https://github.com/sasjs/server/commit/7e8cbbf377b27a7f5dd9af0bc6605c01f302f5d9))
## [0.35.2](https://github.com/sasjs/server/compare/v0.35.1...v0.35.2) (2023-08-07)
### Bug Fixes
* add _debug as optional query param in swagger apis for GET stp/execute ([9586dbb](https://github.com/sasjs/server/commit/9586dbb2d0d6611061c9efdfb84030144f62c2ee))
## [0.35.1](https://github.com/sasjs/server/compare/v0.35.0...v0.35.1) (2023-07-25)
### Bug Fixes
* **log-separator:** log separator should always wrap log ([8940f4d](https://github.com/sasjs/server/commit/8940f4dc47abae2036b4fcdeb772c31a0ca07cca))
# [0.35.0](https://github.com/sasjs/server/compare/v0.34.2...v0.35.0) (2023-05-03)
### Bug Fixes
* **editor:** fixed log/webout/print tabs ([d2de9dc](https://github.com/sasjs/server/commit/d2de9dc13ef2e980286dd03cca5e22cea443ed0c))
* **execute:** added atribute indicating stp api ([e78f87f](https://github.com/sasjs/server/commit/e78f87f5c00038ea11261dffb525ac8f1024e40b))
* **execute:** fixed adding print output ([9aaffce](https://github.com/sasjs/server/commit/9aaffce82051d81bf39adb69942bb321e9795141))
* **execution:** removed empty webout from response ([6dd2f4f](https://github.com/sasjs/server/commit/6dd2f4f87673336135bc7a6de0d2e143e192c025))
* **webout:** fixed adding empty webout to response payload ([31df72a](https://github.com/sasjs/server/commit/31df72ad88fe2c771d0ef8445d6db9dd147c40c9))
### Features
* **editor:** parse print output in response payload ([eb42683](https://github.com/sasjs/server/commit/eb42683fff701bd5b4d2b68760fe0c3ecad573dd))
## [0.34.2](https://github.com/sasjs/server/compare/v0.34.1...v0.34.2) (2023-05-01)
### Bug Fixes
* use custom logic for handling sequence ids ([dba53de](https://github.com/sasjs/server/commit/dba53de64664c9d8a40fe69de6281c53d1c73641))
## [0.34.1](https://github.com/sasjs/server/compare/v0.34.0...v0.34.1) (2023-04-28)
### Bug Fixes
* **css:** fixed css loading ([9c5acd6](https://github.com/sasjs/server/commit/9c5acd6de32afdbc186f79ae5b35375dda2e49b0))
* **log:** fixed chunk collapsing ([64b156f](https://github.com/sasjs/server/commit/64b156f7627969b7f13022726f984fbbfe1a33ef))
# [0.34.0](https://github.com/sasjs/server/compare/v0.33.3...v0.34.0) (2023-04-28)
### Bug Fixes
* **log:** fixed checks for errors and warnings ([02e2b06](https://github.com/sasjs/server/commit/02e2b060f9bedf4806f45f5205fd87bfa2ecae90))
* **log:** fixed default runtime ([e04300a](https://github.com/sasjs/server/commit/e04300ad2ac237be7b28a6332fa87a3bcf761c7b))
* **log:** fixed parsing log for different runtime ([3b1e4a1](https://github.com/sasjs/server/commit/3b1e4a128b1f22ff6f3069f5aaada6bfb1b40d12))
* **log:** fixed scrolling issue ([56a522c](https://github.com/sasjs/server/commit/56a522c07c6f6d4c26c6d3b7cd6e9ef7007067a9))
* **log:** fixed single chunk display ([8254b78](https://github.com/sasjs/server/commit/8254b789555cb8bbb169f52b754b4ce24e876dd2))
* **log:** fixed single chunk scrolling ([57b7f95](https://github.com/sasjs/server/commit/57b7f954a17936f39aa9b757998b5b25e9442601))
* **log:** fixed switching runtime ([c7a7399](https://github.com/sasjs/server/commit/c7a73991a7aa25d0c75d0c00e712bdc78769300b))
* **log:** fixing switching from SAS to other runtime ([c72ecc7](https://github.com/sasjs/server/commit/c72ecc7e5943af9536ee31cfa85398e016d5354f))
### Features
* **log:** added download chunk and entire log ([a38a9f9](https://github.com/sasjs/server/commit/a38a9f9c3dfe36bd55d32024c166147318216995))
* **log:** added logComponent and LogTabWithIcons ([3a887de](https://github.com/sasjs/server/commit/3a887dec55371b6a00b92291bb681e4cccb770c0))
* **log:** added parseErrorsAndWarnings utility ([7c1c1e2](https://github.com/sasjs/server/commit/7c1c1e241002313c10f94dd61702584b9f148010))
* **log:** added time to downloaded log name ([3848bb0](https://github.com/sasjs/server/commit/3848bb0added69ca81a5c9419ea414bdd1c294bb))
* **log:** put download log icon into log tab ([777b3a5](https://github.com/sasjs/server/commit/777b3a55be1ecf5b05bf755ce8b14735496509e1))
* **log:** split large log into chunks ([75f5a3c](https://github.com/sasjs/server/commit/75f5a3c0b39665bef8b83dc7e1e8b3e5f23fc303))
* **log:** use improved log for SAS run time only ([7b12591](https://github.com/sasjs/server/commit/7b12591595cdd5144d9311ffa06a80c5dab79364))
## [0.33.3](https://github.com/sasjs/server/compare/v0.33.2...v0.33.3) (2023-04-27)
### Bug Fixes
* use RateLimiterMemory instead of RateLimiterMongo ([6a520f5](https://github.com/sasjs/server/commit/6a520f5b26a3e2ed6345721b30ff4e3d9bfa903d))
## [0.33.2](https://github.com/sasjs/server/compare/v0.33.1...v0.33.2) (2023-04-24)
### Bug Fixes
* removing print redirection pending full [#274](https://github.com/sasjs/server/issues/274) fix ([d49ea47](https://github.com/sasjs/server/commit/d49ea47bd7a2add42bdb9a717082201f29e16597))
## [0.33.1](https://github.com/sasjs/server/compare/v0.33.0...v0.33.1) (2023-04-20)
### Bug Fixes
* applying nologo only for sas.exe ([b4436ba](https://github.com/sasjs/server/commit/b4436bad0d24d5b5a402272632db1739b1018c90)), closes [#352](https://github.com/sasjs/server/issues/352)
# [0.33.0](https://github.com/sasjs/server/compare/v0.32.0...v0.33.0) (2023-04-05)
### Features
* option to reset admin password on startup ([eda8e56](https://github.com/sasjs/server/commit/eda8e56bb0ea20fdaacabbbe7dcf1e3ea7bd215a))
# [0.32.0](https://github.com/sasjs/server/compare/v0.31.0...v0.32.0) (2023-04-05)
### Features
* add an api endpoint for admin to get list of client ids ([6ffaa7e](https://github.com/sasjs/server/commit/6ffaa7e9e2a62c083bb9fcc3398dcbed10cebdb1))
# [0.31.0](https://github.com/sasjs/server/compare/v0.30.3...v0.31.0) (2023-03-30)
### Features
* prevent brute force attack by rate limiting login endpoint ([a82cabb](https://github.com/sasjs/server/commit/a82cabb00134c79c5ee77afd1b1628a1f768e050))
## [0.30.3](https://github.com/sasjs/server/compare/v0.30.2...v0.30.3) (2023-03-07)
### Bug Fixes
* add location.pathname to location.origin conditionally ([edab51c](https://github.com/sasjs/server/commit/edab51c51997f17553e037dc7c2b5e5fa6ea8ffe))
## [0.30.2](https://github.com/sasjs/server/compare/v0.30.1...v0.30.2) (2023-03-07)
### Bug Fixes
* **web:** add path to base in launch program url ([2c31922](https://github.com/sasjs/server/commit/2c31922f58a8aa20d7fa6bfc95b53a350f90c798))
## [0.30.1](https://github.com/sasjs/server/compare/v0.30.0...v0.30.1) (2023-03-01)
### Bug Fixes
* **web:** add proper base url in axios.defaults ([5e3ce8a](https://github.com/sasjs/server/commit/5e3ce8a98f1825e14c1d26d8da0c9821beeff7b3))
# [0.30.0](https://github.com/sasjs/server/compare/v0.29.0...v0.30.0) (2023-02-28)
### Bug Fixes
* lint + remove default settings ([3de59ac](https://github.com/sasjs/server/commit/3de59ac4f8e3d95cad31f09e6963bd04c4811f26))
### Features
* add new env config DB_TYPE ([158f044](https://github.com/sasjs/server/commit/158f044363abf2576c8248f0ca9da4bc9cb7e9d8))
# [0.29.0](https://github.com/sasjs/server/compare/v0.28.7...v0.29.0) (2023-02-06)
### Features
* Add /SASjsApi endpoint in permissions ([b3402ea](https://github.com/sasjs/server/commit/b3402ea80afb8802eee8b8b6cbbbcc29903424bc))
## [0.28.7](https://github.com/sasjs/server/compare/v0.28.6...v0.28.7) (2023-02-03)
### Bug Fixes
* add user to all users group on user creation ([2bae52e](https://github.com/sasjs/server/commit/2bae52e307327d7ee4a94b19d843abdc0ccec9d1))
## [0.28.6](https://github.com/sasjs/server/compare/v0.28.5...v0.28.6) (2023-01-26)
### Bug Fixes
* show loading spinner on login screen while request is in process ([69f2576](https://github.com/sasjs/server/commit/69f2576ee6d3d7b7f3325922a88656d511e3ac88))
## [0.28.5](https://github.com/sasjs/server/compare/v0.28.4...v0.28.5) (2023-01-01)
### Bug Fixes
* adding NOPRNGETLIST system option for faster startup ([96eca3a](https://github.com/sasjs/server/commit/96eca3a35dce4521150257ee019beb4488c8a08f))
## [0.28.4](https://github.com/sasjs/server/compare/v0.28.3...v0.28.4) (2022-12-07)
### Bug Fixes
* replace main class with container class ([71c429b](https://github.com/sasjs/server/commit/71c429b093b91e2444ae75d946579dccc2e48636))
## [0.28.3](https://github.com/sasjs/server/compare/v0.28.2...v0.28.3) (2022-12-06)
### Bug Fixes
* stringify json file ([1192583](https://github.com/sasjs/server/commit/1192583843d7efd1a6ab6943207f394c3ae966be))
## [0.28.2](https://github.com/sasjs/server/compare/v0.28.1...v0.28.2) (2022-12-05)
### Bug Fixes
* execute child process asyncronously ([23c997b](https://github.com/sasjs/server/commit/23c997b3beabeb6b733ae893031d2f1a48f28ad2))
* JS / Python / R session folders should be NEW folders, not existing SAS folders ([39ba995](https://github.com/sasjs/server/commit/39ba995355daa24bb7ab22720f8fc57d2dc85f40))
## [0.28.1](https://github.com/sasjs/server/compare/v0.28.0...v0.28.1) (2022-11-28)
### Bug Fixes
* update the content type header after the program has been executed ([4dcee4b](https://github.com/sasjs/server/commit/4dcee4b3c3950d402220b8f451c50ad98a317d83))
# [0.28.0](https://github.com/sasjs/server/compare/v0.27.0...v0.28.0) (2022-11-28)
### Bug Fixes
* update the response header of request to stp/execute routes ([112431a](https://github.com/sasjs/server/commit/112431a1b7461989c04100418d67d975a2a8f354))
### Features
* **api:** add the api endpoint for updating user password ([4581f32](https://github.com/sasjs/server/commit/4581f325344eb68c5df5a28492f132312f15bb5c))
* ask for updated password on first login ([1d48f88](https://github.com/sasjs/server/commit/1d48f8856b1fbbf3ef868914558333190e04981f))
* **web:** add the UI for updating user password ([8b8c43c](https://github.com/sasjs/server/commit/8b8c43c21bde5379825c5ec44ecd81a92425f605))
# [0.27.0](https://github.com/sasjs/server/compare/v0.26.2...v0.27.0) (2022-11-17)
### Features
* on startup add webout.sas file in sasautos folder ([200f6c5](https://github.com/sasjs/server/commit/200f6c596a6e732d799ed408f1f0fd92f216ba58))
## [0.26.2](https://github.com/sasjs/server/compare/v0.26.1...v0.26.2) (2022-11-15)
### Bug Fixes
* comments ([7ae862c](https://github.com/sasjs/server/commit/7ae862c5ce720e9483d4728f4295dede4f849436))
## [0.26.1](https://github.com/sasjs/server/compare/v0.26.0...v0.26.1) (2022-11-15)
### Bug Fixes
* change the expiration of access/refresh tokens from days to seconds ([bb05493](https://github.com/sasjs/server/commit/bb054938c5bd0535ae6b9da93ba0b14f9b80ddcd))
# [0.26.0](https://github.com/sasjs/server/compare/v0.25.1...v0.26.0) (2022-11-13)
### Bug Fixes
* **web:** dispose monaco editor actions in return of useEffect ([acc25cb](https://github.com/sasjs/server/commit/acc25cbd686952d3f1c65e57aefcebe1cb859cc7))
### Features
* make access token duration configurable when creating client/secret ([2413c05](https://github.com/sasjs/server/commit/2413c05fea3960f7e5c3c8b7b2f85d61314f08db))
* make refresh token duration configurable ([abd5c64](https://github.com/sasjs/server/commit/abd5c64b4a726e3f17594a98111b6aa269b71fee))
## [0.25.1](https://github.com/sasjs/server/compare/v0.25.0...v0.25.1) (2022-11-07)
### Bug Fixes
* **web:** use mui treeView instead of custom implementation ([c51b504](https://github.com/sasjs/server/commit/c51b50428f32608bc46438e9d7964429b2d595da))
# [0.25.0](https://github.com/sasjs/server/compare/v0.24.0...v0.25.0) (2022-11-02)
### Features
* Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server ([1c9d167](https://github.com/sasjs/server/commit/1c9d167f86bbbb108b96e9bc30efaf8de65d82ff))
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)
### Features
* cli mock testing ([6434123](https://github.com/sasjs/server/commit/643412340162e854f31fba2f162d83b7ab1751d8))
* mocking sas9 responses with JS STP ([36be3a7](https://github.com/sasjs/server/commit/36be3a7d5e7df79f9a1f3f00c3661b925f462383))
## [0.23.4](https://github.com/sasjs/server/compare/v0.23.3...v0.23.4) (2022-10-11)
### Bug Fixes
* add action to editor ref for running code ([2412622](https://github.com/sasjs/server/commit/2412622367eb46c40f388e988ae4606a7ec239b2))
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)
### Bug Fixes
* added domain for session cookies ([94072c3](https://github.com/sasjs/server/commit/94072c3d24a4d0d4c97900dc31bfbf1c9d2559b7))
## [0.23.2](https://github.com/sasjs/server/compare/v0.23.1...v0.23.2) (2022-10-06)
### Bug Fixes
* bump in correct place ([14731e8](https://github.com/sasjs/server/commit/14731e8824fa9f3d1daf89fd62f9916d5e3fcae4))
* bumping sasjs/score ([258cc35](https://github.com/sasjs/server/commit/258cc35f14cf50f2160f607000c60de27593fd79))
* reverting commit ([fda0e0b](https://github.com/sasjs/server/commit/fda0e0b57d56e3b5231e626a8d933343ac0c5cdc))
## [0.23.1](https://github.com/sasjs/server/compare/v0.23.0...v0.23.1) (2022-10-04)
### Bug Fixes
* ldap issues ([4d64420](https://github.com/sasjs/server/commit/4d64420c45424134b4d2014a2d5dd6e846ed03b3))
# [0.23.0](https://github.com/sasjs/server/compare/v0.22.1...v0.23.0) (2022-10-03)
### Features
* Enable SAS_PACKAGES in SASjs Server ([424f0fc](https://github.com/sasjs/server/commit/424f0fc1faec765eb7a14619584e649454105b70))
## [0.22.1](https://github.com/sasjs/server/compare/v0.22.0...v0.22.1) (2022-10-03)
### Bug Fixes
* spelling issues ([3bb0597](https://github.com/sasjs/server/commit/3bb05974d216d69368f4498eb9f309bce7d97fd8))
# [0.22.0](https://github.com/sasjs/server/compare/v0.21.7...v0.22.0) (2022-10-03)
### Bug Fixes
* do not throw error on deleting group when it is created by an external auth provider ([68f0c5c](https://github.com/sasjs/server/commit/68f0c5c5884431e7e8f586dccf98132abebb193e))
* no need to restrict api endpoints when ldap auth is applied ([a142660](https://github.com/sasjs/server/commit/a14266077d3541c7a33b7635efa4208335e73519))
* remove authProvider attribute from user and group payload interface ([bbd7786](https://github.com/sasjs/server/commit/bbd7786c6ce13b374d896a45c23255b8fa3e8bd2))
### Features
* implemented LDAP authentication ([f915c51](https://github.com/sasjs/server/commit/f915c51b077a2b8c4099727355ed914ecd6364bd))
## [0.21.7](https://github.com/sasjs/server/compare/v0.21.6...v0.21.7) (2022-09-30)

View File

@@ -93,10 +93,6 @@ R_PATH=/usr/bin/Rscript
SASJS_ROOT=./sasjs_root
# This location is for files, sasjs packages and appStreamConfig.json
DRIVE_LOCATION=./sasjs_root/drive
# options: [http|https] default: http
PROTOCOL=
@@ -107,11 +103,6 @@ PORT=
# If not present, mocking function is disabled
MOCK_SERVERTYPE=
# default: /api/mocks
# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs
# Server will automatically use subdirectory accordingly
STATIC_MOCK_LOCATION=
#
## Additional SAS Options
#
@@ -134,22 +125,9 @@ PRIVATE_KEY=privkey.pem (required)
CERT_CHAIN=certificate.pem (required)
CA_ROOT=fullchain.pem (optional)
## ENV variables required for MODE: `server`
# ENV variables required for MODE: `server`
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
# options: [mongodb|cosmos_mongodb] default: mongodb
DB_TYPE=
# AUTH_PROVIDERS options: [ldap] default: ``
AUTH_PROVIDERS=
## ENV variables required for AUTH_MECHANISM: `ldap`
LDAP_URL= <LDAP_SERVER_URL>
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
LDAP_BIND_PASSWORD = <password>
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
# If enabled, be sure to also configure the WHITELIST of third party servers.
CORS=
@@ -158,7 +136,7 @@ CORS=
WHITELIST=
# HELMET Cross Origin Embedder Policy
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
# options: [true|false] default: true
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
HELMET_COEP=
@@ -175,32 +153,6 @@ HELMET_COEP=
# }
HELMET_CSP_CONFIG_PATH=./csp.config.json
# To prevent brute force attack on login route we have implemented rate limiter
# Only valid for MODE: server
# Following are configurable env variable rate limiter
# After this, access is blocked for 1 day
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
# After this, access is blocked for an hour
# Store number for 24 days since first fail
# Once a successful login is attempted, it resets
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
# Name of the admin user that will be created on startup if not exists already
# Default is `secretuser`
ADMIN_USERNAME=secretuser
# Temporary password for the ADMIN_USERNAME, which is in place until the first login
# Default is `secretpassword`
ADMIN_PASSWORD_INITIAL=secretpassword
# Specify whether app has to reset the ADMIN_USERNAME's password or not
# Default is NO. Possible options are YES and NO
# If ADMIN_PASSWORD_RESET is YES then the ADMIN_USERNAME will be prompted to change the password from ADMIN_PASSWORD_INITIAL on their next login. This will repeat on every server restart, unless the option is removed / set to NO.
ADMIN_PASSWORD_RESET=NO
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
LOG_FORMAT_MORGAN=

View File

@@ -1,6 +1,5 @@
MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
ALLOWED_DOMAIN=<just domain e.g. example.com >
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http
@@ -14,25 +13,6 @@ 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
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
DB_TYPE=[mongodb|cosmos_mongodb] default considered as mongodb
AUTH_PROVIDERS=[ldap]
LDAP_URL= <LDAP_SERVER_URL>
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
LDAP_BIND_PASSWORD = <password>
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
#default value is 100
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
#default value is 10
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10
ADMIN_USERNAME=secretuser
ADMIN_PASSWORD_INITIAL=secretpassword
ADMIN_PASSWORD_RESET=NO
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
@@ -41,7 +21,6 @@ PYTHON_PATH=/usr/bin/python
R_PATH=/usr/bin/Rscript
SASJS_ROOT=./sasjs_root
DRIVE_LOCATION=./sasjs_root/drive
LOG_FORMAT_MORGAN=common
LOG_LOCATION=./sasjs_root/logs

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

View File

@@ -9,7 +9,7 @@
<div class="content">
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
<!--form container-->
<input type="hidden" name="lt" value="validtoken" aria-hidden="true" />
<input type="hidden" name="lt" value="LT-8-WGkt9EXwICBihaVbxGc92opjufTK1D" aria-hidden="true" />
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />

3810
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "Api of SASjs server",
"main": "./src/server.ts",
"scripts": {
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore && npm run downloadMacros",
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
"prestart": "npm run initial",
"prebuild": "npm run initial",
"start": "NODE_ENV=development nodemon ./src/server.ts",
@@ -17,21 +17,20 @@
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"exe": "npm run build && pkg .",
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sas:copy && npm run web:copy",
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
"public:copy": "cp -r ./public/ ./build/public/",
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
"sas:copy": "cp -r ./sas/ ./build/sas/",
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts",
"downloadMacros": "ts-node ./scripts/downloadMacros.ts"
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
},
"bin": "./build/src/server.js",
"pkg": {
"assets": [
"./build/public/**/*",
"./build/sasjsbuild/**/*",
"./build/sas/**/*",
"./build/sasjscore/**/*",
"./web/build/**/*"
],
"targets": [
@@ -48,8 +47,8 @@
},
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^4.40.1",
"@sasjs/utils": "3.2.0",
"@sasjs/core": "^4.31.3",
"@sasjs/utils": "2.48.1",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
@@ -59,11 +58,10 @@
"helmet": "^5.0.2",
"joi": "^17.4.2",
"jsonwebtoken": "^8.5.1",
"ldapjs": "2.3.3",
"mongoose": "^6.0.12",
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"rate-limiter-flexible": "2.4.1",
"rotating-file-stream": "^3.0.4",
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",
@@ -78,7 +76,7 @@
"@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24",
"@types/jsonwebtoken": "^8.5.5",
"@types/ldapjs": "^2.2.4",
"@types/mongoose-sequence": "^3.0.6",
"@types/morgan": "^1.9.3",
"@types/multer": "^1.4.7",
"@types/node": "^15.12.2",
@@ -86,13 +84,11 @@
"@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9",
"axios": "0.27.2",
"csrf": "^3.1.0",
"dotenv": "^16.0.1",
"dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
"mongodb-memory-server": "8.11.4",
"nodejs-file-downloader": "4.10.2",
"mongodb-memory-server": "^8.0.0",
"nodemon": "^2.0.7",
"pkg": "5.6.0",
"prettier": "^2.3.1",

View File

@@ -40,27 +40,13 @@ components:
clientId:
type: string
userId:
type: string
type: number
format: double
required:
- clientId
- userId
type: object
additionalProperties: false
UpdatePasswordPayload:
properties:
currentPassword:
type: string
description: 'Current Password'
example: currentPasswordString
newPassword:
type: string
description: 'New Password'
example: newPassword
required:
- currentPassword
- newPassword
type: object
additionalProperties: false
ClientPayload:
properties:
clientId:
@@ -71,16 +57,6 @@ components:
type: string
description: 'Client Secret'
example: someRandomCryptoString
accessTokenExpiration:
type: number
format: double
description: 'Number of seconds after which access token will expire. Default is 86400 (1 day)'
example: 86400
refreshTokenExpiration:
type: number
format: double
description: 'Number of seconds after which access token will expire. Default is 2592000 (30 days)'
example: 2592000
required:
- clientId
- clientSecret
@@ -97,47 +73,17 @@ components:
properties:
code:
type: string
description: 'The code to be executed'
example: '* Your Code HERE;'
description: 'Code of program'
example: '* Code HERE;'
runTime:
$ref: '#/components/schemas/RunTimeType'
description: 'The runtime for the code - eg SAS, JS, PY or R'
description: 'runtime for program'
example: js
required:
- code
- runTime
type: object
additionalProperties: false
TriggerCodeResponse:
properties:
sessionId:
type: string
description: "The SessionId is the name of the temporary folder used to store the outputs.\nFor SAS, this would be the SASWORK folder. Can be used to poll job status.\nThis session ID should be used to poll job status."
example: '{ sessionId: ''20241028074744-54132-1730101664824'' }'
required:
- sessionId
type: object
additionalProperties: false
TriggerCodePayload:
properties:
code:
type: string
description: 'The code to be executed'
example: '* Your Code HERE;'
runTime:
$ref: '#/components/schemas/RunTimeType'
description: 'The runtime for the code - eg SAS, JS, PY or R'
example: sas
expiresAfterMins:
type: number
format: double
description: "Amount of minutes after the completion of the job when the session must be\ndestroyed."
example: 15
required:
- code
- runTime
type: object
additionalProperties: false
MemberType.folder:
enum:
- folder
@@ -314,8 +260,9 @@ components:
additionalProperties: false
UserResponse:
properties:
uid:
type: string
id:
type: number
format: double
username:
type: string
displayName:
@@ -323,7 +270,7 @@ components:
isAdmin:
type: boolean
required:
- uid
- id
- username
- displayName
- isAdmin
@@ -331,30 +278,32 @@ components:
additionalProperties: false
GroupResponse:
properties:
uid:
type: string
groupId:
type: number
format: double
name:
type: string
description:
type: string
required:
- uid
- groupId
- name
- description
type: object
additionalProperties: false
UserDetailsResponse:
properties:
uid:
id:
type: number
format: double
displayName:
type: string
username:
type: string
displayName:
type: string
isAdmin:
type: boolean
isActive:
type: boolean
isAdmin:
type: boolean
autoExec:
type: string
groups:
@@ -362,11 +311,11 @@ components:
$ref: '#/components/schemas/GroupResponse'
type: array
required:
- uid
- username
- id
- displayName
- isAdmin
- username
- isActive
- isAdmin
type: object
additionalProperties: false
UserPayload:
@@ -402,8 +351,9 @@ components:
additionalProperties: false
GroupDetailsResponse:
properties:
uid:
type: string
groupId:
type: number
format: double
name:
type: string
description:
@@ -415,7 +365,7 @@ components:
$ref: '#/components/schemas/UserResponse'
type: array
required:
- uid
- groupId
- name
- description
- isActive
@@ -484,8 +434,9 @@ components:
additionalProperties: false
PermissionDetailsResponse:
properties:
uid:
type: string
permissionId:
type: number
format: double
path:
type: string
type:
@@ -497,7 +448,7 @@ components:
group:
$ref: '#/components/schemas/GroupDetailsResponse'
required:
- uid
- permissionId
- path
- type
- setting
@@ -536,8 +487,10 @@ components:
description: 'Indicates the type of principal'
example: user
principalId:
type: string
type: number
format: double
description: 'The id of user or group to which a rule is assigned.'
example: 123
required:
- path
- type
@@ -556,39 +509,6 @@ components:
- setting
type: object
additionalProperties: false
Pick_UserResponse.Exclude_keyofUserResponse.uid__:
properties:
username:
type: string
displayName:
type: string
isAdmin:
type: boolean
required:
- username
- displayName
- isAdmin
type: object
description: 'From T, pick a set of properties whose keys are in the union K'
SessionResponse:
properties:
username:
type: string
displayName:
type: string
isAdmin:
type: boolean
id:
type: string
needsToUpdatePassword:
type: boolean
required:
- username
- displayName
- isAdmin
- id
type: object
additionalProperties: false
ExecutePostRequestPayload:
properties:
_program:
@@ -702,70 +622,6 @@ paths:
-
bearerAuth: []
parameters: []
/SASjsApi/auth/updatePassword:
patch:
operationId: UpdatePassword
responses:
'204':
description: 'No content'
summary: 'Update user''s password.'
tags:
- Auth
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdatePasswordPayload'
/SASjsApi/authConfig:
get:
operationId: GetDetail
responses:
'200':
description: Ok
content:
application/json:
schema: {}
examples:
'Example 1':
value: {ldap: {LDAP_URL: 'ldaps://my.ldap.server:636', LDAP_BIND_DN: 'cn=admin,ou=system,dc=cloudron', LDAP_BIND_PASSWORD: secret, LDAP_USERS_BASE_DN: 'ou=users,dc=cloudron', LDAP_GROUPS_BASE_DN: 'ou=groups,dc=cloudron'}}
summary: 'Gives the detail of Auth Mechanism.'
tags:
- Auth_Config
security:
-
bearerAuth: []
parameters: []
/SASjsApi/authConfig/synchroniseWithLDAP:
post:
operationId: SynchroniseWithLDAP
responses:
'200':
description: Ok
content:
application/json:
schema:
properties:
groupCount: {type: number, format: double}
userCount: {type: number, format: double}
required:
- groupCount
- userCount
type: object
examples:
'Example 1':
value: {users: 5, groups: 3}
summary: 'Synchronises LDAP users and groups with internal DB and returns the count of imported users and groups.'
tags:
- Auth_Config
security:
-
bearerAuth: []
parameters: []
/SASjsApi/client:
post:
operationId: CreateClient
@@ -778,8 +634,8 @@ paths:
$ref: '#/components/schemas/ClientPayload'
examples:
'Example 1':
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiration: 86400}
summary: "Admin only task. Create client with the following attributes:\nClientId,\nClientSecret,\naccessTokenExpiration (optional),\nrefreshTokenExpiration (optional)"
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString}
summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.'
tags:
- Client
security:
@@ -792,27 +648,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ClientPayload'
get:
operationId: GetAllClients
responses:
'200':
description: Ok
content:
application/json:
schema:
items:
$ref: '#/components/schemas/ClientPayload'
type: array
examples:
'Example 1':
value: [{clientId: someClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiration: 86400}, {clientId: someOtherClientID, clientSecret: someOtherRandomCryptoString, accessTokenExpiration: 86400}]
summary: 'Admin only task. Returns the list of all the clients'
tags:
- Client
security:
-
bearerAuth: []
parameters: []
/SASjsApi/code/execute:
post:
operationId: ExecuteCode
@@ -826,7 +661,7 @@ paths:
- {type: string}
- {type: string, format: byte}
description: 'Execute Code on the Specified Runtime'
summary: "Run Code and Return Webout Content, Log and Print output\nThe order of returned parts of the payload is:\n1. Webout (if present)\n2. Logs UUID (used as separator)\n3. Log\n4. Logs UUID (used as separator)\n5. Print (if present and if the runtime is SAS)\nPlease see"
summary: 'Run Code and Return Webout Content and Log'
tags:
- Code
security:
@@ -839,30 +674,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ExecuteCodePayload'
/SASjsApi/code/trigger:
post:
operationId: TriggerCode
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerCodeResponse'
description: 'Trigger Code on the Specified Runtime'
summary: 'Triggers code and returns SessionId immediately - does not wait for job completion'
tags:
- Code
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerCodePayload'
/SASjsApi/drive/deploy:
post:
operationId: Deploy
@@ -1264,7 +1075,7 @@ paths:
type: array
examples:
'Example 1':
value: [{uid: userIdString, username: johnusername, displayName: John, isAdmin: false}, {uid: anotherUserIdString, username: starkusername, displayName: Stark, isAdmin: true}]
value: [{id: 123, username: johnusername, displayName: John, isAdmin: false}, {id: 456, username: starkusername, displayName: Stark, isAdmin: true}]
summary: 'Get list of all users (username, displayname). All users can request this.'
tags:
- User
@@ -1283,7 +1094,7 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse'
examples:
'Example 1':
value: {uid: userIdString, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
summary: 'Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.'
tags:
- User
@@ -1334,7 +1145,7 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse'
examples:
'Example 1':
value: {uid: userIdString, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
tags:
- User
@@ -1385,7 +1196,7 @@ paths:
password:
type: string
type: object
'/SASjsApi/user/{uid}':
'/SASjsApi/user/{userId}':
get:
operationId: GetUser
responses:
@@ -1404,12 +1215,14 @@ paths:
bearerAuth: []
parameters:
-
description: 'The user''s identifier'
in: path
name: uid
name: userId
required: true
schema:
type: string
'/SASjsApi/user/{userId}':
format: double
type: number
example: 1234
patch:
operationId: UpdateUser
responses:
@@ -1421,7 +1234,7 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse'
examples:
'Example 1':
value: {uid: userIdString, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
tags:
- User
@@ -1435,7 +1248,8 @@ paths:
name: userId
required: true
schema:
type: string
format: double
type: number
example: '1234'
requestBody:
required: true
@@ -1461,7 +1275,8 @@ paths:
name: userId
required: true
schema:
type: string
format: double
type: number
example: 1234
requestBody:
required: true
@@ -1486,7 +1301,7 @@ paths:
type: array
examples:
'Example 1':
value: [{uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users'}]
value: [{groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users'}]
summary: 'Get list of all groups (groupName and groupDescription). All users can request this.'
tags:
- Group
@@ -1505,7 +1320,7 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse'
examples:
'Example 1':
value: {uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
summary: 'Create a new group. Admin only.'
tags:
- Group
@@ -1521,7 +1336,7 @@ paths:
$ref: '#/components/schemas/GroupPayload'
'/SASjsApi/group/by/groupname/{name}':
get:
operationId: GetGroupByName
operationId: GetGroupByGroupName
responses:
'200':
description: Ok
@@ -1543,7 +1358,7 @@ paths:
required: true
schema:
type: string
'/SASjsApi/group/{uid}':
'/SASjsApi/group/{groupId}':
get:
operationId: GetGroup
responses:
@@ -1563,11 +1378,12 @@ paths:
-
description: 'The group''s identifier'
in: path
name: uid
name: groupId
required: true
schema:
type: string
example: 12ByteString
format: double
type: number
example: 1234
delete:
operationId: DeleteGroup
responses:
@@ -1589,12 +1405,13 @@ paths:
-
description: 'The group''s identifier'
in: path
name: uid
name: groupId
required: true
schema:
type: string
example: 12ByteString
'/SASjsApi/group/{groupUid}/{userUid}':
format: double
type: number
example: 1234
'/SASjsApi/group/{groupId}/{userId}':
post:
operationId: AddUserToGroup
responses:
@@ -1606,7 +1423,7 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse'
examples:
'Example 1':
value: {uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
summary: 'Add a user to a group. Admin task only.'
tags:
- Group
@@ -1617,18 +1434,21 @@ paths:
-
description: 'The group''s identifier'
in: path
name: groupUid
name: groupId
required: true
schema:
type: string
example: 12ByteString
format: double
type: number
example: '1234'
-
description: 'The user''s identifier'
in: path
name: userUid
name: userId
required: true
schema:
type: string
format: double
type: number
example: '6789'
delete:
operationId: RemoveUserFromGroup
responses:
@@ -1640,8 +1460,8 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse'
examples:
'Example 1':
value: {uid: groupIdString, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
summary: 'Remove a user from a group. Admin task only.'
value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}
summary: 'Remove a user to a group. Admin task only.'
tags:
- Group
security:
@@ -1651,19 +1471,21 @@ paths:
-
description: 'The group''s identifier'
in: path
name: groupUid
name: groupId
required: true
schema:
type: string
example: 12ByteString
format: double
type: number
example: '1234'
-
description: 'The user''s identifier'
in: path
name: userUid
name: userId
required: true
schema:
type: string
example: 12ByteString
format: double
type: number
example: '6789'
/SASjsApi/info:
get:
operationId: Info
@@ -1714,7 +1536,7 @@ paths:
type: array
examples:
'Example 1':
value: [{uid: permissionId1String, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: user1-id, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}, {uid: permissionId2String, path: /SASjsApi/code/execute, type: Route, setting: Grant, group: {uid: group1-id, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}}]
value: [{permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}, {permissionId: 124, path: /SASjsApi/code/execute, type: Route, setting: Grant, group: {groupId: 1, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []}}]
description: "Get the list of permission rules applicable the authenticated user.\nIf the user is an admin, all rules are returned."
summary: 'Get the list of permission rules. If the user is admin, all rules are returned.'
tags:
@@ -1734,7 +1556,7 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse'
examples:
'Example 1':
value: {uid: permissionIdString, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: userIdString, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
value: {permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
summary: 'Create a new permission. Admin only.'
tags:
- Permission
@@ -1748,7 +1570,7 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/RegisterPermissionPayload'
'/SASjsApi/permission/{uid}':
'/SASjsApi/permission/{permissionId}':
patch:
operationId: UpdatePermission
responses:
@@ -1760,7 +1582,7 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse'
examples:
'Example 1':
value: {uid: permissionIdString, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {uid: userIdString, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
value: {permissionId: 123, path: /SASjsApi/code/execute, type: Route, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow', isAdmin: false}}
summary: 'Update permission setting. Admin only'
tags:
- Permission
@@ -1769,11 +1591,14 @@ paths:
bearerAuth: []
parameters:
-
description: 'The permission''s identifier'
in: path
name: uid
name: permissionId
required: true
schema:
type: string
format: double
type: number
example: 1234
requestBody:
required: true
content:
@@ -1793,11 +1618,14 @@ paths:
bearerAuth: []
parameters:
-
description: 'The user''s identifier'
in: path
name: uid
name: permissionId
required: true
schema:
type: string
format: double
type: number
example: 1234
/SASjsApi/session:
get:
operationId: Session
@@ -1807,10 +1635,10 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/SessionResponse'
$ref: '#/components/schemas/UserResponse'
examples:
'Example 1':
value: {id: userIdString, username: johnusername, displayName: John, isAdmin: false, needsToUpdatePassword: false}
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
summary: 'Get session info (username).'
tags:
- Session
@@ -1830,7 +1658,7 @@ paths:
anyOf:
- {type: string}
- {type: string, format: byte}
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts additional URL parameters (converted to session variables)\nand file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
tags:
- STP
@@ -1839,22 +1667,13 @@ paths:
bearerAuth: []
parameters:
-
description: 'Location of Stored Program in SASjs Drive.'
description: 'Location of code in SASjs Drive'
in: query
name: _program
required: true
schema:
type: string
example: /Projects/myApp/some/program
-
description: 'Optional query param for setting debug mode (returns the session log in the response body).'
in: query
name: _debug
required: false
schema:
format: double
type: number
example: 131
post:
operationId: ExecutePostRequest
responses:
@@ -1913,7 +1732,7 @@ paths:
application/json:
schema:
properties:
user: {properties: {needsToUpdatePassword: {type: boolean}, isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {}}, required: [needsToUpdatePassword, isAdmin, displayName, username, id], type: object}
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
@@ -1975,9 +1794,6 @@ tags:
-
name: Auth
description: 'Operations about auth'
-
name: Auth_Config
description: 'Operations about external auth providers'
-
name: Client
description: 'Operations about clients'

View File

@@ -1,39 +0,0 @@
import axios from 'axios'
import Downloader from 'nodejs-file-downloader'
import { createFile, listFilesInFolder } from '@sasjs/utils'
import { sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils/file'
export const downloadMacros = async () => {
const url =
'https://api.github.com/repos/yabwon/SAS_PACKAGES/contents/SPF/Macros'
console.info(`Downloading macros from ${url}`)
await axios
.get(url)
.then(async (res) => {
await downloadFiles(res.data)
})
.catch((err) => {
throw new Error(err)
})
}
const downloadFiles = async function (fileList: any) {
for (const file of fileList) {
const downloader = new Downloader({
url: file.download_url,
directory: sasJSCoreMacros,
fileName: file.path.replace(/^SPF\/Macros/, ''),
cloneFiles: false
})
await downloader.download()
}
const fileNames = await listFilesInFolder(sasJSCoreMacros)
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
}
downloadMacros()

View File

@@ -15,7 +15,7 @@ export const configureCors = (app: Express) => {
whiteList.push(url.replace(/\/$/, ''))
})
process.logger.info('All CORS Requests are enabled for:', whiteList)
console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
}
}

View File

@@ -1,38 +1,22 @@
import { Express, CookieOptions } from 'express'
import { Express } from 'express'
import mongoose from 'mongoose'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import { DatabaseType, ModeType, ProtocolType } from '../utils'
import { ModeType } from '../utils'
import { cookieOptions } from '../app'
export const configureExpressSession = (app: Express) => {
const { MODE, DB_TYPE } = process.env
const { MODE } = process.env
if (MODE === ModeType.Server) {
let store: MongoStore | undefined
if (process.env.NODE_ENV !== 'test') {
if (DB_TYPE === DatabaseType.COSMOS_MONGODB) {
// COSMOS DB requires specific connection options (compatibility mode)
// See: https://www.npmjs.com/package/connect-mongo#set-the-compatibility-mode
store = MongoStore.create({
client: mongoose.connection!.getClient() as any,
autoRemove: 'interval'
})
} else {
store = MongoStore.create({
client: mongoose.connection!.getClient() as any
})
}
}
const { PROTOCOL, ALLOWED_DOMAIN } = process.env
const cookieOptions: CookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
domain: ALLOWED_DOMAIN?.trim() || undefined
store = MongoStore.create({
client: mongoose.connection!.getClient() as any,
collectionName: 'sessions'
})
}
app.use(

View File

@@ -23,7 +23,7 @@ export const configureLogger = (app: Express) => {
path: logsFolder
})
process.logger.info('Writing Logs to :', path.join(logsFolder, filename))
console.log('Writing Logs to :', path.join(logsFolder, filename))
options = { stream: accessLogStream }
}

View File

@@ -1,21 +1,17 @@
import path from 'path'
import express, { ErrorRequestHandler } from 'express'
import express, { ErrorRequestHandler, CookieOptions } from 'express'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
import {
copySASjsCore,
createWeboutSasFile,
getFilesFolder,
getPackagesFolder,
getWebBuildFolder,
instantiateLogger,
loadAppStreamConfig,
ProtocolType,
ReturnCode,
setProcessVariables,
setupFilesFolder,
setupPackagesFolder,
setupUserAutoExec,
setupFolders,
verifyEnvVariables
} from './utils'
import {
@@ -24,7 +20,6 @@ import {
configureLogger,
configureSecurity
} from './app-modules'
import { folderExists } from '@sasjs/utils'
dotenv.config()
@@ -34,8 +29,17 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express()
const { PROTOCOL } = process.env
export const cookieOptions: CookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
const onError: ErrorRequestHandler = (err, req, res, next) => {
process.logger.error(err.stack)
console.error(err.stack)
res.status(500).send('Something broke!')
}
@@ -68,21 +72,8 @@ export default setProcessVariables().then(async () => {
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
app.use(express.urlencoded({ extended: true }))
await setupUserAutoExec()
if (!(await folderExists(getFilesFolder()))) await setupFilesFolder()
if (!(await folderExists(getPackagesFolder()))) await setupPackagesFolder()
const sasautosPath = path.join(process.driveLoc, 'sas', 'sasautos')
if (await folderExists(sasautosPath)) {
process.logger.warn(
`SASAUTOS was not refreshed. To force a refresh, delete the ${sasautosPath} folder`
)
} else {
await copySASjsCore()
await createWeboutSasFile()
}
await setupFolders()
await copySASjsCore()
// loading these modules after setting up variables due to
// multer's usage of process var process.driveLoc

View File

@@ -1,16 +1,4 @@
import express from 'express'
import {
Security,
Route,
Tags,
Example,
Post,
Patch,
Request,
Body,
Query,
Hidden
} from 'tsoa'
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import {
@@ -20,21 +8,19 @@ import {
removeTokensInDB,
saveTokensInDB
} from '../utils'
import Client from '../model/Client'
import User from '../model/User'
@Route('SASjsApi/auth')
@Tags('Auth')
export class AuthController {
static authCodes: { [key: string]: { [key: string]: string } } = {}
static saveCode = (userId: string, clientId: string, code: string) => {
static saveCode = (userId: number, clientId: string, code: string) => {
if (AuthController.authCodes[userId])
return (AuthController.authCodes[userId][clientId] = code)
AuthController.authCodes[userId] = { [clientId]: code }
return AuthController.authCodes[userId][clientId]
}
static deleteCode = (userId: string, clientId: string) =>
static deleteCode = (userId: number, clientId: string) =>
delete AuthController.authCodes[userId][clientId]
/**
@@ -75,18 +61,6 @@ export class AuthController {
public async logout(@Query() @Hidden() data?: InfoJWT) {
return logout(data!)
}
/**
* @summary Update user's password.
*/
@Security('bearerAuth')
@Patch('updatePassword')
public async updatePassword(
@Request() req: express.Request,
@Body() body: UpdatePasswordPayload
) {
return updatePassword(req, body)
}
}
const token = async (data: any): Promise<TokenResponse> => {
@@ -109,17 +83,8 @@ const token = async (data: any): Promise<TokenResponse> => {
}
}
const client = await Client.findOne({ clientId })
if (!client) throw new Error('Invalid clientId.')
const accessToken = generateAccessToken(
userInfo,
client.accessTokenExpiration
)
const refreshToken = generateRefreshToken(
userInfo,
client.refreshTokenExpiration
)
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
@@ -127,17 +92,8 @@ const token = async (data: any): Promise<TokenResponse> => {
}
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
const client = await Client.findOne({ clientId: userInfo.clientId })
if (!client) throw new Error('Invalid clientId.')
const accessToken = generateAccessToken(
userInfo,
client.accessTokenExpiration
)
const refreshToken = generateRefreshToken(
userInfo,
client.refreshTokenExpiration
)
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
await saveTokensInDB(
userInfo.userId,
@@ -153,40 +109,6 @@ const logout = async (userInfo: InfoJWT) => {
await removeTokensInDB(userInfo.userId, userInfo.clientId)
}
const updatePassword = async (
req: express.Request,
data: UpdatePasswordPayload
) => {
const { currentPassword, newPassword } = data
const userId = req.user?.userId
const dbUser = await User.findOne({ _id: userId })
if (!dbUser)
throw {
code: 404,
message: `User not found!`
}
if (dbUser?.authProvider) {
throw {
code: 405,
message:
'Can not update password of user that is created by an external auth provider.'
}
}
const validPass = dbUser.comparePassword(currentPassword)
if (!validPass)
throw {
code: 403,
message: `Invalid current password!`
}
dbUser.password = User.hashPassword(newPassword)
dbUser.needsToUpdatePassword = false
await dbUser.save()
}
interface TokenPayload {
/**
* Client ID
@@ -213,19 +135,6 @@ interface TokenResponse {
refreshToken: string
}
interface UpdatePasswordPayload {
/**
* Current Password
* @example "currentPasswordString"
*/
currentPassword: string
/**
* New Password
* @example "newPassword"
*/
newPassword: string
}
const verifyAuthCode = async (
clientId: string,
code: string

View File

@@ -1,186 +0,0 @@
import express from 'express'
import { Security, Route, Tags, Get, Post, Example } from 'tsoa'
import { LDAPClient, LDAPUser, LDAPGroup, AuthProviderType } from '../utils'
import { randomBytes } from 'crypto'
import User from '../model/User'
import Group from '../model/Group'
import Permission from '../model/Permission'
@Security('bearerAuth')
@Route('SASjsApi/authConfig')
@Tags('Auth_Config')
export class AuthConfigController {
/**
* @summary Gives the detail of Auth Mechanism.
*
*/
@Example({
ldap: {
LDAP_URL: 'ldaps://my.ldap.server:636',
LDAP_BIND_DN: 'cn=admin,ou=system,dc=cloudron',
LDAP_BIND_PASSWORD: 'secret',
LDAP_USERS_BASE_DN: 'ou=users,dc=cloudron',
LDAP_GROUPS_BASE_DN: 'ou=groups,dc=cloudron'
}
})
@Get('/')
public getDetail() {
return getAuthConfigDetail()
}
/**
* @summary Synchronises LDAP users and groups with internal DB and returns the count of imported users and groups.
*
*/
@Example({
users: 5,
groups: 3
})
@Post('/synchroniseWithLDAP')
public async synchroniseWithLDAP() {
return synchroniseWithLDAP()
}
}
const synchroniseWithLDAP = async () => {
process.logger.info('Syncing LDAP with internal DB')
const permissions = await Permission.get({})
await Permission.deleteMany()
await User.deleteMany({ authProvider: AuthProviderType.LDAP })
await Group.deleteMany({ authProvider: AuthProviderType.LDAP })
const ldapClient = await LDAPClient.init()
process.logger.info('fetching LDAP users')
const users = await ldapClient.getAllLDAPUsers()
process.logger.info('inserting LDAP users to DB')
const existingUsers: string[] = []
const importedUsers: LDAPUser[] = []
for (const user of users) {
const usernameExists = await User.findOne({ username: user.username })
if (usernameExists) {
existingUsers.push(user.username)
continue
}
const hashPassword = User.hashPassword(randomBytes(64).toString('hex'))
await User.create({
displayName: user.displayName,
username: user.username,
password: hashPassword,
authProvider: AuthProviderType.LDAP,
needsToUpdatePassword: false
})
importedUsers.push(user)
}
if (existingUsers.length > 0) {
process.logger.info(
'Failed to insert following users as they already exist in DB:'
)
existingUsers.forEach((user) => process.logger.log(`* ${user}`))
}
process.logger.info('fetching LDAP groups')
const groups = await ldapClient.getAllLDAPGroups()
process.logger.info('inserting LDAP groups to DB')
const existingGroups: string[] = []
const importedGroups: LDAPGroup[] = []
for (const group of groups) {
const groupExists = await Group.findOne({ name: group.name })
if (groupExists) {
existingGroups.push(group.name)
continue
}
await Group.create({
name: group.name,
authProvider: AuthProviderType.LDAP
})
importedGroups.push(group)
}
if (existingGroups.length > 0) {
process.logger.info(
'Failed to insert following groups as they already exist in DB:'
)
existingGroups.forEach((group) => process.logger.log(`* ${group}`))
}
process.logger.info('associating users and groups')
for (const group of importedGroups) {
const dbGroup = await Group.findOne({ name: group.name })
if (dbGroup) {
for (const member of group.members) {
const user = importedUsers.find((user) => user.uid === member)
if (user) {
const dbUser = await User.findOne({ username: user.username })
if (dbUser) await dbGroup.addUser(dbUser)
}
}
}
}
process.logger.info('setting permissions')
for (const permission of permissions) {
const newPermission = new Permission({
path: permission.path,
type: permission.type,
setting: permission.setting
})
if (permission.user) {
const dbUser = await User.findOne({ username: permission.user.username })
if (dbUser) newPermission.user = dbUser._id
} else if (permission.group) {
const dbGroup = await Group.findOne({ name: permission.group.name })
if (dbGroup) newPermission.group = dbGroup._id
}
await newPermission.save()
}
process.logger.info('LDAP synchronization completed!')
return {
userCount: importedUsers.length,
groupCount: importedGroups.length
}
}
const getAuthConfigDetail = () => {
const { AUTH_PROVIDERS } = process.env
const returnObj: any = {}
if (AUTH_PROVIDERS === AuthProviderType.LDAP) {
const {
LDAP_URL,
LDAP_BIND_DN,
LDAP_BIND_PASSWORD,
LDAP_USERS_BASE_DN,
LDAP_GROUPS_BASE_DN
} = process.env
returnObj.ldap = {
LDAP_URL: LDAP_URL ?? '',
LDAP_BIND_DN: LDAP_BIND_DN ?? '',
LDAP_BIND_PASSWORD: LDAP_BIND_PASSWORD ?? '',
LDAP_USERS_BASE_DN: LDAP_USERS_BASE_DN ?? '',
LDAP_GROUPS_BASE_DN: LDAP_GROUPS_BASE_DN ?? ''
}
}
return returnObj
}

View File

@@ -1,27 +1,18 @@
import { Security, Route, Tags, Example, Post, Body, Get } from 'tsoa'
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
import Client, {
ClientPayload,
NUMBER_OF_SECONDS_IN_A_DAY
} from '../model/Client'
import Client, { ClientPayload } from '../model/Client'
@Security('bearerAuth')
@Route('SASjsApi/client')
@Tags('Client')
export class ClientController {
/**
* @summary Admin only task. Create client with the following attributes:
* ClientId,
* ClientSecret,
* accessTokenExpiration (optional),
* refreshTokenExpiration (optional)
* @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task.
*
*/
@Example<ClientPayload>({
clientId: 'someFormattedClientID1234',
clientSecret: 'someRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
clientSecret: 'someRandomCryptoString'
})
@Post('/')
public async createClient(
@@ -29,37 +20,10 @@ export class ClientController {
): Promise<ClientPayload> {
return createClient(body)
}
/**
* @summary Admin only task. Returns the list of all the clients
*/
@Example<ClientPayload[]>([
{
clientId: 'someClientID1234',
clientSecret: 'someRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
},
{
clientId: 'someOtherClientID',
clientSecret: 'someOtherRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
])
@Get('/')
public async getAllClients(): Promise<ClientPayload[]> {
return getAllClients()
}
}
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
const {
clientId,
clientSecret,
accessTokenExpiration,
refreshTokenExpiration
} = data
const createClient = async (data: any): Promise<ClientPayload> => {
const { clientId, clientSecret } = data
// Checking if client is already in the database
const clientExist = await Client.findOne({ clientId })
@@ -68,27 +32,13 @@ const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
// Create a new client
const client = new Client({
clientId,
clientSecret,
accessTokenExpiration,
refreshTokenExpiration
clientSecret
})
const savedClient = await client.save()
return {
clientId: savedClient.clientId,
clientSecret: savedClient.clientSecret,
accessTokenExpiration: savedClient.accessTokenExpiration,
refreshTokenExpiration: savedClient.refreshTokenExpiration
clientSecret: savedClient.clientSecret
}
}
const getAllClients = async (): Promise<ClientPayload[]> => {
return Client.find({}).select({
_id: 0,
clientId: 1,
clientSecret: 1,
accessTokenExpiration: 1,
refreshTokenExpiration: 1
})
}

View File

@@ -1,69 +1,34 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecutionController, getSessionController } from './internal'
import { ExecutionController } from './internal'
import {
getPreProgramVariables,
getUserAutoExec,
ModeType,
parseLogToArray,
RunTimeType
} from '../utils'
interface ExecuteCodePayload {
/**
* The code to be executed
* @example "* Your Code HERE;"
* Code of program
* @example "* Code HERE;"
*/
code: string
/**
* The runtime for the code - eg SAS, JS, PY or R
* runtime for program
* @example "js"
*/
runTime: RunTimeType
}
interface TriggerCodePayload {
/**
* The code to be executed
* @example "* Your Code HERE;"
*/
code: string
/**
* The runtime for the code - eg SAS, JS, PY or R
* @example "sas"
*/
runTime: RunTimeType
/**
* Amount of minutes after the completion of the job when the session must be
* destroyed.
* @example 15
*/
expiresAfterMins?: number
}
interface TriggerCodeResponse {
/**
* The SessionId is the name of the temporary folder used to store the outputs.
* For SAS, this would be the SASWORK folder. Can be used to poll job status.
* This session ID should be used to poll job status.
* @example "{ sessionId: '20241028074744-54132-1730101664824' }"
*/
sessionId: string
}
@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('Code')
export class CodeController {
/**
* Execute Code on the Specified Runtime
* @summary Run Code and Return Webout Content, Log and Print output
* The order of returned parts of the payload is:
* 1. Webout (if present)
* 2. Logs UUID (used as separator)
* 3. Log
* 4. Logs UUID (used as separator)
* 5. Print (if present and if the runtime is SAS)
* Please see @sasjs/server/api/src/controllers/internal/Execution.ts for more information
* @summary Run Code and Return Webout Content and Log
*/
@Post('/execute')
public async executeCode(
@@ -72,18 +37,6 @@ export class CodeController {
): Promise<string | Buffer> {
return executeCode(request, body)
}
/**
* Trigger Code on the Specified Runtime
* @summary Triggers code and returns SessionId immediately - does not wait for job completion
*/
@Post('/trigger')
public async triggerCode(
@Request() request: express.Request,
@Body() body: TriggerCodePayload
): Promise<TriggerCodeResponse> {
return triggerCode(request, body)
}
}
const executeCode = async (
@@ -102,8 +55,7 @@ const executeCode = async (
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime,
includePrintOutput: true
runTime: runTime
})
return result
@@ -116,49 +68,3 @@ const executeCode = async (
}
}
}
const triggerCode = async (
req: express.Request,
{ code, runTime, expiresAfterMins }: TriggerCodePayload
): Promise<{ sessionId: string }> => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: await getUserAutoExec()
// get session controller based on runTime
const sessionController = getSessionController(runTime)
// get session
const session = await sessionController.getSession()
// add expiresAfterMins to session if provided
if (expiresAfterMins) {
// expiresAfterMins.used is set initially to false
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
}
try {
// call executeProgram method of ExecutionController without awaiting
new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime,
includePrintOutput: true,
session // session is provided
})
// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -12,29 +12,27 @@ import {
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
import User from '../model/User'
import { GetUserBy, UserResponse } from './user'
import { UserResponse } from './user'
export interface GroupResponse {
uid: string
groupId: number
name: string
description: string
}
export interface GroupDetailsResponse extends GroupResponse {
export interface GroupDetailsResponse {
groupId: number
name: string
description: string
isActive: boolean
users: UserResponse[]
}
interface GetGroupBy {
_id?: string
groupId?: number
name?: string
}
enum GroupAction {
AddUser = 'addUser',
RemoveUser = 'removeUser'
}
@Security('bearerAuth')
@Route('SASjsApi/group')
@Tags('Group')
@@ -45,7 +43,7 @@ export class GroupController {
*/
@Example<GroupResponse[]>([
{
uid: 'groupIdString',
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users'
}
@@ -60,7 +58,7 @@ export class GroupController {
*
*/
@Example<GroupDetailsResponse>({
uid: 'groupIdString',
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
@@ -79,7 +77,7 @@ export class GroupController {
* @example dcgroup
*/
@Get('by/groupname/{name}')
public async getGroupByName(
public async getGroupByGroupName(
@Path() name: string
): Promise<GroupDetailsResponse> {
return getGroup({ name })
@@ -87,79 +85,81 @@ export class GroupController {
/**
* @summary Get list of members of a group (userName). All users can request this.
* @param uid The group's identifier
* @example uid "12ByteString"
* @param groupId The group's identifier
* @example groupId 1234
*/
@Get('{uid}')
public async getGroup(@Path() uid: string): Promise<GroupDetailsResponse> {
return getGroup({ _id: uid })
@Get('{groupId}')
public async getGroup(
@Path() groupId: number
): Promise<GroupDetailsResponse> {
return getGroup({ groupId })
}
/**
* @summary Add a user to a group. Admin task only.
* @param groupUid The group's identifier
* @example groupUid "12ByteString"
* @param userUid The user's identifier
* @example userId "12ByteString"
* @param groupId The group's identifier
* @example groupId "1234"
* @param userId The user's identifier
* @example userId "6789"
*/
@Example<GroupDetailsResponse>({
uid: 'groupIdString',
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
users: []
})
@Post('{groupUid}/{userUid}')
@Post('{groupId}/{userId}')
public async addUserToGroup(
@Path() groupUid: string,
@Path() userUid: string
@Path() groupId: number,
@Path() userId: number
): Promise<GroupDetailsResponse> {
return addUserToGroup(groupUid, userUid)
return addUserToGroup(groupId, userId)
}
/**
* @summary Remove a user from a group. Admin task only.
* @param groupUid The group's identifier
* @example groupUid "12ByteString"
* @param userUid The user's identifier
* @example userUid "12ByteString"
* @summary Remove a user to a group. Admin task only.
* @param groupId The group's identifier
* @example groupId "1234"
* @param userId The user's identifier
* @example userId "6789"
*/
@Example<GroupDetailsResponse>({
uid: 'groupIdString',
groupId: 123,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
users: []
})
@Delete('{groupUid}/{userUid}')
@Delete('{groupId}/{userId}')
public async removeUserFromGroup(
@Path() groupUid: string,
@Path() userUid: string
@Path() groupId: number,
@Path() userId: number
): Promise<GroupDetailsResponse> {
return removeUserFromGroup(groupUid, userUid)
return removeUserFromGroup(groupId, userId)
}
/**
* @summary Delete a group. Admin task only.
* @param uid The group's identifier
* @example uid "12ByteString"
* @param groupId The group's identifier
* @example groupId 1234
*/
@Delete('{uid}')
public async deleteGroup(@Path() uid: string) {
const group = await Group.findOne({ _id: uid })
if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
return await group.remove()
@Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) {
const group = await Group.findOne({ groupId })
if (group) return await group.remove()
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
}
}
const getAllGroups = async (): Promise<GroupResponse[]> =>
await Group.find({}).select('uid name description').exec()
await Group.find({})
.select({ _id: 0, groupId: 1, name: 1, description: 1 })
.exec()
const createGroup = async ({
name,
@@ -184,7 +184,7 @@ const createGroup = async ({
const savedGroup = await group.save()
return {
uid: savedGroup.uid,
groupId: savedGroup.groupId,
name: savedGroup.name,
description: savedGroup.description,
isActive: savedGroup.isActive,
@@ -195,12 +195,11 @@ const createGroup = async ({
const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
const group = (await Group.findOne(
findBy,
'uid name description isActive users'
'groupId name description isActive users -_id'
).populate(
'users',
'uid username displayName isAdmin'
'id username displayName isAdmin -_id'
)) as unknown as GroupDetailsResponse
if (!group)
throw {
code: 404,
@@ -209,7 +208,7 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
}
return {
uid: group.uid,
groupId: group.groupId,
name: group.name,
description: group.description,
isActive: group.isActive,
@@ -218,23 +217,23 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
}
const addUserToGroup = async (
groupUid: string,
userUid: string
groupId: number,
userId: number
): Promise<GroupDetailsResponse> =>
updateUsersListInGroup(groupUid, userUid, GroupAction.AddUser)
updateUsersListInGroup(groupId, userId, 'addUser')
const removeUserFromGroup = async (
groupUid: string,
userUid: string
groupId: number,
userId: number
): Promise<GroupDetailsResponse> =>
updateUsersListInGroup(groupUid, userUid, GroupAction.RemoveUser)
updateUsersListInGroup(groupId, userId, 'removeUser')
const updateUsersListInGroup = async (
groupUid: string,
userUid: string,
action: GroupAction
groupId: number,
userId: number,
action: 'addUser' | 'removeUser'
): Promise<GroupDetailsResponse> => {
const group = await Group.findOne({ _id: groupUid })
const group = await Group.findOne({ groupId })
if (!group)
throw {
code: 404,
@@ -249,14 +248,7 @@ const updateUsersListInGroup = async (
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
}
if (group.authProvider)
throw {
code: 405,
status: 'Method Not Allowed',
message: `Can't add/remove user to group created by external auth provider.`
}
const user = await User.findOne({ _id: userUid })
const user = await User.findOne({ id: userId })
if (!user)
throw {
code: 404,
@@ -264,15 +256,8 @@ const updateUsersListInGroup = async (
message: 'User not found.'
}
if (user.authProvider)
throw {
code: 405,
status: 'Method Not Allowed',
message: `Can't add/remove user to group created by external auth provider.`
}
const updatedGroup =
action === GroupAction.AddUser
action === 'addUser'
? await group.addUser(user)
: await group.removeUser(user)
@@ -284,7 +269,7 @@ const updateUsersListInGroup = async (
}
return {
uid: updatedGroup.uid,
groupId: updatedGroup.groupId,
name: updatedGroup.name,
description: updatedGroup.description,
isActive: updatedGroup.isActive,

View File

@@ -1,5 +1,4 @@
export * from './auth'
export * from './authConfig'
export * from './client'
export * from './code'
export * from './drive'

View File

@@ -28,12 +28,10 @@ interface ExecuteFileParams {
returnJson?: boolean
session?: Session
runTime: RunTimeType
forceStringResult?: boolean
}
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
program: string
includePrintOutput?: boolean
}
export class ExecutionController {
@@ -44,8 +42,7 @@ export class ExecutionController {
otherArgs,
returnJson,
session,
runTime,
forceStringResult
runTime
}: ExecuteFileParams) {
const program = await readFile(programPath)
@@ -56,8 +53,7 @@ export class ExecutionController {
otherArgs,
returnJson,
session,
runTime,
forceStringResult
runTime
})
}
@@ -67,9 +63,7 @@ export class ExecutionController {
vars,
otherArgs,
session: sessionByFileUpload,
runTime,
forceStringResult,
includePrintOutput
runTime
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
const sessionController = getSessionController(runTime)
@@ -107,15 +101,10 @@ export class ExecutionController {
? await readFile(headersPath)
: ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
if (isDebugOn(vars)) {
httpHeaders['content-type'] = 'text/plain'
}
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
const webout = (await fileExists(weboutPath))
? fileResponse && !forceStringResult
? fileResponse
? await readFileBinary(weboutPath)
: await readFile(weboutPath)
: ''
@@ -123,29 +112,12 @@ export class ExecutionController {
// it should be deleted by scheduleSessionDestroy
session.inUse = false
const resultParts = []
// INFO: webout can be a Buffer, that is why it's length should be checked to determine if it is empty
if (webout && webout.length !== 0) resultParts.push(webout)
// INFO: log separator wraps the log from the beginning and the end
resultParts.push(process.logsUUID)
resultParts.push(log)
resultParts.push(process.logsUUID)
if (includePrintOutput && runTime === RunTimeType.SAS) {
const printOutputPath = path.join(session.path, 'output.lst')
const printOutput = (await fileExists(printOutputPath))
? await readFile(printOutputPath)
: ''
if (printOutput) resultParts.push(printOutput)
}
return {
httpHeaders,
result:
isDebugOn(vars) || session.crashed ? resultParts.join(`\n`) : webout
isDebugOn(vars) || session.crashed
? `${webout}\n${process.logsUUID}\n${log}`
: webout
}
}

View File

@@ -3,7 +3,6 @@ import { Session } from '../../types'
import { promisify } from 'util'
import { execFile } from 'child_process'
import {
getPackagesFolder,
getSessionsFolder,
generateUniqueFileName,
sysInitCompiledPath,
@@ -14,7 +13,8 @@ import {
createFile,
fileExists,
generateTimestamp,
readFile
readFile,
isWindows
} from '@sasjs/utils'
const execFilePromise = promisify(execFile)
@@ -49,7 +49,7 @@ export class SessionController {
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'content-type: text/html; charset=utf-8')
await createFile(headersPath, 'Content-type: text/plain')
this.sessions.push(session)
return session
@@ -93,7 +93,7 @@ export class SASSessionController extends SessionController {
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'content-type: text/html; charset=utf-8\n')
await createFile(headersPath, 'Content-type: text/plain')
// we do not want to leave sessions running forever
// we clean them up after a predefined period, if unused
@@ -104,8 +104,7 @@ export class SASSessionController extends SessionController {
// the autoexec file is executed on SAS startup
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
const contentForAutoExec = `filename packages "${getPackagesFolder()}";
/* compiled systemInit */
const contentForAutoExec = `/* compiled systemInit */
${compiledSystemInitContent}
/* autoexec */
${autoExecContent}`
@@ -133,24 +132,23 @@ ${autoExecContent}`
session.path,
'-AUTOEXEC',
autoExecPath,
process.sasLoc!.endsWith('sas.exe') ? '-nologo' : '',
isWindows() ? '-nologo' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
process.sasLoc!.endsWith('sas.exe') ? '-NOPRNGETLIST' : '',
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
])
.then(() => {
session.completed = true
process.logger.info('session completed', session)
console.log('session completed', session)
})
.catch((err) => {
session.completed = true
session.crashed = err.toString()
process.logger.error('session crashed', session.id, session.crashed)
console.log('session crashed', session.id, session.crashed)
})
// we have a triggered session - add to array
@@ -170,10 +168,7 @@ ${autoExecContent}`
while ((await fileExists(codeFilePath)) && !session.crashed) {}
if (session.crashed)
process.logger.error(
'session crashed! while waiting to be ready',
session.crashed
)
console.log('session crashed! while waiting to be ready', session.crashed)
session.ready = true
}
@@ -189,52 +184,29 @@ ${autoExecContent}`
}
private scheduleSessionDestroy(session: Session) {
setTimeout(
async () => {
if (session.inUse) {
// adding 10 more minutes
const newDeathTimeStamp =
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
setTimeout(async () => {
if (session.inUse) {
// adding 10 more minutes
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
this.scheduleSessionDestroy(session)
} else {
const { expiresAfterMins } = session
// delay session destroy if expiresAfterMins present
if (expiresAfterMins && !expiresAfterMins.used) {
// calculate session death time using expiresAfterMins
const newDeathTimeStamp =
parseInt(session.deathTimeStamp) +
expiresAfterMins.mins * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
// set expiresAfterMins to true to avoid using it again
session.expiresAfterMins!.used = true
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}
},
parseInt(session.deathTimeStamp) - new Date().getTime() - 100
)
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
}
}
export const getSessionController = (
runTime: RunTimeType
): SessionController => {
if (runTime === RunTimeType.SAS) {
process.sasSessionController =
process.sasSessionController || new SASSessionController()
return process.sasSessionController
}
if (process.sessionController) return process.sessionController
process.sessionController =
process.sessionController || new SessionController()
runTime === RunTimeType.SAS
? new SASSessionController()
: new SessionController()
return process.sessionController
}

View File

@@ -15,7 +15,7 @@ export const createJSProgram = async (
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) =>
`${computed}const ${key} = \`${vars[key]}\`;\n`,
`${computed}const ${key} = '${vars[key]}';\n`,
''
)

View File

@@ -40,6 +40,8 @@ export const createSASProgram = async (
%mend;
%_sasjs_server_init()
proc printto print="%sysfunc(getoption(log))";
run;
`
program = `

View File

@@ -1,6 +1,6 @@
import path from 'path'
import { WriteStream, createWriteStream } from 'fs'
import { execFile } from 'child_process'
import fs from 'fs'
import { execFileSync } from 'child_process'
import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
@@ -105,58 +105,30 @@ export const processProgram = async (
throw new Error('Invalid runtime!')
}
await createFile(codePath, program)
try {
await createFile(codePath, program)
// create a stream that will write to console outputs to log file
const writeStream = createWriteStream(logPath)
// waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open')
// create a stream that will write to console outputs to log file
const writeStream = fs.createWriteStream(logPath)
await execFilePromise(executablePath, [codePath], writeStream)
.then(() => {
session.completed = true
process.logger.info('session completed', session)
})
.catch((err) => {
session.completed = true
session.crashed = err.toString()
process.logger.error('session crashed', session.id, session.crashed)
// waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open')
execFileSync(executablePath, [codePath], {
stdio: ['ignore', writeStream, writeStream]
})
// copy the code file to log and end write stream
writeStream.end(program)
// copy the code file to log and end write stream
writeStream.end(program)
session.completed = true
console.log('session completed', session)
} catch (err: any) {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
}
}
}
/**
* Promisified child_process.execFile
*
* @param file - The name or path of the executable file to run.
* @param args - List of string arguments.
* @param writeStream - Child process stdout and stderr will be piped to it.
*
* @returns {Promise<{ stdout: string, stderr: string }>}
*/
const execFilePromise = (
file: string,
args: string[],
writeStream: WriteStream
): Promise<{ stdout: string; stderr: string }> => {
return new Promise((resolve, reject) => {
const child = execFile(file, args, (err, stdout, stderr) => {
if (err) reject(err)
resolve({ stdout, stderr })
})
child.stdout?.on('data', (data) => {
writeStream.write(data)
})
child.stderr?.on('data', (data) => {
writeStream.write(data)
})
})
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

View File

@@ -2,16 +2,6 @@ import { readFile } from '@sasjs/utils'
import express from 'express'
import path from 'path'
import { Request, Post, Get } from 'tsoa'
import dotenv from 'dotenv'
import { ExecutionController } from './internal'
import {
getPreProgramVariables,
getRunTimeAndFilePath,
makeFilesNamesMap
} from '../utils'
import { MulterFile } from '../types/Upload'
dotenv.config()
export interface Sas9Response {
content: string
@@ -26,17 +16,9 @@ export interface MockFileRead {
export class MockSas9Controller {
private loggedIn: string | undefined
private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
@Get('/SASStoredProcess')
public async sasStoredProcess(
@Request() req: express.Request
): Promise<Sas9Response> {
const username = req.query._username?.toString() || undefined
const password = req.query._password?.toString() || undefined
if (username && password) this.loggedIn = req.body.username
public async sasStoredProcess(): Promise<Sas9Response> {
if (!this.loggedIn) {
return {
content: '',
@@ -44,87 +26,17 @@ export class MockSas9Controller {
}
}
let program = req.query._program?.toString() || undefined
const filePath: string[] = program
? program.replace('/', '').split('/')
: ['generic', 'sas-stored-process']
if (program) {
return await getMockResponseFromFile([
process.cwd(),
this.mocksPath,
'sas9',
...filePath
])
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'generic',
'sas9',
...filePath
])
}
@Get('/SASStoredProcess/do')
public async sasStoredProcessDoGet(
@Request() req: express.Request
): Promise<Sas9Response> {
const username = req.query._username?.toString() || undefined
const password = req.query._password?.toString() || undefined
if (username && password) this.loggedIn = username
if (!this.loggedIn) {
return {
content: '',
redirect: '/SASLogon/login'
}
}
const program = req.query._program ?? req.body?._program
const filePath: string[] = ['generic', 'sas-stored-process']
if (program) {
const vars = { ...req.query, ...req.body, _requestMethod: req.method }
const otherArgs = {}
try {
const { codePath, runTime } = await getRunTimeAndFilePath(
program + '.js'
)
const result = await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: vars,
otherArgs: otherArgs,
runTime,
forceStringResult: true
})
return {
content: result.result as string
}
} catch (err) {
process.logger.error('err', err)
}
return {
content: 'No webout returned.'
}
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
...filePath
'sas-stored-process'
])
}
@Post('/SASStoredProcess/do/')
public async sasStoredProcessDoPost(
public async sasStoredProcessDo(
@Request() req: express.Request
): Promise<Sas9Response> {
if (!this.loggedIn) {
@@ -141,38 +53,23 @@ export class MockSas9Controller {
}
}
const program = req.query._program ?? req.body?._program
const vars = {
...req.query,
...req.body,
_requestMethod: req.method,
_driveLoc: process.driveLoc
}
const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[])
: null
const otherArgs = { filesNamesMap: filesNamesMap }
const { codePath, runTime } = await getRunTimeAndFilePath(program + '.js')
try {
const result = await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: vars,
otherArgs: otherArgs,
runTime,
session: req.sasjsSession,
forceStringResult: true
})
let program = req.query._program?.toString() || ''
program = program.replace('/', '')
return {
content: result.result as string
}
} catch (err) {
process.logger.error('err', err)
const content = await getMockResponseFromFile([
process.cwd(),
'mocks',
...program.split('/')
])
if (content.error) {
return content
}
const parsedContent = parseJsonIfValid(content.content)
return {
content: 'No webout returned.'
content: parsedContent
}
}
@@ -188,8 +85,8 @@ export class MockSas9Controller {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'sas9',
'logged-in'
])
}
@@ -198,27 +95,21 @@ export class MockSas9Controller {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'sas9',
'login'
])
}
@Post('/SASLogon/login')
public async loginPost(req: express.Request): Promise<Sas9Response> {
if (req.body.lt && req.body.lt !== 'validtoken')
return {
content: '',
redirect: '/SASLogon/login'
}
this.loggedIn = req.body.username
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'sas9',
'logged-in'
])
}
@@ -231,8 +122,8 @@ export class MockSas9Controller {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'sas9',
'public-access-denied'
])
}
@@ -240,8 +131,8 @@ export class MockSas9Controller {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
'generic',
'sas9',
'logged-out'
])
}
@@ -261,6 +152,23 @@ export class MockSas9Controller {
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
}
/**
* If JSON is valid it will be parsed otherwise will return text unaltered
* @param content string to be parsed
* @returns JSON or string
*/
const parseJsonIfValid = (content: string) => {
let fileContent = ''
try {
fileContent = JSON.parse(content)
} catch (err: any) {
fileContent = content
}
return fileContent
}
const getMockResponseFromFile = async (
filePath: string[]
): Promise<MockFileRead> => {
@@ -269,7 +177,7 @@ const getMockResponseFromFile = async (
let file = await readFile(filePathParsed).catch((err: any) => {
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
process.logger.error(errMsg)
console.error(errMsg)
error = true

View File

@@ -56,9 +56,9 @@ interface RegisterPermissionPayload {
principalType: PrincipalType
/**
* The id of user or group to which a rule is assigned.
* @example 'groupIdString'
* @example 123
*/
principalId: string
principalId: number
}
interface UpdatePermissionPayload {
@@ -70,7 +70,7 @@ interface UpdatePermissionPayload {
}
export interface PermissionDetailsResponse {
uid: string
permissionId: number
path: string
type: string
setting: string
@@ -91,24 +91,24 @@ export class PermissionController {
*/
@Example<PermissionDetailsResponse[]>([
{
uid: 'permissionId1String',
permissionId: 123,
path: '/SASjsApi/code/execute',
type: 'Route',
setting: 'Grant',
user: {
uid: 'user1-id',
id: 1,
username: 'johnSnow01',
displayName: 'John Snow',
isAdmin: false
}
},
{
uid: 'permissionId2String',
permissionId: 124,
path: '/SASjsApi/code/execute',
type: 'Route',
setting: 'Grant',
group: {
uid: 'group1-id',
groupId: 1,
name: 'DCGroup',
description: 'This group represents Data Controller Users',
isActive: true,
@@ -128,12 +128,12 @@ export class PermissionController {
*
*/
@Example<PermissionDetailsResponse>({
uid: 'permissionIdString',
permissionId: 123,
path: '/SASjsApi/code/execute',
type: 'Route',
setting: 'Grant',
user: {
uid: 'userIdString',
id: 1,
username: 'johnSnow01',
displayName: 'John Snow',
isAdmin: false
@@ -149,36 +149,36 @@ export class PermissionController {
/**
* @summary Update permission setting. Admin only
* @param permissionId The permission's identifier
* @example permissionId "permissionIdString"
* @example permissionId 1234
*/
@Example<PermissionDetailsResponse>({
uid: 'permissionIdString',
permissionId: 123,
path: '/SASjsApi/code/execute',
type: 'Route',
setting: 'Grant',
user: {
uid: 'userIdString',
id: 1,
username: 'johnSnow01',
displayName: 'John Snow',
isAdmin: false
}
})
@Patch('{uid}')
@Patch('{permissionId}')
public async updatePermission(
@Path() uid: string,
@Path() permissionId: number,
@Body() body: UpdatePermissionPayload
): Promise<PermissionDetailsResponse> {
return updatePermission(uid, body)
return updatePermission(permissionId, body)
}
/**
* @summary Delete a permission. Admin only.
* @param permissionId The user's identifier
* @example permissionId "permissionIdString"
* @example permissionId 1234
*/
@Delete('{uid}')
public async deletePermission(@Path() uid: string) {
return deletePermission(uid)
@Delete('{permissionId}')
public async deletePermission(@Path() permissionId: number) {
return deletePermission(permissionId)
}
}
@@ -191,7 +191,7 @@ const getAllPermissions = async (
else {
const permissions: PermissionDetailsResponse[] = []
const dbUser = await User.findOne({ _id: user?.userId })
const dbUser = await User.findOne({ id: user?.userId })
if (!dbUser)
throw {
code: 404,
@@ -227,7 +227,7 @@ const createPermission = async ({
switch (principalType) {
case PrincipalType.user: {
const userInDB = await User.findOne({ _id: principalId })
const userInDB = await User.findOne({ id: principalId })
if (!userInDB)
throw {
code: 404,
@@ -259,7 +259,7 @@ const createPermission = async ({
permission.user = userInDB._id
user = {
uid: userInDB.uid,
id: userInDB.id,
username: userInDB.username,
displayName: userInDB.displayName,
isAdmin: userInDB.isAdmin
@@ -267,7 +267,7 @@ const createPermission = async ({
break
}
case PrincipalType.group: {
const groupInDB = await Group.findOne({ _id: principalId })
const groupInDB = await Group.findOne({ groupId: principalId })
if (!groupInDB)
throw {
code: 404,
@@ -291,13 +291,13 @@ const createPermission = async ({
permission.group = groupInDB._id
group = {
uid: groupInDB.uid,
groupId: groupInDB.groupId,
name: groupInDB.name,
description: groupInDB.description,
isActive: groupInDB.isActive,
users: groupInDB.populate({
path: 'users',
select: 'uid username displayName isAdmin -_id',
select: 'id username displayName isAdmin -_id',
options: { limit: 15 }
}) as unknown as UserResponse[]
}
@@ -314,7 +314,7 @@ const createPermission = async ({
const savedPermission = await permission.save()
return {
uid: savedPermission.uid,
permissionId: savedPermission.permissionId,
path: savedPermission.path,
type: savedPermission.type,
setting: savedPermission.setting,
@@ -324,21 +324,27 @@ const createPermission = async ({
}
const updatePermission = async (
uid: string,
id: number,
data: UpdatePermissionPayload
): Promise<PermissionDetailsResponse> => {
const { setting } = data
const updatedPermission = (await Permission.findOneAndUpdate(
{ _id: uid },
{ permissionId: id },
{ setting },
{ new: true }
)
.select('uid path type setting')
.populate({ path: 'user', select: 'uid username displayName isAdmin' })
.select({
_id: 0,
permissionId: 1,
path: 1,
type: 1,
setting: 1
})
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
.populate({
path: 'group',
select: 'groupId name description'
select: 'groupId name description -_id'
})) as unknown as PermissionDetailsResponse
if (!updatedPermission)
throw {
@@ -350,13 +356,13 @@ const updatePermission = async (
return updatedPermission
}
const deletePermission = async (uid: string) => {
const permission = await Permission.findOne({ _id: uid })
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({ _id: uid })
await Permission.deleteOne({ permissionId: id })
}

View File

@@ -2,11 +2,6 @@ import express from 'express'
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
import { UserResponse } from './user'
interface SessionResponse extends Omit<UserResponse, 'uid'> {
id: string
needsToUpdatePassword?: boolean
}
@Security('bearerAuth')
@Route('SASjsApi/session')
@Tags('Session')
@@ -15,17 +10,16 @@ export class SessionController {
* @summary Get session info (username).
*
*/
@Example<SessionResponse>({
id: 'userIdString',
@Example<UserResponse>({
id: 123,
username: 'johnusername',
displayName: 'John',
isAdmin: false,
needsToUpdatePassword: false
isAdmin: false
})
@Get('/')
public async session(
@Request() request: express.Request
): Promise<SessionResponse> {
): Promise<UserResponse> {
return session(request)
}
}
@@ -34,6 +28,5 @@ const session = (req: express.Request) => ({
id: req.user!.userId,
username: req.user!.username,
displayName: req.user!.displayName,
isAdmin: req.user!.isAdmin,
needsToUpdatePassword: req.user!.needsToUpdatePassword
isAdmin: req.user!.isAdmin
})

View File

@@ -3,11 +3,12 @@ import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
import { ExecutionController, ExecutionVars } from './internal'
import {
getPreProgramVariables,
HTTPHeaders,
LogLine,
makeFilesNamesMap,
getRunTimeAndFilePath
} from '../utils'
import { MulterFile } from '../types/Upload'
import { debug } from 'console'
interface ExecutePostRequestPayload {
/**
@@ -24,31 +25,20 @@ export class STPController {
/**
* Trigger a Stored Program using the _program URL parameter.
*
* Accepts additional URL parameters (converted to session variables)
* and file uploads. For more details, see docs:
* Accepts URL parameters and file uploads. For more details, see docs:
*
* https://server.sasjs.io/storedprograms
*
* @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of Stored Program in SASjs Drive.
* @param _debug Optional query param for setting debug mode (returns the session log in the response body).
* @param _program Location of code in SASjs Drive
* @example _program "/Projects/myApp/some/program"
* @example _debug 131
*/
@Get('/execute')
public async executeGetRequest(
@Request() request: express.Request,
@Query() _program: string,
@Query() _debug?: number
@Query() _program: string
): Promise<string | Buffer> {
let vars = request.query as ExecutionVars
if (_debug) {
vars = {
...vars,
_debug
}
}
const vars = request.query as ExecutionVars
return execute(request, _program, vars)
}
@@ -101,8 +91,6 @@ const execute = async (
}
)
req.res?.header(httpHeaders)
if (result instanceof Buffer) {
;(req as any).sasHeaders = httpHeaders
}

View File

@@ -17,23 +17,22 @@ import {
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User'
import {
getUserAutoExec,
updateUserAutoExec,
ModeType,
ALL_USERS_GROUP
} from '../utils'
import { GroupController, GroupResponse } from './group'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
import { GroupResponse } from './group'
export interface UserResponse {
uid: string
id: number
username: string
displayName: string
isAdmin: boolean
}
export interface UserDetailsResponse extends UserResponse {
export interface UserDetailsResponse {
id: number
displayName: string
username: string
isActive: boolean
isAdmin: boolean
autoExec?: string
groups?: GroupResponse[]
}
@@ -48,13 +47,13 @@ export class UserController {
*/
@Example<UserResponse[]>([
{
uid: 'userIdString',
id: 123,
username: 'johnusername',
displayName: 'John',
isAdmin: false
},
{
uid: 'anotherUserIdString',
id: 456,
username: 'starkusername',
displayName: 'Stark',
isAdmin: true
@@ -70,7 +69,7 @@ export class UserController {
*
*/
@Example<UserDetailsResponse>({
uid: 'userIdString',
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
@@ -107,20 +106,20 @@ export class UserController {
* Only Admin or user itself will get user autoExec code.
* @summary Get user properties - such as group memberships, userName, displayName.
* @param userId The user's identifier
* @example userId "userIdString"
* @example userId 1234
*/
@Get('{uid}')
@Get('{userId}')
public async getUser(
@Request() req: express.Request,
@Path() uid: string
@Path() userId: number
): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
const { user } = req
const getAutoExec = user!.isAdmin || user!.userId === uid
return getUser({ _id: uid }, getAutoExec)
const getAutoExec = user!.isAdmin || user!.userId == userId
return getUser({ id: userId }, getAutoExec)
}
/**
@@ -129,7 +128,7 @@ export class UserController {
* @example username "johnSnow01"
*/
@Example<UserDetailsResponse>({
uid: 'userIdString',
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
@@ -154,7 +153,7 @@ export class UserController {
* @example userId "1234"
*/
@Example<UserDetailsResponse>({
uid: 'userIdString',
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
@@ -162,7 +161,7 @@ export class UserController {
})
@Patch('{userId}')
public async updateUser(
@Path() userId: string,
@Path() userId: number,
@Body() body: UserPayload
): Promise<UserDetailsResponse> {
const { MODE } = process.env
@@ -170,7 +169,7 @@ export class UserController {
if (MODE === ModeType.Desktop)
return updateDesktopAutoExec(body.autoExec ?? '')
return updateUser({ _id: userId }, body)
return updateUser({ id: userId }, body)
}
/**
@@ -194,27 +193,25 @@ export class UserController {
*/
@Delete('{userId}')
public async deleteUser(
@Path() userId: string,
@Path() userId: number,
@Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false
) {
return deleteUser({ _id: userId }, isAdmin, body)
return deleteUser({ id: userId }, isAdmin, body)
}
}
const getAllUsers = async (): Promise<UserResponse[]> =>
await User.find({}).select('uid username displayName isAdmin').exec()
await User.find({})
.select({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 })
.exec()
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive, autoExec } = data
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist)
throw {
code: 409,
message: 'Username already exists.'
}
if (usernameExist) throw new Error('Username already exists.')
// Hash passwords
const hashPassword = User.hashPassword(password)
@@ -231,17 +228,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const savedUser = await user.save()
const groupController = new GroupController()
const allUsersGroup = await groupController
.getGroupByName(ALL_USERS_GROUP.name)
.catch(() => {})
if (allUsersGroup) {
await groupController.addUserToGroup(allUsersGroup.uid, savedUser.uid)
}
return {
uid: savedUser.uid,
id: savedUser.id,
displayName: savedUser.displayName,
username: savedUser.username,
isActive: savedUser.isActive,
@@ -250,8 +238,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
}
}
export interface GetUserBy {
_id?: string
interface GetUserBy {
id?: number
username?: string
}
@@ -261,25 +249,21 @@ const getUser = async (
): Promise<UserDetailsResponse> => {
const user = (await User.findOne(
findBy,
`uid displayName username isActive isAdmin autoExec`
`id displayName username isActive isAdmin autoExec -_id`
).populate(
'groups',
'uid name description'
'groupId name description -_id'
)) as unknown as UserDetailsResponse
if (!user)
throw {
code: 404,
message: 'User is not found.'
}
if (!user) throw new Error('User is not found.')
return {
uid: user.uid,
id: user.id,
displayName: user.displayName,
username: user.username,
isActive: user.isActive,
isAdmin: user.isAdmin,
autoExec: getAutoExec ? (user.autoExec ?? '') : undefined,
autoExec: getAutoExec ? user.autoExec ?? '' : undefined,
groups: user.groups
}
}
@@ -287,7 +271,7 @@ const getUser = async (
const getDesktopAutoExec = async () => {
return {
...desktopUser,
uid: desktopUser.userId,
id: desktopUser.userId,
autoExec: await getUserAutoExec()
}
}
@@ -300,36 +284,15 @@ const updateUser = async (
const params: any = { displayName, isAdmin, isActive, autoExec }
const user = await User.findOne(findBy)
if (username && username !== user?.username && user?.authProvider) {
throw {
code: 405,
message:
'Can not update username of user that is created by an external auth provider.'
}
}
if (displayName && displayName !== user?.displayName && user?.authProvider) {
throw {
code: 405,
message:
'Can not update display name of user that is created by an external auth provider.'
}
}
if (username) {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist) {
if (
(findBy._id && usernameExist.uid !== findBy._id) ||
(findBy.username && usernameExist.username !== findBy.username)
(findBy.id && usernameExist.id != findBy.id) ||
(findBy.username && usernameExist.username != findBy.username)
)
throw {
code: 409,
message: 'Username already exists.'
}
throw new Error('Username already exists.')
}
params.username = username
}
@@ -342,13 +305,10 @@ const updateUser = async (
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
if (!updatedUser)
throw {
code: 404,
message: `Unable to find user with ${findBy._id || findBy.username}`
}
throw new Error(`Unable to find user with ${findBy.id || findBy.username}`)
return {
uid: updatedUser.uid,
id: updatedUser.id,
username: updatedUser.username,
displayName: updatedUser.displayName,
isAdmin: updatedUser.isAdmin,
@@ -361,7 +321,7 @@ const updateDesktopAutoExec = async (autoExec: string) => {
await updateUserAutoExec(autoExec)
return {
...desktopUser,
uid: desktopUser.userId,
id: desktopUser.userId,
autoExec
}
}
@@ -372,19 +332,11 @@ const deleteUser = async (
{ password }: { password?: string }
) => {
const user = await User.findOne(findBy)
if (!user)
throw {
code: 404,
message: 'User is not found.'
}
if (!user) throw new Error('User is not found.')
if (!isAdmin) {
const validPass = user.comparePassword(password!)
if (!validPass)
throw {
code: 401,
message: 'Invalid password.'
}
if (!validPass) throw new Error('Invalid password.')
}
await User.deleteOne(findBy)

View File

@@ -1,17 +1,11 @@
import path from 'path'
import express from 'express'
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
import { readFile, convertSecondsToHms } from '@sasjs/utils'
import { readFile } from '@sasjs/utils'
import User from '../model/User'
import Client from '../model/Client'
import {
getWebBuildFolder,
generateAuthCode,
RateLimiter,
AuthProviderType,
LDAPClient
} from '../utils'
import { getWebBuildFolder, generateAuthCode } from '../utils'
import { InfoJWT } from '../types'
import { AuthController } from './auth'
@@ -84,37 +78,10 @@ const login = async (
) => {
// Authenticate User
const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.')
let validPass = false
if (user) {
if (
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
user.authProvider === AuthProviderType.LDAP
) {
const ldapClient = await LDAPClient.init()
validPass = await ldapClient
.verifyUser(username, password)
.catch(() => false)
} else {
validPass = user.comparePassword(password)
}
}
// code to prevent brute force attack
const rateLimiter = RateLimiter.getInstance()
if (!validPass) {
const retrySecs = await rateLimiter.consume(req.ip, user?.username)
if (retrySecs > 0) throw errors.tooManyRequests(retrySecs)
}
if (!user) throw errors.userNotFound
if (!validPass) throw errors.invalidPassword
// Reset on successful authorization
rateLimiter.resetOnSuccess(req.ip, user.username)
const validPass = user.comparePassword(password)
if (!validPass) throw new Error('Invalid password.')
req.session.loggedIn = true
req.session.user = {
@@ -124,8 +91,7 @@ const login = async (
displayName: user.displayName,
isAdmin: user.isAdmin,
isActive: user.isActive,
autoExec: user.autoExec,
needsToUpdatePassword: user.needsToUpdatePassword
autoExec: user.autoExec
}
return {
@@ -134,8 +100,7 @@ const login = async (
id: user.id,
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin,
needsToUpdatePassword: user.needsToUpdatePassword
isAdmin: user.isAdmin
}
}
}
@@ -192,18 +157,3 @@ interface AuthorizeResponse {
*/
code: string
}
const errors = {
invalidPassword: {
code: 401,
message: 'Invalid Password.'
},
userNotFound: {
code: 401,
message: 'Username is not found.'
},
tooManyRequests: (seconds: number) => ({
code: 429,
message: `Too Many Requests! Retry after ${convertSecondsToHms(seconds)}`
})
}

View File

@@ -76,13 +76,12 @@ const authenticateToken = async (
const { MODE } = process.env
if (MODE === ModeType.Desktop) {
req.user = {
userId: '1234',
userId: 1234,
clientId: 'desktopModeClientId',
username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName',
isAdmin: true,
isActive: true,
needsToUpdatePassword: false
isActive: true
}
req.accessToken = 'desktopModeAccessToken'
return next()

View File

@@ -5,7 +5,7 @@ import {
PermissionSettingForRoute,
PermissionType
} from '../controllers/permission'
import { getPath, isPublicRoute, TopLevelRoutes } from '../utils'
import { getPath, isPublicRoute } from '../utils'
export const authorize: RequestHandler = async (req, res, next) => {
const { user } = req
@@ -18,13 +18,10 @@ export const authorize: RequestHandler = async (req, res, next) => {
// no need to check for permissions when route is Public
if (await isPublicRoute(req)) return next()
const dbUser = await User.findOne({ _id: user.userId })
const dbUser = await User.findOne({ id: user.userId })
if (!dbUser) return res.sendStatus(401)
const path = getPath(req)
const { baseUrl } = req
const topLevelRoute =
TopLevelRoutes.find((route) => baseUrl.startsWith(route)) || baseUrl
// find permission w.r.t user
const permission = await Permission.findOne({
@@ -38,21 +35,6 @@ export const authorize: RequestHandler = async (req, res, next) => {
else return res.sendStatus(401)
}
// find permission w.r.t user on top level
const topLevelPermission = await Permission.findOne({
path: topLevelRoute,
type: PermissionType.route,
user: dbUser._id
})
if (topLevelPermission) {
if (topLevelPermission.setting === PermissionSettingForRoute.grant)
return next()
else return res.sendStatus(401)
}
let isPermissionDenied = false
// find permission w.r.t user's groups
for (const group of dbUser.groups) {
const groupPermission = await Permission.findOne({
@@ -60,28 +42,8 @@ export const authorize: RequestHandler = async (req, res, next) => {
type: PermissionType.route,
group
})
if (groupPermission) {
if (groupPermission.setting === PermissionSettingForRoute.grant) {
return next()
} else {
isPermissionDenied = true
}
}
if (groupPermission?.setting === PermissionSettingForRoute.grant)
return next()
}
if (!isPermissionDenied) {
// find permission w.r.t user's groups on top level
for (const group of dbUser.groups) {
const groupPermission = await Permission.findOne({
path: topLevelRoute,
type: PermissionType.route,
group
})
if (groupPermission?.setting === PermissionSettingForRoute.grant)
return next()
}
}
return res.sendStatus(401)
}

View File

@@ -1,22 +0,0 @@
import { RequestHandler } from 'express'
import { convertSecondsToHms } from '@sasjs/utils'
import { RateLimiter } from '../utils'
export const bruteForceProtection: RequestHandler = async (req, res, next) => {
const ip = req.ip
const username = req.body.username
const rateLimiter = RateLimiter.getInstance()
const retrySecs = await rateLimiter.check(ip, username)
if (retrySecs > 0) {
res
.status(429)
.send(`Too Many Requests! Retry after ${convertSecondsToHms(retrySecs)}`)
return
}
next()
}

View File

@@ -28,11 +28,10 @@ export const desktopRestrict: RequestHandler = (req, res, next) => {
}
export const desktopUser: RequestUser = {
userId: '12345',
userId: 12345,
clientId: 'desktop_app',
username: userInfo().username,
displayName: userInfo().username,
isAdmin: true,
isActive: true,
needsToUpdatePassword: false
isActive: true
}

View File

@@ -4,4 +4,3 @@ export * from './csrfProtection'
export * from './desktop'
export * from './verifyAdmin'
export * from './verifyAdminIfNeeded'
export * from './bruteForceProtection'

View File

@@ -8,8 +8,8 @@ export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
if (!user?.isAdmin) {
let adminAccountRequired: boolean = true
if (req.params.uid) {
adminAccountRequired = user?.userId !== req.params.uid
if (req.params.userId) {
adminAccountRequired = user?.userId !== parseInt(req.params.userId)
} else if (req.params.username) {
adminAccountRequired = user?.username !== req.params.username
}

View File

@@ -1,6 +1,5 @@
import mongoose, { Schema } from 'mongoose'
export const NUMBER_OF_SECONDS_IN_A_DAY = 86400
export interface ClientPayload {
/**
* Client ID
@@ -12,16 +11,6 @@ export interface ClientPayload {
* @example "someRandomCryptoString"
*/
clientSecret: string
/**
* Number of seconds after which access token will expire. Default is 86400 (1 day)
* @example 86400
*/
accessTokenExpiration?: number
/**
* Number of seconds after which access token will expire. Default is 2592000 (30 days)
* @example 2592000
*/
refreshTokenExpiration?: number
}
const ClientSchema = new Schema<ClientPayload>({
@@ -32,14 +21,6 @@ const ClientSchema = new Schema<ClientPayload>({
clientSecret: {
type: String,
required: true
},
accessTokenExpiration: {
type: Number,
default: NUMBER_OF_SECONDS_IN_A_DAY
},
refreshTokenExpiration: {
type: Number,
default: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
})

View File

@@ -1,9 +1,9 @@
import { Schema, model, Document, Model } from 'mongoose'
import mongoose, { Schema, model, Document, Model } from 'mongoose'
import { GroupDetailsResponse } from '../controllers'
import User, { IUser } from './User'
import { AuthProviderType } from '../utils'
const AutoIncrement = require('mongoose-sequence')(mongoose)
export const PUBLIC_GROUP_NAME = 'public'
export const PUBLIC_GROUP_NAME = 'Public'
export interface GroupPayload {
/**
@@ -24,12 +24,9 @@ export interface GroupPayload {
}
interface IGroupDocument extends GroupPayload, Document {
groupId: number
isActive: boolean
users: Schema.Types.ObjectId[]
authProvider?: AuthProviderType
// Declare virtual properties as read-only properties
readonly uid: string
}
interface IGroup extends IGroupDocument {
@@ -39,46 +36,28 @@ interface IGroup extends IGroupDocument {
}
interface IGroupModel extends Model<IGroup> {}
const opts = {
toJSON: {
virtuals: true,
transform: function (doc: any, ret: any, options: any) {
delete ret._id
delete ret.id
return ret
}
}
}
const groupSchema = new Schema<IGroupDocument>(
{
name: {
type: String,
required: true,
unique: true
},
description: {
type: String,
default: 'Group description.'
},
authProvider: {
type: String,
enum: AuthProviderType
},
isActive: {
type: Boolean,
default: true
},
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
const groupSchema = new Schema<IGroupDocument>({
name: {
type: String,
required: true,
unique: true
},
opts
)
groupSchema.virtual('uid').get(function () {
return this._id.toString()
description: {
type: String,
default: 'Group description.'
},
isActive: {
type: Boolean,
default: true
},
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
})
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
// Hooks
groupSchema.post('save', function (group: IGroup, next: Function) {
group.populate('users', 'uid username displayName').then(function () {
group.populate('users', 'id username displayName -_id').then(function () {
next()
})
})

View File

@@ -1,4 +1,5 @@
import { Schema, model, Document, Model } from 'mongoose'
import mongoose, { Schema, model, Document, Model } from 'mongoose'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import { PermissionDetailsResponse } from '../controllers'
interface GetPermissionBy {
@@ -10,11 +11,9 @@ interface IPermissionDocument extends Document {
path: string
type: string
setting: string
permissionId: number
user: Schema.Types.ObjectId
group: Schema.Types.ObjectId
// Declare virtual properties as read-only properties
readonly uid: string
}
interface IPermission extends IPermissionDocument {}
@@ -23,54 +22,44 @@ interface IPermissionModel extends Model<IPermission> {
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
}
const opts = {
toJSON: {
virtuals: true,
transform: function (doc: any, ret: any, options: any) {
delete ret._id
delete ret.id
return ret
}
}
}
const permissionSchema = new Schema<IPermissionDocument>(
{
path: {
type: String,
required: true
},
type: {
type: String,
required: true
},
setting: {
type: String,
required: true
},
user: { type: Schema.Types.ObjectId, ref: 'User' },
group: { type: Schema.Types.ObjectId, ref: 'Group' }
const permissionSchema = new Schema<IPermissionDocument>({
path: {
type: String,
required: true
},
opts
)
permissionSchema.virtual('uid').get(function () {
return this._id.toString()
type: {
type: String,
required: true
},
setting: {
type: String,
required: true
},
user: { type: Schema.Types.ObjectId, ref: 'User' },
group: { type: Schema.Types.ObjectId, ref: 'Group' }
})
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
// Static Methods
permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
PermissionDetailsResponse[]
> {
return (await this.find(getBy)
.select('uid path type setting')
.populate({ path: 'user', select: 'uid username displayName isAdmin' })
.select({
_id: 0,
permissionId: 1,
path: 1,
type: 1,
setting: 1
})
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
.populate({
path: 'group',
select: 'uid name description',
select: 'groupId name description -_id',
populate: {
path: 'users',
select: 'uid username displayName isAdmin',
select: 'id username displayName isAdmin -_id',
options: { limit: 15 }
}
})) as unknown as PermissionDetailsResponse[]

View File

@@ -1,6 +1,6 @@
import { Schema, model, Document, Model, ObjectId } from 'mongoose'
import mongoose, { Schema, model, Document, Model } from 'mongoose'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import bcrypt from 'bcryptjs'
import { AuthProviderType } from '../utils'
export interface UserPayload {
/**
@@ -36,16 +36,12 @@ export interface UserPayload {
interface IUserDocument extends UserPayload, Document {
_id: Schema.Types.ObjectId
id: number
isAdmin: boolean
isActive: boolean
needsToUpdatePassword: boolean
autoExec: string
groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }]
authProvider?: AuthProviderType
// Declare virtual properties as read-only properties
readonly uid: string
}
export interface IUser extends IUserDocument {
@@ -56,75 +52,51 @@ export interface IUser extends IUserDocument {
interface IUserModel extends Model<IUser> {
hashPassword(password: string): string
}
const opts = {
toJSON: {
virtuals: true,
transform: function (doc: any, ret: any, options: any) {
delete ret._id
delete ret.id
return ret
}
}
}
const userSchema = new Schema<IUserDocument>(
{
displayName: {
type: String,
required: true
},
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
authProvider: {
type: String,
enum: AuthProviderType
},
isAdmin: {
type: Boolean,
default: false
},
isActive: {
type: Boolean,
default: true
},
needsToUpdatePassword: {
type: Boolean,
default: true
},
autoExec: {
type: String
},
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
tokens: [
{
clientId: {
type: String,
required: true
},
accessToken: {
type: String,
required: true
},
refreshToken: {
type: String,
required: true
}
}
]
const userSchema = new Schema<IUserDocument>({
displayName: {
type: String,
required: true
},
opts
)
userSchema.virtual('uid').get(function () {
return this._id.toString()
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
isAdmin: {
type: Boolean,
default: false
},
isActive: {
type: Boolean,
default: true
},
autoExec: {
type: String
},
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
tokens: [
{
clientId: {
type: String,
required: true
},
accessToken: {
type: String,
required: true
},
refreshToken: {
type: String,
required: true
}
}
]
})
userSchema.plugin(AutoIncrement, { inc_field: 'id' })
// Static Methods
userSchema.static('hashPassword', (password: string): string => {

View File

@@ -7,28 +7,12 @@ import {
authenticateRefreshToken
} from '../../middlewares'
import { tokenValidation, updatePasswordValidation } from '../../utils'
import { tokenValidation } from '../../utils'
import { InfoJWT } from '../../types'
const authRouter = express.Router()
const controller = new AuthController()
authRouter.patch(
'/updatePassword',
authenticateAccessToken,
async (req, res) => {
const { error, value: body } = updatePasswordValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
await controller.updatePassword(req, body)
res.sendStatus(204)
} catch (err: any) {
res.status(err.code).send(err.message)
}
}
)
authRouter.post('/token', async (req, res) => {
const { error, value: body } = tokenValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)

View File

@@ -1,25 +0,0 @@
import express from 'express'
import { AuthConfigController } from '../../controllers'
const authConfigRouter = express.Router()
authConfigRouter.get('/', async (req, res) => {
const controller = new AuthConfigController()
try {
const response = controller.getDetail()
res.send(response)
} catch (err: any) {
res.status(500).send(err.toString())
}
})
authConfigRouter.post('/synchroniseWithLDAP', async (req, res) => {
const controller = new AuthConfigController()
try {
const response = await controller.synchroniseWithLDAP()
res.send(response)
} catch (err: any) {
res.status(500).send(err.toString())
}
})
export default authConfigRouter

View File

@@ -1,7 +1,6 @@
import express from 'express'
import { ClientController } from '../../controllers'
import { registerClientValidation } from '../../utils'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
const clientRouter = express.Router()
@@ -18,19 +17,4 @@ clientRouter.post('/', async (req, res) => {
}
})
clientRouter.get(
'/',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const controller = new ClientController()
try {
const response = await controller.getAllClients()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
export default clientRouter

View File

@@ -1,5 +1,5 @@
import express from 'express'
import { runCodeValidation, triggerCodeValidation } from '../../utils'
import { runCodeValidation } from '../../utils'
import { CodeController } from '../../controllers/'
const runRouter = express.Router()
@@ -28,22 +28,4 @@ runRouter.post('/execute', async (req, res) => {
}
})
runRouter.post('/trigger', async (req, res) => {
const { error, value: body } = triggerCodeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.triggerCode(req, body)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default runRouter

View File

@@ -1,11 +1,7 @@
import express from 'express'
import { GroupController } from '../../controllers/'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
import {
getGroupValidation,
registerGroupValidation,
uidValidation
} from '../../utils'
import { getGroupValidation, registerGroupValidation } from '../../utils'
const groupRouter = express.Router()
@@ -22,7 +18,11 @@ groupRouter.post(
const response = await controller.createGroup(body)
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
@@ -33,22 +33,27 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllGroups()
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
groupRouter.get('/:uid', authenticateAccessToken, async (req, res) => {
const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const { groupId } = req.params
const controller = new GroupController()
try {
const response = await controller.getGroup(uid)
const response = await controller.getGroup(parseInt(groupId))
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
@@ -63,64 +68,83 @@ groupRouter.get(
const controller = new GroupController()
try {
const response = await controller.getGroupByName(name)
const response = await controller.getGroupByGroupName(name)
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
groupRouter.post(
'/:groupUid/:userUid',
'/:groupId/:userId',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const { groupUid, userUid } = req.params
const { groupId, userId } = req.params
const controller = new GroupController()
try {
const response = await controller.addUserToGroup(groupUid, userUid)
const response = await controller.addUserToGroup(
parseInt(groupId),
parseInt(userId)
)
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
groupRouter.delete(
'/:groupUid/:userUid',
'/:groupId/:userId',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const { groupUid, userUid } = req.params
const { groupId, userId } = req.params
const controller = new GroupController()
try {
const response = await controller.removeUserFromGroup(groupUid, userUid)
const response = await controller.removeUserFromGroup(
parseInt(groupId),
parseInt(userId)
)
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
groupRouter.delete(
'/:uid',
'/:groupId',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
const { groupId } = req.params
const controller = new GroupController()
try {
await controller.deleteGroup(uid)
await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!')
} catch (err: any) {
res.status(err.code).send(err.message)
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)

View File

@@ -18,7 +18,6 @@ import clientRouter from './client'
import authRouter from './auth'
import sessionRouter from './session'
import permissionRouter from './permission'
import authConfigRouter from './authConfig'
const router = express.Router()
@@ -44,14 +43,6 @@ router.use(
permissionRouter
)
router.use(
'/authConfig',
desktopRestrict,
authenticateAccessToken,
verifyAdmin,
authConfigRouter
)
router.use(
'/',
swaggerUi.serve,

View File

@@ -3,7 +3,6 @@ import { PermissionController } from '../../controllers/'
import { verifyAdmin } from '../../middlewares'
import {
registerPermissionValidation,
uidValidation,
updatePermissionValidation
} from '../../utils'
@@ -35,17 +34,14 @@ permissionRouter.post('/', verifyAdmin, async (req, res) => {
}
})
permissionRouter.patch('/:uid', verifyAdmin, async (req: any, res) => {
const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
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(uid, body)
const response = await controller.updatePermission(permissionId, body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
@@ -54,18 +50,20 @@ permissionRouter.patch('/:uid', verifyAdmin, async (req: any, res) => {
}
})
permissionRouter.delete('/:uid', verifyAdmin, async (req: any, res) => {
const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
permissionRouter.delete(
'/:permissionId',
verifyAdmin,
async (req: any, res) => {
const { permissionId } = req.params
const { uid } = params
try {
await controller.deletePermission(uid)
res.status(200).send('Permission Deleted!')
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
try {
await controller.deletePermission(permissionId)
res.status(200).send('Permission Deleted!')
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
})
)
export default permissionRouter

View File

@@ -13,7 +13,6 @@ import {
generateAccessToken,
generateAuthCode,
generateRefreshToken,
randomBytesHexString,
saveTokensInDB,
verifyTokenInDB
} from '../../../utils'
@@ -21,6 +20,7 @@ import {
const clientId = 'someclientID'
const clientSecret = 'someclientSecret'
const user = {
id: 1234,
displayName: 'Test User',
username: 'testUsername',
password: '87654321',
@@ -52,7 +52,7 @@ describe('auth', () => {
describe('token', () => {
const userInfo: InfoJWT = {
clientId,
userId: randomBytesHexString(12)
userId: user.id
}
beforeAll(async () => {
await userController.createUser(user)
@@ -151,10 +151,10 @@ describe('auth', () => {
currentUser = await userController.createUser(user)
refreshToken = generateRefreshToken({
clientId,
userId: currentUser.uid
userId: currentUser.id
})
await saveTokensInDB(
currentUser.uid,
currentUser.id,
clientId,
'accessToken',
refreshToken
@@ -202,11 +202,11 @@ describe('auth', () => {
currentUser = await userController.createUser(user)
accessToken = generateAccessToken({
clientId,
userId: currentUser.uid
userId: currentUser.id
})
await saveTokensInDB(
currentUser.uid,
currentUser.id,
clientId,
accessToken,
'refreshToken'

View File

@@ -5,7 +5,6 @@ import request from 'supertest'
import appPromise from '../../../app'
import { UserController, ClientController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../../../model/Client'
const client = {
clientId: 'someclientID',
@@ -27,7 +26,6 @@ describe('client', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let adminAccessToken: string
const userController = new UserController()
const clientController = new ClientController()
@@ -36,18 +34,6 @@ describe('client', () => {
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
const dbUser = await userController.createUser(adminUser)
adminAccessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.uid
})
await saveTokensInDB(
dbUser.uid,
client.clientId,
adminAccessToken,
'refreshToken'
)
})
afterAll(async () => {
@@ -57,6 +43,22 @@ describe('client', () => {
})
describe('create', () => {
let adminAccessToken: string
beforeAll(async () => {
const dbUser = await userController.createUser(adminUser)
adminAccessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.id
})
await saveTokensInDB(
dbUser.id,
client.clientId,
adminAccessToken,
'refreshToken'
)
})
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['clients']
@@ -95,10 +97,10 @@ describe('client', () => {
const dbUser = await userController.createUser(user)
const accessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.uid
userId: dbUser.id
})
await saveTokensInDB(
dbUser.uid,
dbUser.id,
client.clientId,
accessToken,
'refreshToken'
@@ -155,80 +157,4 @@ describe('client', () => {
expect(res.body).toEqual({})
})
})
describe('get', () => {
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['clients']
await collection.deleteMany({})
})
it('should respond with an array of all clients', async () => {
await clientController.createClient(newClient)
await clientController.createClient({
clientId: 'clientID',
clientSecret: 'clientSecret'
})
const res = await request(app)
.get('/SASjsApi/client')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const expected = [
{
clientId: 'newClientID',
clientSecret: 'newClientSecret',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
},
{
clientId: 'clientID',
clientSecret: 'clientSecret',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
]
expect(res.body).toEqual(expected)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get('/SASjsApi/client').send().expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbideen if access token is not of an admin account', async () => {
const user = {
displayName: 'User 2',
username: 'username2',
password: '12345678',
isAdmin: false,
isActive: true
}
const dbUser = await userController.createUser(user)
const accessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.uid
})
await saveTokensInDB(
dbUser.uid,
client.clientId,
accessToken,
'refreshToken'
)
const res = await request(app)
.get('/SASjsApi/client')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
})
})

View File

@@ -71,31 +71,31 @@ describe('drive', () => {
con = await mongoose.connect(mongoServer.getUri())
const dbUser = await controller.createUser(user)
accessToken = await generateAndSaveToken(dbUser.uid)
accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/deploy',
principalId: dbUser.uid
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/deploy/upload',
principalId: dbUser.uid
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/file',
principalId: dbUser.uid
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/folder',
principalId: dbUser.uid
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/rename',
principalId: dbUser.uid
principalId: dbUser.id
})
})
@@ -1197,7 +1197,7 @@ const getExampleService = (): ServiceMember =>
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
.members[0] as ServiceMember
const generateAndSaveToken = async (userId: string) => {
const generateAndSaveToken = async (userId: number) => {
const adminAccessToken = generateAccessToken({
clientId,
userId

View File

@@ -4,14 +4,8 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController, GroupController } from '../../../controllers/'
import {
generateAccessToken,
saveTokensInDB,
AuthProviderType
} from '../../../utils'
import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group'
import User from '../../../model/User'
import { randomBytes } from 'crypto'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { PUBLIC_GROUP_NAME } from '../../../model/Group'
const clientId = 'someclientID'
const adminUser = {
@@ -76,7 +70,7 @@ describe('group', () => {
.send(group)
.expect(200)
expect(res.body.uid).toBeTruthy()
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
@@ -156,7 +150,7 @@ describe('group', () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}`)
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
@@ -175,17 +169,17 @@ describe('group', () => {
username: 'deletegroup2'
})
await groupController.addUserToGroup(dbGroup.uid, dbUser1.uid)
await groupController.addUserToGroup(dbGroup.uid, dbUser2.uid)
await groupController.addUserToGroup(dbGroup.groupId, dbUser1.id)
await groupController.addUserToGroup(dbGroup.groupId, dbUser2.id)
await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}`)
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res1 = await request(app)
.get(`/SASjsApi/user/${dbUser1.uid}`)
.get(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
@@ -193,7 +187,7 @@ describe('group', () => {
expect(res1.body.groups).toEqual([])
const res2 = await request(app)
.get(`/SASjsApi/user/${dbUser2.uid}`)
.get(`/SASjsApi/user/${dbUser2.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
@@ -202,10 +196,8 @@ describe('group', () => {
})
it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app)
.delete(`/SASjsApi/group/${hexValue}`)
.delete(`/SASjsApi/group/1234`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
@@ -232,7 +224,7 @@ describe('group', () => {
})
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}`)
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
@@ -248,15 +240,15 @@ describe('group', () => {
})
it('should respond with group', async () => {
const { uid } = await groupController.createGroup(group)
const { groupId } = await groupController.createGroup(group)
const res = await request(app)
.get(`/SASjsApi/group/${uid}`)
.get(`/SASjsApi/group/${groupId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.uid).toBeTruthy()
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
@@ -269,15 +261,15 @@ describe('group', () => {
username: 'get' + user.username
})
const { uid } = await groupController.createGroup(group)
const { groupId } = await groupController.createGroup(group)
const res = await request(app)
.get(`/SASjsApi/group/${uid}`)
.get(`/SASjsApi/group/${groupId}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.uid).toBeTruthy()
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
@@ -295,10 +287,8 @@ describe('group', () => {
})
it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app)
.get(`/SASjsApi/group/${hexValue}`)
.get('/SASjsApi/group/1234')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
@@ -317,7 +307,7 @@ describe('group', () => {
.send()
.expect(200)
expect(res.body.uid).toBeTruthy()
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
@@ -338,7 +328,7 @@ describe('group', () => {
.send()
.expect(200)
expect(res.body.uid).toBeTruthy()
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
@@ -384,7 +374,7 @@ describe('group', () => {
expect(res.body).toEqual([
{
uid: expect.anything(),
groupId: expect.anything(),
name: group.name,
description: group.description
}
@@ -406,7 +396,7 @@ describe('group', () => {
expect(res.body).toEqual([
{
uid: expect.anything(),
groupId: expect.anything(),
name: group.name,
description: group.description
}
@@ -431,18 +421,18 @@ describe('group', () => {
const dbUser = await userController.createUser(user)
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.uid).toBeTruthy()
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([
{
uid: expect.anything(),
id: expect.anything(),
username: user.username,
displayName: user.displayName
}
@@ -457,20 +447,20 @@ describe('group', () => {
})
await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res = await request(app)
.get(`/SASjsApi/user/${dbUser.uid}`)
.get(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groups).toEqual([
{
uid: expect.anything(),
groupId: expect.anything(),
name: group.name,
description: group.description
}
@@ -483,21 +473,21 @@ describe('group', () => {
...user,
username: 'addUserRandomUser'
})
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.uid).toBeTruthy()
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([
{
uid: expect.anything(),
id: expect.anything(),
username: 'addUserRandomUser',
displayName: user.displayName
}
@@ -531,10 +521,8 @@ describe('group', () => {
})
it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app)
.post(`/SASjsApi/group/${hexValue}/123`)
.post('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
@@ -545,10 +533,8 @@ describe('group', () => {
it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group)
const hexValue = randomBytes(12).toString('hex')
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${hexValue}`)
.post(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
@@ -565,7 +551,7 @@ describe('group', () => {
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(400)
@@ -574,46 +560,6 @@ describe('group', () => {
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
)
})
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
const dbGroup = await Group.create({
...group,
authProvider: AuthProviderType.LDAP
})
const dbUser = await userController.createUser({
...user,
username: 'ldapGroupUser'
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await User.create({
...user,
username: 'ldapUser',
authProvider: AuthProviderType.LDAP
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
})
describe('RemoveUser', () => {
@@ -627,15 +573,15 @@ describe('group', () => {
...user,
username: 'removeUserRandomUser'
})
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.uid).toBeTruthy()
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
@@ -648,16 +594,16 @@ describe('group', () => {
...user,
username: 'removeGroupFromUser'
})
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res = await request(app)
.get(`/SASjsApi/user/${dbUser.uid}`)
.get(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
@@ -665,46 +611,6 @@ describe('group', () => {
expect(res.body.groups).toEqual([])
})
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
const dbGroup = await Group.create({
...group,
authProvider: AuthProviderType.LDAP
})
const dbUser = await userController.createUser({
...user,
username: 'removeLdapGroupUser'
})
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await User.create({
...user,
username: 'removeLdapUser',
authProvider: AuthProviderType.LDAP
})
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(405)
expect(res.text).toEqual(
`Can't add/remove user to group created by external auth provider.`
)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')
@@ -732,10 +638,8 @@ describe('group', () => {
})
it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app)
.delete(`/SASjsApi/group/${hexValue}/123`)
.delete('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
@@ -746,10 +650,8 @@ describe('group', () => {
it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group)
const hexValue = randomBytes(12).toString('hex')
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}/${hexValue}`)
.delete(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
@@ -765,10 +667,10 @@ const generateSaveTokenAndCreateUser = async (
): Promise<string> => {
const dbUser = await userController.createUser(someUser ?? adminUser)
return generateAndSaveToken(dbUser.uid)
return generateAndSaveToken(dbUser.id)
}
const generateAndSaveToken = async (userId: string) => {
const generateAndSaveToken = async (userId: number) => {
const adminAccessToken = generateAccessToken({
clientId,
userId

View File

@@ -17,7 +17,6 @@ import {
PermissionDetailsResponse
} from '../../../controllers'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { randomBytes } from 'crypto'
const deployPayload = {
appLoc: 'string',
@@ -104,10 +103,10 @@ describe('permission', () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...permission, principalId: dbUser.uid })
.send({ ...permission, principalId: dbUser.id })
.expect(200)
expect(res.body.uid).toBeTruthy()
expect(res.body.permissionId).toBeTruthy()
expect(res.body.path).toEqual(permission.path)
expect(res.body.type).toEqual(permission.type)
expect(res.body.setting).toEqual(permission.setting)
@@ -123,11 +122,11 @@ describe('permission', () => {
.send({
...permission,
principalType: 'group',
principalId: dbGroup.uid
principalId: dbGroup.groupId
})
.expect(200)
expect(res.body.uid).toBeTruthy()
expect(res.body.permissionId).toBeTruthy()
expect(res.body.path).toEqual(permission.path)
expect(res.body.type).toEqual(permission.type)
expect(res.body.setting).toEqual(permission.setting)
@@ -145,7 +144,7 @@ describe('permission', () => {
})
it('should respond with Unauthorized if access token is not of an admin account', async () => {
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.post('/SASjsApi/permission')
@@ -282,19 +281,17 @@ describe('permission', () => {
expect(res.body).toEqual({})
})
it('should respond with Bad Request if principalId is not a string of 24 hex characters', async () => {
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: randomBytes(10).toString('hex')
principalId: 'someCharacters'
})
.expect(400)
expect(res.text).toEqual(
'"principalId" length must be 24 characters long'
)
expect(res.text).toEqual('"principalId" must be a number')
expect(res.body).toEqual({})
})
@@ -310,7 +307,7 @@ describe('permission', () => {
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalId: adminUser.uid
principalId: adminUser.id
})
.expect(400)
@@ -324,7 +321,7 @@ describe('permission', () => {
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalId: randomBytes(12).toString('hex')
principalId: 123
})
.expect(404)
@@ -339,7 +336,7 @@ describe('permission', () => {
.send({
...permission,
principalType: 'group',
principalId: randomBytes(12).toString('hex')
principalId: 123
})
.expect(404)
@@ -350,13 +347,13 @@ describe('permission', () => {
it('should respond with Conflict (409) if permission already exists', async () => {
await permissionController.createPermission({
...permission,
principalId: dbUser.uid
principalId: dbUser.id
})
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...permission, principalId: dbUser.uid })
.send({ ...permission, principalId: dbUser.id })
.expect(409)
expect(res.text).toEqual(
@@ -371,7 +368,7 @@ describe('permission', () => {
beforeAll(async () => {
dbPermission = await permissionController.createPermission({
...permission,
principalId: dbUser.uid
principalId: dbUser.id
})
})
@@ -381,7 +378,7 @@ describe('permission', () => {
it('should respond with updated permission', async () => {
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.uid}`)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ setting: PermissionSettingForRoute.deny })
.expect(200)
@@ -391,7 +388,7 @@ describe('permission', () => {
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.uid}`)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.send()
.expect(401)
@@ -406,7 +403,7 @@ describe('permission', () => {
})
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.uid}`)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
@@ -417,7 +414,7 @@ describe('permission', () => {
it('should respond with Bad Request if setting is missing', async () => {
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.uid}`)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(400)
@@ -428,7 +425,7 @@ describe('permission', () => {
it('should respond with Bad Request if setting is invalid', async () => {
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.uid}`)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({
setting: 'invalid'
@@ -440,9 +437,8 @@ describe('permission', () => {
})
it('should respond with not found (404) if permission with provided id does not exist', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app)
.patch(`/SASjsApi/permission/${hexValue}`)
.patch('/SASjsApi/permission/123')
.auth(adminAccessToken, { type: 'bearer' })
.send({
setting: PermissionSettingForRoute.deny
@@ -458,10 +454,10 @@ describe('permission', () => {
it('should delete permission', async () => {
const dbPermission = await permissionController.createPermission({
...permission,
principalId: dbUser.uid
principalId: dbUser.id
})
const res = await request(app)
.delete(`/SASjsApi/permission/${dbPermission?.uid}`)
.delete(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
@@ -470,10 +466,8 @@ describe('permission', () => {
})
it('should respond with not found (404) if permission with provided id does not exists', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app)
.delete(`/SASjsApi/permission/${hexValue}`)
.delete('/SASjsApi/permission/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
@@ -487,12 +481,12 @@ describe('permission', () => {
await permissionController.createPermission({
...permission,
path: '/test-1',
principalId: dbUser.uid
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/test-2',
principalId: dbUser.uid
principalId: dbUser.id
})
})
@@ -511,12 +505,12 @@ describe('permission', () => {
...user,
username: 'get' + user.username
})
const accessToken = await generateAndSaveToken(nonAdminUser.uid)
const accessToken = await generateAndSaveToken(nonAdminUser.id)
await permissionController.createPermission({
path: '/test-1',
type: PermissionType.route,
principalType: PrincipalType.user,
principalId: nonAdminUser.uid,
principalId: nonAdminUser.id,
setting: PermissionSettingForRoute.grant
})
@@ -537,7 +531,7 @@ describe('permission', () => {
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/deploy',
principalId: dbUser.uid
principalId: dbUser.id
})
})
@@ -557,7 +551,7 @@ describe('permission', () => {
})
it('should create files in SASJS drive', async () => {
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
await request(app)
.get('/SASjsApi/drive/deploy')
@@ -567,7 +561,7 @@ describe('permission', () => {
})
it('should respond unauthorized', async () => {
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
await request(app)
.get('/SASjsApi/drive/deploy/upload')
@@ -583,10 +577,10 @@ const generateSaveTokenAndCreateUser = async (
): Promise<string> => {
const dbUser = await userController.createUser(someUser ?? adminUser)
return generateAndSaveToken(dbUser.uid)
return generateAndSaveToken(dbUser.id)
}
const generateAndSaveToken = async (userId: string) => {
const generateAndSaveToken = async (userId: number) => {
const adminAccessToken = generateAccessToken({
clientId,
userId

View File

@@ -58,12 +58,12 @@ describe('stp', () => {
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
const dbUser = await userController.createUser(user)
accessToken = await generateAndSaveToken(dbUser.uid)
accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
path: '/SASjsApi/stp/execute',
type: PermissionType.route,
principalType: PrincipalType.user,
principalId: dbUser.uid,
principalId: dbUser.id,
setting: PermissionSettingForRoute.grant
})
})
@@ -456,7 +456,7 @@ const makeRequestAndAssert = async (
)
}
const generateAndSaveToken = async (userId: string) => {
const generateAndSaveToken = async (userId: number) => {
const accessToken = generateAccessToken({
clientId,
userId

View File

@@ -1,16 +1,10 @@
import { randomBytes } from 'crypto'
import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController, GroupController } from '../../../controllers/'
import {
generateAccessToken,
saveTokensInDB,
AuthProviderType
} from '../../../utils'
import User from '../../../model/User'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
const clientId = 'someclientID'
const adminUser = {
@@ -102,9 +96,9 @@ describe('user', () => {
const dbUser = await controller.createUser(user)
const accessToken = generateAccessToken({
clientId,
userId: dbUser.uid
userId: dbUser.id
})
await saveTokensInDB(dbUser.uid, clientId, accessToken, 'refreshToken')
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
const res = await request(app)
.post('/SASjsApi/user')
@@ -116,16 +110,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Conflict if username is already present', async () => {
it('should respond with Forbidden if username is already present', async () => {
await controller.createUser(user)
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send(user)
.expect(409)
.expect(403)
expect(res.text).toEqual('Username already exists.')
expect(res.text).toEqual('Error: Username already exists.')
expect(res.body).toEqual({})
})
@@ -188,7 +182,7 @@ describe('user', () => {
const newDisplayName = 'My new display Name'
const res = await request(app)
.patch(`/SASjsApi/user/${dbUser.uid}`)
.patch(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...user, displayName: newDisplayName })
.expect(200)
@@ -201,11 +195,11 @@ describe('user', () => {
it('should respond with updated user when user himself requests', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const newDisplayName = 'My new display Name'
const res = await request(app)
.patch(`/SASjsApi/user/${dbUser.uid}`)
.patch(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' })
.send({
displayName: newDisplayName,
@@ -222,46 +216,16 @@ describe('user', () => {
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const newDisplayName = 'My new display Name'
await request(app)
.patch(`/SASjsApi/user/${dbUser.uid}`)
.patch(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ ...user, displayName: newDisplayName })
.expect(400)
})
it('should respond with Method Not Allowed, when updating username of user created by an external auth provider', async () => {
const dbUser = await User.create({
...user,
authProvider: AuthProviderType.LDAP
})
const accessToken = await generateAndSaveToken(dbUser!.id)
const newUsername = 'newUsername'
await request(app)
.patch(`/SASjsApi/user/${dbUser!.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ username: newUsername })
.expect(405)
})
it('should respond with Method Not Allowed, when updating displayName of user created by an external auth provider', async () => {
const dbUser = await User.create({
...user,
authProvider: AuthProviderType.LDAP
})
const accessToken = await generateAndSaveToken(dbUser!.id)
const newDisplayName = 'My new display Name'
await request(app)
.patch(`/SASjsApi/user/${dbUser!.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ displayName: newDisplayName })
.expect(405)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/user/1234')
@@ -278,10 +242,10 @@ describe('user', () => {
...user,
username: 'randomUser'
})
const accessToken = await generateAndSaveToken(dbUser2.uid)
const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.uid}`)
.patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(accessToken, { type: 'bearer' })
.send(user)
.expect(401)
@@ -290,7 +254,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Conflict if username is already present', async () => {
it('should respond with Forbidden if username is already present', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
@@ -298,12 +262,12 @@ describe('user', () => {
})
const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.uid}`)
.patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(409)
.expect(403)
expect(res.text).toEqual('Username already exists.')
expect(res.text).toEqual('Error: Username already exists.')
expect(res.body).toEqual({})
})
@@ -326,7 +290,7 @@ describe('user', () => {
it('should respond with updated user when user himself requests', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const newDisplayName = 'My new display Name'
const res = await request(app)
@@ -347,7 +311,7 @@ describe('user', () => {
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const newDisplayName = 'My new display Name'
await request(app)
@@ -373,10 +337,10 @@ describe('user', () => {
...user,
username: 'randomUser'
})
const accessToken = await generateAndSaveToken(dbUser2.uid)
const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.uid}`)
.patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(accessToken, { type: 'bearer' })
.send(user)
.expect(401)
@@ -385,7 +349,7 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Conflict if username is already present', async () => {
it('should respond with Forbidden if username is already present', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
@@ -396,9 +360,9 @@ describe('user', () => {
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(409)
.expect(403)
expect(res.text).toEqual('Username already exists.')
expect(res.text).toEqual('Error: Username already exists.')
expect(res.body).toEqual({})
})
})
@@ -419,7 +383,7 @@ describe('user', () => {
const dbUser = await controller.createUser(user)
const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.uid}`)
.delete(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
@@ -429,10 +393,10 @@ describe('user', () => {
it('should respond with OK when user himself requests', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.uid}`)
.delete(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: user.password })
.expect(200)
@@ -442,10 +406,10 @@ describe('user', () => {
it('should respond with Bad Request when user himself requests and password is missing', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.uid}`)
.delete(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
@@ -470,10 +434,10 @@ describe('user', () => {
...user,
username: 'randomUser'
})
const accessToken = await generateAndSaveToken(dbUser2.uid)
const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app)
.delete(`/SASjsApi/user/${dbUser1.uid}`)
.delete(`/SASjsApi/user/${dbUser1.id}`)
.auth(accessToken, { type: 'bearer' })
.send(user)
.expect(401)
@@ -482,17 +446,17 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.uid}`)
.delete(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(401)
.expect(403)
expect(res.text).toEqual('Invalid password.')
expect(res.text).toEqual('Error: Invalid password.')
expect(res.body).toEqual({})
})
@@ -511,7 +475,7 @@ describe('user', () => {
it('should respond with OK when user himself requests', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
@@ -524,7 +488,7 @@ describe('user', () => {
it('should respond with Bad Request when user himself requests and password is missing', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
@@ -552,7 +516,7 @@ describe('user', () => {
...user,
username: 'randomUser'
})
const accessToken = await generateAndSaveToken(dbUser2.uid)
const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser1.username}`)
@@ -564,17 +528,17 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(401)
.expect(403)
expect(res.text).toEqual('Invalid password.')
expect(res.text).toEqual('Error: Invalid password.')
expect(res.body).toEqual({})
})
})
@@ -593,7 +557,7 @@ describe('user', () => {
it('should respond with user autoExec when same user requests', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.uid
const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId)
const res = await request(app)
@@ -612,7 +576,7 @@ describe('user', () => {
it('should respond with user autoExec when admin user requests', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.uid
const userId = dbUser.id
const res = await request(app)
.get(`/SASjsApi/user/${userId}`)
@@ -635,7 +599,7 @@ describe('user', () => {
})
const dbUser = await controller.createUser(user)
const userId = dbUser.uid
const userId = dbUser.id
const res = await request(app)
.get(`/SASjsApi/user/${userId}`)
@@ -653,7 +617,7 @@ describe('user', () => {
it('should respond with user along with associated groups', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.uid
const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId)
const group = {
@@ -662,7 +626,7 @@ describe('user', () => {
}
const groupController = new GroupController()
const dbGroup = await groupController.createGroup(group)
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid)
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
const res = await request(app)
.get(`/SASjsApi/user/${userId}`)
@@ -688,25 +652,23 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Not Found if userId is incorrect', async () => {
it('should respond with Forbidden if userId is incorrect', async () => {
await controller.createUser(user)
const hexValue = randomBytes(12).toString('hex')
const res = await request(app)
.get(`/SASjsApi/user/${hexValue}`)
.get('/SASjsApi/user/1234')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
.expect(403)
expect(res.text).toEqual('User is not found.')
expect(res.text).toEqual('Error: User is not found.')
expect(res.body).toEqual({})
})
describe('by username', () => {
it('should respond with user autoExec when same user requests', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.uid
const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId)
const res = await request(app)
@@ -769,16 +731,16 @@ describe('user', () => {
expect(res.body).toEqual({})
})
it('should respond with Not Found if username is incorrect', async () => {
it('should respond with Forbidden if username is incorrect', async () => {
await controller.createUser(user)
const res = await request(app)
.get('/SASjsApi/user/by/username/randomUsername')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
.expect(403)
expect(res.text).toEqual('User is not found.')
expect(res.text).toEqual('Error: User is not found.')
expect(res.body).toEqual({})
})
})
@@ -806,13 +768,13 @@ describe('user', () => {
expect(res.body).toEqual([
{
uid: expect.anything(),
id: expect.anything(),
username: adminUser.username,
displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin
},
{
uid: expect.anything(),
id: expect.anything(),
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin
@@ -834,13 +796,13 @@ describe('user', () => {
expect(res.body).toEqual([
{
uid: expect.anything(),
id: expect.anything(),
username: adminUser.username,
displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin
},
{
uid: expect.anything(),
id: expect.anything(),
username: 'randomUser',
displayName: user.displayName,
isAdmin: user.isAdmin
@@ -862,10 +824,10 @@ const generateSaveTokenAndCreateUser = async (
): Promise<string> => {
const dbUser = await controller.createUser(someUser ?? adminUser)
return generateAndSaveToken(dbUser.uid)
return generateAndSaveToken(dbUser.id)
}
const generateAndSaveToken = async (userId: string) => {
const generateAndSaveToken = async (userId: number) => {
const adminAccessToken = generateAccessToken({
clientId,
userId

View File

@@ -47,6 +47,72 @@ describe('web', () => {
})
})
describe('SASLogon/login', () => {
let csrfToken: string
beforeAll(async () => {
;({ csrfToken } = await getCSRF(app))
})
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond with successful login', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
password: user.password
})
.expect(200)
expect(res.body.loggedIn).toBeTruthy()
expect(res.body.user).toEqual({
id: expect.any(Number),
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin
})
})
it('should respond with Bad Request if CSRF Token is not present', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if CSRF Token is invalid', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', 'INVALID_CSRF_TOKEN')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
})
describe('SASLogon/authorize', () => {
let csrfToken: string
let authCookies: string
@@ -117,147 +183,6 @@ describe('web', () => {
expect(res.body).toEqual({})
})
})
describe('SASLogon/login', () => {
let csrfToken: string
beforeAll(async () => {
;({ csrfToken } = await getCSRF(app))
})
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond with successful login', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
password: user.password
})
.expect(200)
expect(res.body.loggedIn).toBeTruthy()
expect(res.body.user).toEqual({
id: expect.any(String),
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin,
needsToUpdatePassword: true
})
})
it('should respond with too many requests when attempting with invalid password for a same user too many times', async () => {
await userController.createUser(user)
const promises: request.Test[] = []
const maxConsecutiveFailsByUsernameAndIp = Number(
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
)
Array(maxConsecutiveFailsByUsernameAndIp + 1)
.fill(0)
.map((_, i) => {
promises.push(
request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
password: 'invalid-password'
})
)
})
await Promise.all(promises)
const res = await request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
password: user.password
})
.expect(429)
expect(res.text).toContain('Too Many Requests!')
})
it('should respond with too many requests when attempting with invalid credentials for different users but with same ip too many times', async () => {
await userController.createUser(user)
const promises: request.Test[] = []
const maxWrongAttemptsByIpPerDay = Number(
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
)
Array(maxWrongAttemptsByIpPerDay + 1)
.fill(0)
.map((_, i) => {
promises.push(
request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({
username: `user${i}`,
password: 'invalid-password'
})
)
})
await Promise.all(promises)
const res = await request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
password: user.password
})
.expect(429)
expect(res.text).toContain('Too Many Requests!')
})
it('should respond with Bad Request if CSRF Token is not present', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if CSRF Token is invalid', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', 'INVALID_CSRF_TOKEN')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
})
})
const getCSRF = async (app: Express) => {

View File

@@ -13,11 +13,7 @@ stpRouter.get('/execute', async (req, res) => {
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.executeGetRequest(
req,
query._program,
query._debug
)
const response = await controller.executeGetRequest(req, query._program)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)

View File

@@ -9,7 +9,6 @@ import {
deleteUserValidation,
getUserValidation,
registerUserValidation,
uidValidation,
updateUserValidation
} from '../../utils'
@@ -24,7 +23,7 @@ userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
const response = await controller.createUser(body)
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
res.status(403).send(err.toString())
}
})
@@ -34,7 +33,7 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllUsers()
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
res.status(403).send(err.toString())
}
})
@@ -52,23 +51,20 @@ userRouter.get(
const response = await controller.getUserByUsername(req, username)
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
res.status(403).send(err.toString())
}
}
)
userRouter.get('/:uid', authenticateAccessToken, async (req, res) => {
const { error, value: params } = uidValidation(req.params)
if (error) return res.status(400).send(error.details[0].message)
const { uid } = params
userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
const { userId } = req.params
const controller = new UserController()
try {
const response = await controller.getUser(req, uid)
const response = await controller.getUser(req, parseInt(userId))
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
res.status(403).send(err.toString())
}
})
@@ -95,22 +91,18 @@ userRouter.patch(
const response = await controller.updateUserByUsername(username, body)
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
res.status(403).send(err.toString())
}
}
)
userRouter.patch(
'/:uid',
'/:userId',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req, res) => {
const { user } = req
const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
const { userId } = req.params
// only an admin can update `isActive` and `isAdmin` fields
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
@@ -118,10 +110,10 @@ userRouter.patch(
const controller = new UserController()
try {
const response = await controller.updateUser(uid, body)
const response = await controller.updateUser(parseInt(userId), body)
res.send(response)
} catch (err: any) {
res.status(err.code).send(err.message)
res.status(403).send(err.toString())
}
}
)
@@ -149,22 +141,18 @@ userRouter.delete(
await controller.deleteUserByUsername(username, data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(err.code).send(err.message)
res.status(403).send(err.toString())
}
}
)
userRouter.delete(
'/:uid',
'/:userId',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req, res) => {
const { user } = req
const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
const { userId } = req.params
// only an admin can delete user without providing password
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
@@ -172,10 +160,10 @@ userRouter.delete(
const controller = new UserController()
try {
await controller.deleteUser(uid, data, user!.isAdmin)
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(err.code).send(err.message)
res.status(403).send(err.toString())
}
}
)

View File

@@ -58,7 +58,7 @@ export const publishAppStream = async (
)
const sasJsPort = process.env.PORT || 5000
process.logger.info(
console.log(
'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
)

View File

@@ -15,5 +15,5 @@ export const setupRoutes = (app: Express) => {
appStreamRouter(req, res, next)
})
app.use('/', webRouter)
app.use('/', csrfProtection, webRouter)
}

View File

@@ -3,7 +3,6 @@ import sas9WebRouter from './sas9-web'
import sasViyaWebRouter from './sasviya-web'
import webRouter from './web'
import { MOCK_SERVERTYPEType } from '../../utils'
import { csrfProtection } from '../../middlewares'
const router = express.Router()
@@ -19,7 +18,7 @@ switch (MOCK_SERVERTYPE) {
break
}
default: {
router.use('/', csrfProtection, webRouter)
router.use('/', webRouter)
}
}

View File

@@ -2,25 +2,12 @@ import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers'
import { MockSas9Controller } from '../../controllers/mock-sas9'
import multer from 'multer'
import path from 'path'
import dotenv from 'dotenv'
import { FileUploadController } from '../../controllers/internal'
dotenv.config()
const sas9WebRouter = express.Router()
const webController = new WebController()
// Mock controller must be singleton because it keeps the states
// for example `isLoggedIn` and potentially more in future mocks
const controller = new MockSas9Controller()
const fileUploadController = new FileUploadController()
const mockPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
const upload = multer({
dest: path.join(process.cwd(), mockPath, 'sas9', 'files-received')
})
sas9WebRouter.get('/', async (req, res) => {
let response
@@ -40,7 +27,7 @@ sas9WebRouter.get('/', async (req, res) => {
})
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
const response = await controller.sasStoredProcess(req)
const response = await controller.sasStoredProcess()
if (response.redirect) {
res.redirect(response.redirect)
@@ -54,8 +41,8 @@ sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
}
})
sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
const response = await controller.sasStoredProcessDoGet(req)
sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
const response = await controller.sasStoredProcessDo(req)
if (response.redirect) {
res.redirect(response.redirect)
@@ -69,26 +56,6 @@ sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
}
})
sas9WebRouter.post(
'/SASStoredProcess/do/',
fileUploadController.preUploadMiddleware,
fileUploadController.getMulterUploadObject().any(),
async (req, res) => {
const response = await controller.sasStoredProcessDoPost(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
const response = await controller.loginGet()

View File

@@ -1,11 +1,7 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers/web'
import {
authenticateAccessToken,
bruteForceProtection,
desktopRestrict
} from '../../middlewares'
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
import { authorizeValidation, loginWebValidation } from '../../utils'
const webRouter = express.Router()
@@ -18,10 +14,7 @@ webRouter.get('/', async (req, res) => {
} catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>'
} finally {
const { ALLOWED_DOMAIN } = process.env
const allowedDomain = ALLOWED_DOMAIN?.trim()
const domain = allowedDomain ? ` Domain=${allowedDomain};` : ''
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()};${domain} Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
@@ -31,26 +24,17 @@ webRouter.get('/', async (req, res) => {
}
})
webRouter.post(
'/SASLogon/login',
desktopRestrict,
bruteForceProtection,
async (req, res) => {
const { error, value: body } = loginWebValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
const { error, value: body } = loginWebValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.login(req, body)
res.send(response)
} catch (err: any) {
if (err instanceof Error) {
res.status(500).send(err.toString())
} else {
res.status(err.code).send(err.message)
}
}
try {
const response = await controller.login(req, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
)
})
webRouter.post(
'/SASLogon/authorize',

View File

@@ -7,11 +7,11 @@ appPromise.then(async (app) => {
const protocol = process.env.PROTOCOL || 'http'
const sasJsPort = process.env.PORT || 5000
process.logger.info('PROTOCOL: ', protocol)
console.log('PROTOCOL: ', protocol)
if (protocol !== 'https') {
app.listen(sasJsPort, () => {
process.logger.info(
console.log(
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
)
})
@@ -20,7 +20,7 @@ appPromise.then(async (app) => {
const httpsServer = createServer({ key, cert, ca }, app)
httpsServer.listen(sasJsPort, () => {
process.logger.info(
console.log(
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
)
})

View File

@@ -1,4 +1,4 @@
export interface InfoJWT {
clientId: string
userId: string
userId: number
}

View File

@@ -1,6 +1,6 @@
export interface PreProgramVars {
username: string
userId: string
userId: number
displayName: string
serverUrl: string
httpHeaders: string[]

View File

@@ -1,10 +1,9 @@
export interface RequestUser {
userId: string
userId: number
clientId: string
username: string
displayName: string
isAdmin: boolean
isActive: boolean
needsToUpdatePassword: boolean
autoExec?: string
}

View File

@@ -8,5 +8,4 @@ export interface Session {
consumed: boolean
completed: boolean
crashed?: string
expiresAfterMins?: { mins: number; used: boolean }
}

View File

@@ -5,11 +5,9 @@ declare namespace NodeJS {
pythonLoc?: string
rLoc?: string
driveLoc: string
sasjsRoot: string
logsLoc: string
logsUUID: string
sessionController?: import('../../controllers/internal').SessionController
sasSessionController?: import('../../controllers/internal').SASSessionController
appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[]

View File

@@ -36,7 +36,7 @@ export const loadAppStreamConfig = async () => {
)
}
process.logger.info('App Stream Config loaded!')
console.log('App Stream Config loaded!')
}
export const addEntryToAppStreamConfig = (

View File

@@ -8,6 +8,6 @@ export const connectDB = async () => {
throw new Error('Unable to connect to DB!')
}
process.logger.success('Connected to DB!')
console.log('Connected to DB!')
return seedDB()
}

View File

@@ -12,7 +12,7 @@ import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
export const copySASjsCore = async () => {
if (process.env.NODE_ENV === 'test') return
process.logger.log('Copying Macros from container to drive.')
console.log('Copying Macros from container to drive(tmp).')
const macrosDrivePath = getMacrosFolder()
@@ -30,5 +30,5 @@ export const copySASjsCore = async () => {
await createFile(macroFileDestPath, macroContent)
})
process.logger.info('Macros Drive Path:', macrosDrivePath)
console.log('Macros Drive Path:', macrosDrivePath)
}

View File

@@ -1,18 +0,0 @@
import path from 'path'
import { createFile } from '@sasjs/utils'
import { getMacrosFolder } from './file'
const fileContent = `%macro webout(action,ds,dslabel=,fmt=,missing=NULL,showmeta=NO,maxobs=MAX);
%ms_webout(&action,ds=&ds,dslabel=&dslabel,fmt=&fmt
,missing=&missing
,showmeta=&showmeta
,maxobs=&maxobs
)
%mend;`
export const createWeboutSasFile = async () => {
const macrosDrivePath = getMacrosFolder()
process.logger.log(`Creating webout.sas at ${macrosDrivePath}`)
const filePath = path.join(macrosDrivePath, 'webout.sas')
await createFile(filePath, fileContent)
}

View File

@@ -1,4 +0,0 @@
import { randomBytes } from 'crypto'
export const randomBytesHexString = (bytesCount: number) =>
randomBytes(bytesCount).toString('hex')

View File

@@ -10,7 +10,7 @@ export const sysInitCompiledPath = path.join(
'systemInitCompiled.sas'
)
export const sasJSCoreMacros = path.join(apiRoot, 'sas', 'sasautos')
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
@@ -20,24 +20,19 @@ export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
export const getDesktopUserAutoExecPath = () =>
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
export const getSasjsRootFolder = () => process.sasjsRoot
export const getSasjsDriveFolder = () => process.driveLoc
export const getSasjsRootFolder = () => process.driveLoc
export const getLogFolder = () => process.logsLoc
export const getAppStreamConfigPath = () =>
path.join(getSasjsDriveFolder(), 'appStreamConfig.json')
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
export const getMacrosFolder = () =>
path.join(getSasjsDriveFolder(), 'sas', 'sasautos')
export const getPackagesFolder = () =>
path.join(getSasjsDriveFolder(), 'sas', 'sas_packages')
path.join(getSasjsRootFolder(), 'sasjscore')
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
export const getFilesFolder = () => path.join(getSasjsDriveFolder(), 'files')
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')

View File

@@ -1,8 +1,7 @@
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
export const generateAccessToken = (data: InfoJWT, expiry?: number) =>
export const generateAccessToken = (data: InfoJWT) =>
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
expiresIn: '1day'
})

View File

@@ -1,8 +1,7 @@
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
export const generateRefreshToken = (data: InfoJWT, expiry?: number) =>
export const generateRefreshToken = (data: InfoJWT) =>
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
expiresIn: '30 days'
})

View File

@@ -1,8 +1,7 @@
import { Request } from 'express'
export const TopLevelRoutes = ['/AppStream', '/SASjsApi']
const StaticAuthorizedRoutes = [
'/AppStream',
'/SASjsApi/code/execute',
'/SASjsApi/stp/execute',
'/SASjsApi/drive/deploy',
@@ -16,7 +15,7 @@ const StaticAuthorizedRoutes = [
export const getAuthorizedRoutes = () => {
const streamingApps = Object.keys(process.appStreamConfig)
const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`)
return [...TopLevelRoutes, ...StaticAuthorizedRoutes, ...streamingAppsRoutes]
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
}
export const getPath = (req: Request) => {

View File

@@ -10,9 +10,9 @@ export const getCertificates = async () => {
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
const caPath = CA_ROOT
process.logger.info('keyPath: ', keyPath)
process.logger.info('certPath: ', certPath)
if (caPath) process.logger.info('caPath: ', caPath)
console.log('keyPath: ', keyPath)
console.log('certPath: ', certPath)
if (caPath) console.log('caPath: ', caPath)
const key = await readFile(keyPath)
const cert = await readFile(certPath)

View File

@@ -18,12 +18,10 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
//In desktop mode when mocking mode is enabled, user was undefined.
//So this is workaround.
return {
username: user ? user.username : 'demo',
userId: user ? user.userId : 'demoId',
displayName: user ? user.displayName : 'demo',
username: user!.username,
userId: user!.userId,
displayName: user!.displayName,
serverUrl: protocol + host,
httpHeaders
}

View File

@@ -4,7 +4,7 @@ import User from '../model/User'
const isValidToken = async (
token: string,
key: string,
userId: string,
userId: number,
clientId: string
) => {
const promise = new Promise<boolean>((resolve, reject) =>
@@ -22,8 +22,8 @@ const isValidToken = async (
return await promise.then(() => true).catch(() => false)
}
export const getTokensFromDB = async (userId: string, clientId: string) => {
const user = await User.findOne({ _id: userId })
export const getTokensFromDB = async (userId: number, clientId: string) => {
const user = await User.findOne({ id: userId })
if (!user) return
const currentTokenObj = user.tokens.find(

View File

@@ -1,8 +1,6 @@
export * from './appStreamConfig'
export * from './connectDB'
export * from './copySASjsCore'
export * from './createWeboutSasFile'
export * from './crypto'
export * from './desktopAutoExec'
export * from './extractHeaders'
export * from './extractName'
@@ -20,17 +18,14 @@ export * from './getTokensFromDB'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './isPublicRoute'
export * from './ldapClient'
export * from './zipped'
export * from './parseLogToArray'
export * from './rateLimiter'
export * from './removeTokensInDB'
export * from './saveTokensInDB'
export * from './seedDB'
export * from './setProcessVariables'
export * from './setupFolders'
export * from './setupUserAutoExec'
export * from './upload'
export * from './validation'
export * from './verifyEnvVariables'
export * from './verifyTokenInDB'
export * from './zipped'

View File

@@ -22,11 +22,10 @@ export const isPublicRoute = async (req: Request): Promise<boolean> => {
}
export const publicUser: RequestUser = {
userId: 'public_user_id',
userId: 0,
clientId: 'public_app',
username: 'publicUser',
displayName: 'Public User',
isAdmin: false,
isActive: true,
needsToUpdatePassword: false
isActive: true
}

View File

@@ -1,163 +0,0 @@
import { createClient, Client } from 'ldapjs'
import { ReturnCode } from './verifyEnvVariables'
export interface LDAPUser {
uid: string
username: string
displayName: string
}
export interface LDAPGroup {
name: string
members: string[]
}
export class LDAPClient {
private ldapClient: Client
private static classInstance: LDAPClient | null
private constructor() {
process.logger.info('creating LDAP client')
this.ldapClient = createClient({ url: process.env.LDAP_URL as string })
this.ldapClient.on('error', (error) => {
process.logger.error(error.message)
})
}
static async init() {
if (!LDAPClient.classInstance) {
LDAPClient.classInstance = new LDAPClient()
process.logger.info('binding LDAP client')
await LDAPClient.classInstance.bind().catch((error) => {
LDAPClient.classInstance = null
throw error
})
}
return LDAPClient.classInstance
}
private async bind() {
const promise = new Promise<void>((resolve, reject) => {
const { LDAP_BIND_DN, LDAP_BIND_PASSWORD } = process.env
this.ldapClient.bind(LDAP_BIND_DN!, LDAP_BIND_PASSWORD!, (error) => {
if (error) reject(error)
resolve()
})
})
await promise.catch((error) => {
throw new Error(error.message)
})
}
async getAllLDAPUsers() {
const promise = new Promise<LDAPUser[]>((resolve, reject) => {
const { LDAP_USERS_BASE_DN } = process.env
const filter = `(objectClass=*)`
this.ldapClient.search(
LDAP_USERS_BASE_DN!,
{ filter },
(error, result) => {
if (error) reject(error)
const users: LDAPUser[] = []
result.on('searchEntry', (entry) => {
users.push({
uid: entry.object.uid as string,
username: entry.object.username as string,
displayName: entry.object.displayname as string
})
})
result.on('end', (result) => {
resolve(users)
})
}
)
})
return await promise
.then((res) => res)
.catch((error) => {
throw new Error(error.message)
})
}
async getAllLDAPGroups() {
const promise = new Promise<LDAPGroup[]>((resolve, reject) => {
const { LDAP_GROUPS_BASE_DN } = process.env
this.ldapClient.search(LDAP_GROUPS_BASE_DN!, {}, (error, result) => {
if (error) reject(error)
const groups: LDAPGroup[] = []
result.on('searchEntry', (entry) => {
const members =
typeof entry.object.memberuid === 'string'
? [entry.object.memberuid]
: entry.object.memberuid
groups.push({
name: entry.object.cn as string,
members
})
})
result.on('end', (result) => {
resolve(groups)
})
})
})
return await promise
.then((res) => res)
.catch((error) => {
throw new Error(error.message)
})
}
async verifyUser(username: string, password: string) {
const promise = new Promise<boolean>((resolve, reject) => {
const { LDAP_USERS_BASE_DN } = process.env
const filter = `(username=${username})`
this.ldapClient.search(
LDAP_USERS_BASE_DN!,
{ filter },
(error, result) => {
if (error) reject(error)
const items: any = []
result.on('searchEntry', (entry) => {
items.push(entry.object)
})
result.on('end', (result) => {
if (result?.status !== 0 || items.length === 0) return reject()
// pick the first found
const user = items[0]
this.ldapClient.bind(user.dn, password, (error) => {
if (error) return reject(error)
resolve(true)
})
})
}
)
})
return await promise
.then(() => true)
.catch(() => {
throw new Error('Invalid password.')
})
}
}

View File

@@ -22,12 +22,12 @@ export const getEnvCSPDirectives = (
try {
cspConfigJson = JSON.parse(file)
} catch (e) {
process.logger.error(
console.error(
'Parsing Content Security Policy JSON config failed. Make sure it is valid json'
)
}
} catch (e) {
process.logger.error('Error reading HELMET CSP config file', e)
console.error('Error reading HELMET CSP config file', e)
}
}

View File

@@ -1,123 +0,0 @@
import { RateLimiterMemory } from 'rate-limiter-flexible'
export class RateLimiter {
private static instance: RateLimiter
private limiterSlowBruteByIP: RateLimiterMemory
private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMemory
private maxWrongAttemptsByIpPerDay: number
private maxConsecutiveFailsByUsernameAndIp: number
private constructor() {
const {
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
} = process.env
this.maxWrongAttemptsByIpPerDay = Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY)
this.maxConsecutiveFailsByUsernameAndIp = Number(
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
)
this.limiterSlowBruteByIP = new RateLimiterMemory({
keyPrefix: 'login_fail_ip_per_day',
points: this.maxWrongAttemptsByIpPerDay,
duration: 60 * 60 * 24,
blockDuration: 60 * 60 * 24 // Block for 1 day
})
this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMemory({
keyPrefix: 'login_fail_consecutive_username_and_ip',
points: this.maxConsecutiveFailsByUsernameAndIp,
duration: 60 * 60 * 24 * 24, // Store number for 24 days since first fail
blockDuration: 60 * 60 // Block for 1 hour
})
}
public static getInstance() {
if (!RateLimiter.instance) {
RateLimiter.instance = new RateLimiter()
}
return RateLimiter.instance
}
private getUsernameIPKey(ip: string, username: string) {
return `${username}_${ip}`
}
/**
* This method checks for brute force attack
* If attack is detected then returns the number of seconds after which user can make another request
* Else returns 0
*/
public async check(ip: string, username: string) {
const usernameIPkey = this.getUsernameIPKey(ip, username)
const [resSlowByIP, resUsernameAndIP] = await Promise.all([
this.limiterSlowBruteByIP.get(ip),
this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
])
// NOTE: To make use of blockDuration option, comparison in both following if statements should have greater than symbol
// otherwise, blockDuration option will not work
// For more info see: https://github.com/animir/node-rate-limiter-flexible/wiki/Options#blockduration
// Check if IP or Username + IP is already blocked
if (
resSlowByIP !== null &&
resSlowByIP.consumedPoints > this.maxWrongAttemptsByIpPerDay
) {
return Math.ceil(resSlowByIP.msBeforeNext / 1000)
} else if (
resUsernameAndIP !== null &&
resUsernameAndIP.consumedPoints > this.maxConsecutiveFailsByUsernameAndIp
) {
return Math.ceil(resUsernameAndIP.msBeforeNext / 1000)
}
return 0
}
/**
* Consume 1 point from limiters on wrong attempt and block if limits reached
* If limit is reached, return the number of seconds after which user can make another request
* Else return 0
*/
public async consume(ip: string, username?: string) {
try {
const promises = [this.limiterSlowBruteByIP.consume(ip)]
if (username) {
const usernameIPkey = this.getUsernameIPKey(ip, username)
// Count failed attempts by Username + IP only for registered users
promises.push(
this.limiterConsecutiveFailsByUsernameAndIP.consume(usernameIPkey)
)
}
await Promise.all(promises)
} catch (rlRejected: any) {
if (rlRejected instanceof Error) {
throw rlRejected
} else {
// based upon the implementation of consume method of RateLimiterMemory
// we are sure that rlRejected will contain msBeforeNext
// for further reference,
// see https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#login-endpoint-protection
// or see https://github.com/animir/node-rate-limiter-flexible#ratelimiterres-object
return Math.ceil(rlRejected.msBeforeNext / 1000)
}
}
return 0
}
public async resetOnSuccess(ip: string, username: string) {
const usernameIPkey = this.getUsernameIPKey(ip, username)
const resUsernameAndIP =
await this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
if (resUsernameAndIP !== null && resUsernameAndIP.consumedPoints > 0) {
await this.limiterConsecutiveFailsByUsernameAndIP.delete(usernameIPkey)
}
}
}

View File

@@ -1,7 +1,7 @@
import User from '../model/User'
export const removeTokensInDB = async (userId: string, clientId: string) => {
const user = await User.findOne({ _id: userId })
export const removeTokensInDB = async (userId: number, clientId: string) => {
const user = await User.findOne({ id: userId })
if (!user) return
const tokenObjIndex = user.tokens.findIndex(

View File

@@ -1,12 +1,12 @@
import User from '../model/User'
export const saveTokensInDB = async (
userId: string,
userId: number,
clientId: string,
accessToken: string,
refreshToken: string
) => {
const user = await User.findOne({ _id: userId })
const user = await User.findOne({ id: userId })
if (!user) return
const currentTokenObj = user.tokens.find(

View File

@@ -1,9 +1,7 @@
import bcrypt from 'bcryptjs'
import Client from '../model/Client'
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
import User, { IUser } from '../model/User'
import User from '../model/User'
import Configuration, { ConfigurationType } from '../model/Configuration'
import { ResetAdminPasswordType } from './verifyEnvVariables'
import { randomBytes } from 'crypto'
@@ -21,16 +19,16 @@ export const seedDB = async (): Promise<ConfigurationType> => {
const client = new Client(CLIENT)
await client.save()
process.logger.success(`DB Seed - client created: ${CLIENT.clientId}`)
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
}
// Checking if 'AllUsers' Group is already in the database
let groupExist = await Group.findOne({ name: ALL_USERS_GROUP.name })
let groupExist = await Group.findOne({ name: GROUP.name })
if (!groupExist) {
const group = new Group(ALL_USERS_GROUP)
const group = new Group(GROUP)
groupExist = await group.save()
process.logger.success(`DB Seed - Group created: ${ALL_USERS_GROUP.name}`)
console.log(`DB Seed - Group created: ${GROUP.name}`)
}
// Checking if 'Public' Group is already in the database
@@ -39,28 +37,22 @@ export const seedDB = async (): Promise<ConfigurationType> => {
const group = new Group(PUBLIC_GROUP)
await group.save()
process.logger.success(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
console.log(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
}
const ADMIN_USER = getAdminUser()
// Checking if user is already in the database
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
if (usernameExist) {
usernameExist = await resetAdminPassword(usernameExist, ADMIN_USER.password)
} else {
if (!usernameExist) {
const user = new User(ADMIN_USER)
usernameExist = await user.save()
process.logger.success(
`DB Seed - admin account created: ${ADMIN_USER.username}`
)
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
}
if (usernameExist.isAdmin && !groupExist.hasUser(usernameExist)) {
if (!groupExist.hasUser(usernameExist)) {
groupExist.addUser(usernameExist)
process.logger.success(
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${ALL_USERS_GROUP.name}'`
console.log(
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'`
)
}
@@ -70,7 +62,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
const configuration = new Configuration(SECRETS)
configExist = await configuration.save()
process.logger.success('DB Seed - configuration added')
console.log('DB Seed - configuration added')
}
return {
@@ -81,8 +73,8 @@ export const seedDB = async (): Promise<ConfigurationType> => {
}
}
export const ALL_USERS_GROUP = {
name: 'all-users',
const GROUP = {
name: 'AllUsers',
description: 'Group contains all users'
}
@@ -96,52 +88,11 @@ const CLIENT = {
clientId: 'clientID1',
clientSecret: 'clientSecret'
}
const getAdminUser = () => {
const { ADMIN_USERNAME, ADMIN_PASSWORD_INITIAL } = process.env
const salt = bcrypt.genSaltSync(10)
const hashedPassword = bcrypt.hashSync(ADMIN_PASSWORD_INITIAL as string, salt)
return {
displayName: 'Super Admin',
username: ADMIN_USERNAME,
password: hashedPassword,
isAdmin: true,
isActive: true
}
}
const resetAdminPassword = async (user: IUser, password: string) => {
const { ADMIN_PASSWORD_RESET } = process.env
if (ADMIN_PASSWORD_RESET === ResetAdminPasswordType.YES) {
if (!user.isAdmin) {
process.logger.error(
`Can not reset the password of non-admin user (${user.username}) on startup.`
)
return user
}
if (user.authProvider) {
process.logger.error(
`Can not reset the password of admin (${user.username}) with ${user.authProvider} as authentication mechanism.`
)
return user
}
process.logger.info(
`DB Seed - resetting password for admin user: ${user.username}`
)
user.password = password
user.needsToUpdatePassword = true
user = await user.save()
process.logger.success(`DB Seed - successfully reset the password`)
}
return user
const ADMIN_USER = {
id: 1,
displayName: 'Super Admin',
username: 'secretuser',
password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO',
isAdmin: true,
isActive: true
}

View File

@@ -1,31 +1,9 @@
import path from 'path'
import {
createFolder,
getAbsolutePath,
getRealPath,
fileExists
} from '@sasjs/utils'
import dotenv from 'dotenv'
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
export const setProcessVariables = async () => {
const { execPath } = process
// Check if execPath ends with 'api-macos' to determine executable for MacOS.
// This is needed to fix picking .env file issue in MacOS executable.
if (execPath) {
const envPathSplitted = execPath.split(path.sep)
if (envPathSplitted.pop() === 'api-macos') {
const envPath = path.join(envPathSplitted.join(path.sep), '.env')
// Override environment variables from envPath if file exists
if (await fileExists(envPath)) {
dotenv.config({ path: envPath, override: true })
}
}
}
const { MODE, RUN_TIMES } = process.env
if (MODE === ModeType.Server) {
@@ -41,9 +19,7 @@ export const setProcessVariables = async () => {
}
if (process.env.NODE_ENV === 'test') {
process.sasjsRoot = path.join(process.cwd(), 'sasjs_root')
process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive')
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
return
}
@@ -56,6 +32,7 @@ export const setProcessVariables = async () => {
process.rLoc = process.env.R_PATH
} else {
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
process.sasLoc = sasLoc
process.nodeLoc = nodeLoc
process.pythonLoc = pythonLoc
@@ -64,34 +41,21 @@ export const setProcessVariables = async () => {
const { SASJS_ROOT } = process.env
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
await createFolder(absPath)
process.sasjsRoot = getRealPath(absPath)
const { DRIVE_LOCATION } = process.env
const absDrivePath = getAbsolutePath(
DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'),
process.cwd()
)
await createFolder(absDrivePath)
process.driveLoc = getRealPath(absDrivePath)
process.driveLoc = getRealPath(absPath)
const { LOG_LOCATION } = process.env
const absLogsPath = getAbsolutePath(
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
LOG_LOCATION ?? `sasjs_root${path.sep}logs`,
process.cwd()
)
await createFolder(absLogsPath)
process.logsLoc = getRealPath(absLogsPath)
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
process.logger.info('sasLoc: ', process.sasLoc)
process.logger.info('sasDrive: ', process.driveLoc)
process.logger.info('sasLogs: ', process.logsLoc)
process.logger.info('runTimes: ', process.runTimes)
console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc)
console.log('sasLogs: ', process.logsLoc)
console.log('runTimes: ', process.runTimes)
}

View File

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

View File

@@ -1,11 +0,0 @@
import { createFile, fileExists } from '@sasjs/utils'
import { getDesktopUserAutoExecPath } from './file'
import { ModeType } from './verifyEnvVariables'
export const setupUserAutoExec = async () => {
if (process.env.MODE === ModeType.Desktop) {
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
await createFile(getDesktopUserAutoExecPath(), '')
}
}
}

View File

@@ -51,8 +51,9 @@ export const generateFileUploadSasCode = async (
let fileCount = 0
const uploadedFiles: UploadedFiles[] = []
const sasSessionFolderList: string[] =
await listFilesInFolder(sasSessionFolder)
const sasSessionFolderList: string[] = await listFilesInFolder(
sasSessionFolder
)
sasSessionFolderList.forEach((fileName) => {
let fileCountString = fileCount < 100 ? '0' + fileCount : fileCount
fileCountString = fileCount < 10 ? '00' + fileCount : fileCount

View File

@@ -12,11 +12,6 @@ const groupnameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
export const uidValidation = (data: any) =>
Joi.object({
uid: Joi.string().length(24).hex().required()
}).validate(data)
export const getUserValidation = (data: any): Joi.ValidationResult =>
Joi.object({
username: usernameSchema.required()
@@ -90,18 +85,10 @@ export const updateUserValidation = (
return Joi.object(validationChecks).validate(data)
}
export const updatePasswordValidation = (data: any): Joi.ValidationResult =>
Joi.object({
currentPassword: Joi.string().required(),
newPassword: passwordSchema.required()
}).validate(data)
export const registerClientValidation = (data: any): Joi.ValidationResult =>
Joi.object({
clientId: Joi.string().required(),
clientSecret: Joi.string().required(),
accessTokenExpiration: Joi.number(),
refreshTokenExpiration: Joi.number()
clientSecret: Joi.string().required()
}).validate(data)
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
@@ -118,7 +105,7 @@ export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
principalType: Joi.string()
.required()
.valid(...Object.values(PrincipalType)),
principalId: Joi.string().length(24).hex().required()
principalId: Joi.number().required()
}).validate(data)
export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
@@ -183,17 +170,9 @@ export const runCodeValidation = (data: any): Joi.ValidationResult =>
runTime: Joi.string().valid(...process.runTimes)
}).validate(data)
export const triggerCodeValidation = (data: any): Joi.ValidationResult =>
Joi.object({
code: Joi.string().required(),
runTime: Joi.string().valid(...process.runTimes),
expiresAfterMins: Joi.number().greater(0)
}).validate(data)
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
Joi.object({
_program: Joi.string().required(),
_debug: Joi.number()
_program: Joi.string().required()
})
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
.validate(data)

View File

@@ -8,10 +8,6 @@ export enum ModeType {
Desktop = 'desktop'
}
export enum AuthProviderType {
LDAP = 'ldap'
}
export enum ProtocolType {
HTTP = 'http',
HTTPS = 'https'
@@ -47,16 +43,6 @@ export enum ReturnCode {
InvalidEnv
}
export enum DatabaseType {
MONGO = 'mongodb',
COSMOS_MONGODB = 'cosmos_mongodb'
}
export enum ResetAdminPasswordType {
YES = 'YES',
NO = 'NO'
}
export const verifyEnvVariables = (): ReturnCode => {
const errors: string[] = []
@@ -78,14 +64,6 @@ export const verifyEnvVariables = (): ReturnCode => {
errors.push(...verifyExecutablePaths())
errors.push(...verifyLDAPVariables())
errors.push(...verifyDbType())
errors.push(...verifyRateLimiter())
errors.push(...verifyAdminUserConfig())
if (errors.length) {
process.logger?.error(
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
@@ -126,22 +104,13 @@ const verifyMODE = (): string[] => {
}
if (process.env.MODE === ModeType.Server) {
const { DB_CONNECT, AUTH_PROVIDERS } = process.env
const { DB_CONNECT } = process.env
if (process.env.NODE_ENV !== 'test') {
if (process.env.NODE_ENV !== 'test')
if (!DB_CONNECT)
errors.push(
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
)
if (AUTH_PROVIDERS) {
const authProvidersType = Object.values(AuthProviderType)
if (!authProvidersType.includes(AUTH_PROVIDERS as AuthProviderType))
errors.push(
`- AUTH_PROVIDERS '${AUTH_PROVIDERS}'\n - valid options ${authProvidersType}`
)
}
}
}
return errors
@@ -283,7 +252,7 @@ const verifyRUN_TIMES = (): string[] => {
return errors
}
const verifyExecutablePaths = (): string[] => {
const verifyExecutablePaths = () => {
const errors: string[] = []
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } =
process.env
@@ -311,158 +280,11 @@ const verifyExecutablePaths = (): string[] => {
return errors
}
const verifyLDAPVariables = () => {
const errors: string[] = []
const {
LDAP_URL,
LDAP_BIND_DN,
LDAP_BIND_PASSWORD,
LDAP_USERS_BASE_DN,
LDAP_GROUPS_BASE_DN,
MODE,
AUTH_PROVIDERS
} = process.env
if (MODE === ModeType.Server && AUTH_PROVIDERS === AuthProviderType.LDAP) {
if (!LDAP_URL) {
errors.push(
`- LDAP_URL is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_BIND_DN) {
errors.push(
`- LDAP_BIND_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_BIND_PASSWORD) {
errors.push(
`- LDAP_BIND_PASSWORD is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_USERS_BASE_DN) {
errors.push(
`- LDAP_USERS_BASE_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
)
}
if (!LDAP_GROUPS_BASE_DN) {
errors.push(
`- LDAP_GROUPS_BASE_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
)
}
}
return errors
}
const verifyDbType = () => {
const errors: string[] = []
const { MODE, DB_TYPE } = process.env
if (MODE === ModeType.Server) {
if (DB_TYPE) {
const dbTypes = Object.values(DatabaseType)
if (!dbTypes.includes(DB_TYPE as DatabaseType))
errors.push(`- DB_TYPE '${DB_TYPE}'\n - valid options ${dbTypes}`)
} else {
process.env.DB_TYPE = DEFAULTS.DB_TYPE
}
}
return errors
}
const verifyRateLimiter = () => {
const errors: string[] = []
const {
MODE,
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
} = process.env
if (MODE === ModeType.Server) {
if (MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) {
if (
!isNumeric(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) ||
Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) < 1
) {
errors.push(
`- Invalid value for 'MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY' - Only positive number is acceptable`
)
}
} else {
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY =
DEFAULTS.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
}
if (MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) {
if (
!isNumeric(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) ||
Number(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) < 1
) {
errors.push(
`- Invalid value for 'MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP' - Only positive number is acceptable`
)
}
} else {
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP =
DEFAULTS.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
}
}
return errors
}
const verifyAdminUserConfig = () => {
const errors: string[] = []
const { MODE, ADMIN_USERNAME, ADMIN_PASSWORD_INITIAL, ADMIN_PASSWORD_RESET } =
process.env
if (MODE === ModeType.Server) {
if (ADMIN_USERNAME) {
process.env.ADMIN_USERNAME = ADMIN_USERNAME.toLowerCase()
} else {
process.env.ADMIN_USERNAME = DEFAULTS.ADMIN_USERNAME
}
if (!ADMIN_PASSWORD_INITIAL)
process.env.ADMIN_PASSWORD_INITIAL = DEFAULTS.ADMIN_PASSWORD_INITIAL
if (ADMIN_PASSWORD_RESET) {
const resetPasswordTypes = Object.values(ResetAdminPasswordType)
if (
!resetPasswordTypes.includes(
ADMIN_PASSWORD_RESET as ResetAdminPasswordType
)
)
errors.push(
`- ADMIN_PASSWORD_RESET '${ADMIN_PASSWORD_RESET}'\n - valid options ${resetPasswordTypes}`
)
} else {
process.env.ADMIN_PASSWORD_RESET = DEFAULTS.ADMIN_PASSWORD_RESET
}
}
return errors
}
const isNumeric = (val: string): boolean => {
return !isNaN(Number(val))
}
const DEFAULTS = {
MODE: ModeType.Desktop,
PROTOCOL: ProtocolType.HTTP,
PORT: '5000',
HELMET_COEP: HelmetCoepType.TRUE,
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
RUN_TIMES: RunTimeType.SAS,
DB_TYPE: DatabaseType.MONGO,
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY: '100',
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP: '10',
ADMIN_USERNAME: 'secretuser',
ADMIN_PASSWORD_INITIAL: 'secretpassword',
ADMIN_PASSWORD_RESET: ResetAdminPasswordType.NO
RUN_TIMES: RunTimeType.SAS
}

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