Compare commits

..
3 Commits
Author SHA1 Message Date
semantic-release-bot bb23bac1c0 chore(release): 0.39.8 [skip ci]
## [0.39.8](https://github.com/sasjs/server/compare/v0.39.7...v0.39.8) (2026-07-15)

### Bug Fixes

* **api:** return 200 with embedded log on SAS session failure instead of 400 ([8506950](https://github.com/sasjs/server/commit/850695024f4b7102982c197ec64b5bb45e7d5f90))
2026-07-15 13:17:59 +00:00
Allan BoweandGitHub bf0ddc952f Merge pull request #392 from sasjs/issue-388-fix
fix(api): return 200 with embedded log on SAS session failure instead…
2026-07-15 14:14:42 +01:00
YuryShkoda 850695024f fix(api): return 200 with embedded log on SAS session failure instead of 400
A failed session (e.g. SAS %abort;, or a non-zero JS/PY/R exit) is a
normal outcome of running arbitrary user code, not a request-shape or
server problem. The #388 fix stopped processProgram() from hanging
forever on a failed SAS session, but did so by throwing a
SessionExecutionError, which surfaced as an HTTP 400 with a bespoke
JSON shape - breaking Studio's log tab, which only renders on 2xx.

Align the SAS branch with the pre-existing JS/PY/R behaviour: resolve
instead of throwing, and let Execution.ts fold session.failureReason
into the log the same way it already does for a debug-mode run. This
removes the now-dead SessionExecutionError class and its try/catch
wrapper entirely.

Update the diagrams in api/docs/diagrams/ to match.
2026-07-15 13:59:01 +03:00
64 changed files with 817 additions and 784 deletions
+7
View File
@@ -1,3 +1,10 @@
## [0.39.8](https://github.com/sasjs/server/compare/v0.39.7...v0.39.8) (2026-07-15)
### Bug Fixes
* **api:** return 200 with embedded log on SAS session failure instead of 400 ([8506950](https://github.com/sasjs/server/commit/850695024f4b7102982c197ec64b5bb45e7d5f90))
## [0.39.7](https://github.com/sasjs/server/compare/v0.39.6...v0.39.7) (2026-07-14) ## [0.39.7](https://github.com/sasjs/server/compare/v0.39.6...v0.39.7) (2026-07-14)
+3 -2
View File
@@ -30,8 +30,9 @@ fresh per request inside `processProgram`.
code into the session and drives it to completion/failure, per runtime. code into the session and drives it to completion/failure, per runtime.
- `api/src/controllers/internal/Execution.ts``ExecutionController`, - `api/src/controllers/internal/Execution.ts``ExecutionController`,
the entry point controllers call; acquires a session, calls the entry point controllers call; acquires a session, calls
`processProgram`, reads back `log.log`/`webout.txt`/headers, builds the `processProgram`, reads back `log.log`/`webout.txt`/headers, and builds
HTTP response (or a `SessionExecutionError` on failure). the HTTP response. A failed session (any runtime) is embedded in that
same response, not thrown as a separate error.
- `api/src/controllers/internal/create{SAS,JS,Python,R}Program.ts` - `api/src/controllers/internal/create{SAS,JS,Python,R}Program.ts`
per-runtime code templating (wraps the user's submitted code with per-runtime code templating (wraps the user's submitted code with
boilerplate: variable injection, `_webout` redirection, etc). boilerplate: variable injection, `_webout` redirection, etc).
+23 -18
View File
@@ -8,7 +8,7 @@ fresh interpreter process per request).
```mermaid ```mermaid
flowchart TD flowchart TD
A["HTTP request<br/>POST /SASjsApi/code/execute<br/>or /SASjsApi/stp/execute"] --> B["Controller<br/>code.ts / stp.ts<br/>try/catch wrapper"] A["HTTP request<br/>POST /SASjsApi/code/execute<br/>or /SASjsApi/stp/execute"] --> B["Controller<br/>code.ts / stp.ts<br/>try/catch wrapper"]
B --> C["ExecutionController.executeProgram<br/>or .executeFile<br/>Execution.ts:58-91"] B --> C["ExecutionController.executeProgram<br/>or .executeFile<br/>Execution.ts:40-77"]
C --> D{"session already provided?<br/>(e.g. file-upload flow)"} C --> D{"session already provided?<br/>(e.g. file-upload flow)"}
D -- no --> E["getSessionController(runTime).getSession()<br/>Session.ts:59-69"] D -- no --> E["getSessionController(runTime).getSession()<br/>Session.ts:59-69"]
D -- yes --> F["use provided session"] D -- yes --> F["use provided session"]
@@ -18,30 +18,28 @@ flowchart TD
H --> J["pre-warm: if pool has fewer<br/>than 3 pending, fire off more<br/>createSession() calls (not awaited)<br/>Session.ts:66"] H --> J["pre-warm: if pool has fewer<br/>than 3 pending, fire off more<br/>createSession() calls (not awaited)<br/>Session.ts:66"]
I --> J I --> J
F --> K F --> K
J --> K["session.state = running<br/>Execution.ts:96"] J --> K["session.state = running<br/>Execution.ts:78"]
K --> L["write webout.txt (empty),<br/>reqHeaders.txt<br/>Execution.ts:103-107"] K --> L["write webout.txt (empty),<br/>reqHeaders.txt<br/>Execution.ts:85-89"]
L --> M["processProgram(...)<br/>processProgram.ts:16-27"] L --> M["processProgram(...)<br/>Execution.ts:96-107"]
M --> N{"runTime?"} M --> N{"runTime?"}
N -- SAS --> O["createSASProgram():<br/>wrap user code with _webout<br/>filename, macro vars, autoexec"] N -- SAS --> O["createSASProgram():<br/>wrap user code with _webout<br/>filename, macro vars, autoexec"]
O --> P["write code.sas via .bkp + rename<br/>processProgram.ts:48-49"] O --> P["write code.sas via .bkp + rename<br/>processProgram.ts:48-49"]
P --> Q["poll: while session.state !== completed<br/>processProgram.ts:52-58"] P --> Q["poll: while state !== completed<br/>AND state !== failed<br/>processProgram.ts:58-63"]
Q -- "state becomes completed" --> R["resolve"] Q --> R["processProgram() resolves either way -<br/>state was set by the session's own<br/>process exit handler in Session.ts,<br/>see sas-execution-handshake.md"]
Q -- "state becomes failed" --> S["throw Error(session.failureReason)"]
N -- "JS / PY / R" --> T["createJSProgram / createPythonProgram /<br/>createRProgram: wrap user code similarly"] N -- "JS / PY / R" --> T["createJSProgram / createPythonProgram /<br/>createRProgram: wrap user code similarly"]
T --> U["write code file; spawn interpreter<br/>fresh via execFile; pipe stdout/stderr<br/>into a log.log write stream<br/>processProgram.ts:108-134"] T --> U["write code file; spawn interpreter<br/>fresh via execFile; pipe stdout/stderr<br/>into a log.log write stream<br/>processProgram.ts:117-124"]
U -- "exit 0" --> R U -- "exit 0" --> Uc["session.state = completed<br/>processProgram.ts:126"]
U -- "exit non-zero" --> S U -- "exit non-zero" --> Uf["session.state = failed<br/>session.failureReason = err.toString()<br/>processProgram.ts:131-133"]
Uc --> R
Uf --> R
R --> V["read log.log, webout.txt,<br/>stpsrv_header.txt<br/>Execution.ts:128-131"] R --> V["read log.log, webout.txt,<br/>stpsrv_header.txt<br/>Execution.ts:109-113, 121-125"]
V --> W["build httpHeaders + result<br/>return ExecuteReturnRaw<br/>Execution.ts:128-151"] V --> W["guard: if state !== failed,<br/>set state = completed<br/>(don't overwrite a failed session)<br/>Execution.ts:127-137"]
W --> X["Controller: res.send(result)<br/>HTTP 200"] W --> X["build httpHeaders + result;<br/>embed log in result if<br/>isDebugOn(vars) or<br/>session.failureReason is set<br/>Execution.ts:139-164"]
X --> Y["Controller: res.send(result)<br/>HTTP 200<br/>(log embedded if the session failed -<br/>same shape as a successful run)"]
S --> Y["catch in Execution.ts:109-126:<br/>read whatever log exists,<br/>throw SessionExecutionError(message, log)"]
Y --> Z["Controller catch block<br/>rethrow { code: 400, status: 'failure',<br/>message, error, log }"]
Z --> AA["res.status(err.code).send(err)<br/>HTTP 400 with complete log"]
``` ```
## Notes ## Notes
@@ -55,9 +53,16 @@ flowchart TD
`failed` either way, and both paths flow through the same log/webout `failed` either way, and both paths flow through the same log/webout
reading logic in `Execution.ts` afterward. The mechanics of *how* that reading logic in `Execution.ts` afterward. The mechanics of *how* that
state gets set differ substantially (see `session-lifecycle.md`). state gets set differ substantially (see `session-lifecycle.md`).
- **A failed session never throws.** `processProgram()` resolves normally
whether the session completed or failed - a failed session (e.g. SAS
`%abort;`, or a non-zero JS/PY/R exit) is a normal outcome of running
arbitrary user code, not a request-shape/server problem. There is no
separate error path: the same `Execution.ts:139-164` logic that embeds
the log for a debug-mode successful run also embeds it when
`session.failureReason` is set, and the controller always responds 200.
- **`includePrintOutput`** (SAS only) additionally appends `output.lst` - **`includePrintOutput`** (SAS only) additionally appends `output.lst`
content to the result when debug mode is on - omitted above for brevity; content to the result when debug mode is on - omitted above for brevity;
see `Execution.ts:135-142`. see `Execution.ts:149-156`.
- **`triggerProgram`/`triggerCode`** (fire-and-forget variants, not shown) - **`triggerProgram`/`triggerCode`** (fire-and-forget variants, not shown)
call the same `ExecutionController` methods without awaiting them and call the same `ExecutionController` methods without awaiting them and
immediately return `{ sessionId }`; the client polls immediately return `{ sessionId }`; the client polls
+10 -10
View File
@@ -33,9 +33,9 @@ sequenceDiagram
Client->>Ctrl: POST .../execute { code, runTime: sas } Client->>Ctrl: POST .../execute { code, runTime: sas }
Ctrl->>Exec: executeProgram({ program, runTime: SAS, ... }) Ctrl->>Exec: executeProgram({ program, runTime: SAS, ... })
Exec->>Pool: getSession() returns the pending session (Session.ts:59-69) Exec->>Pool: getSession() returns the pending session (Session.ts:59-69)
Exec->>Exec: session.state = running (Execution.ts:96) Exec->>Exec: session.state = running (Execution.ts:78)
Exec->>FS: createFile(webout.txt, "") and createFile(reqHeaders.txt, ...) (Execution.ts:103-107) Exec->>FS: createFile(webout.txt, "") and createFile(reqHeaders.txt, ...) (Execution.ts:85-89)
Exec->>Pool: processProgram(program, session, ...) (Execution.ts:110-121) Exec->>Pool: processProgram(program, session, ...) (Execution.ts:96-107)
Pool->>FS: createSASProgram() wraps user code with macro vars,<br/>_webout filename, autoexec injection (createSASProgram.ts) Pool->>FS: createSASProgram() wraps user code with macro vars,<br/>_webout filename, autoexec injection (createSASProgram.ts)
Pool->>FS: write code.sas.bkp, then rename to code.sas (processProgram.ts:48-49) Pool->>FS: write code.sas.bkp, then rename to code.sas (processProgram.ts:48-49)
Note over FS: write-then-rename, not a direct write,<br/>so SAS never reads a partial file Note over FS: write-then-rename, not a direct write,<br/>so SAS never reads a partial file
@@ -45,24 +45,24 @@ sequenceDiagram
Note over SAS: -SYSIN has pointed at code.sas from the start,<br/>so SAS now executes ITS CONTENT as the main job Note over SAS: -SYSIN has pointed at code.sas from the start,<br/>so SAS now executes ITS CONTENT as the main job
SAS->>FS: writes log.log, output.lst, webout.txt while running SAS->>FS: writes log.log, output.lst, webout.txt while running
Pool->>Pool: processProgram poll loop:<br/>while session.state !== completed (processProgram.ts:52-58) Pool->>Pool: processProgram poll loop:<br/>while state !== completed AND state !== failed (processProgram.ts:58-63)
alt program reaches EOF normally alt program reaches EOF normally
SAS->>SAS: exits with code 0 SAS->>SAS: exits with code 0
SAS-->>Pool: execFilePromise resolves - .then() (Session.ts:148-152) SAS-->>Pool: execFilePromise resolves - .then() (Session.ts:148-152)
Pool->>Pool: session.state = completed Pool->>Pool: session.state = completed
Pool-->>Exec: processProgram() resolves else program aborts (e.g. %abort, fatal error, license failure, hard STOP)
else program aborts (fatal error, license failure, hard STOP)
SAS->>SAS: exits with non-zero code SAS->>SAS: exits with non-zero code
SAS-->>Pool: execFilePromise rejects - .catch() (Session.ts:153-163) SAS-->>Pool: execFilePromise rejects - .catch() (Session.ts:153-163)
Pool->>Pool: session.state = failed<br/>session.failureReason = err.toString() Pool->>Pool: session.state = failed<br/>session.failureReason = err.toString()
Pool-->>Exec: processProgram() throws (processProgram.ts:53-55)
end end
deactivate SAS deactivate SAS
Note over Pool,Exec: either way processProgram() just RESOLVES here -<br/>a failed session is a normal outcome of running user<br/>code, not a request-shape/server problem, so it never throws<br/>(processProgram.ts:58-63, same as the JS/PY/R branch below)
Exec->>FS: read log.log, webout.txt, stpsrv_header.txt (Execution.ts:123, 128-131) Exec->>FS: read log.log, webout.txt, stpsrv_header.txt (Execution.ts:109-112, 121-125)
Exec-->>Ctrl: { httpHeaders, result }<br/>or throws SessionExecutionError{ message, log } (Execution.ts:109-126) Note over Exec: if session.failureReason is set, the log is folded into<br/>result the same way isDebugOn(vars) already does for a<br/>successful run - no separate error shape (Execution.ts:158-164)
Ctrl-->>Client: 200 + result<br/>or 400 { status, message, error, log } Exec-->>Ctrl: { httpHeaders, result }
Ctrl-->>Client: 200 + result<br/>(log embedded in result if the session failed)
``` ```
## Why this design ## Why this design
+13 -6
View File
@@ -14,10 +14,10 @@ stateDiagram-v2
initialising --> pending: dummy SYSIN file deleted<br/>by SAS's autoexec<br/>waitForSession(), Session.ts:175-192 initialising --> pending: dummy SYSIN file deleted<br/>by SAS's autoexec<br/>waitForSession(), Session.ts:175-192
initialising --> failed: spawned SAS process exits<br/>before handshake completes<br/>Session.ts:153-163, 184-188 initialising --> failed: spawned SAS process exits<br/>before handshake completes<br/>Session.ts:153-163, 184-188
pending --> running: session picked for a request<br/>executeProgram(), Execution.ts:94-96 pending --> running: session picked for a request<br/>executeProgram(), Execution.ts:76-78
running --> completed: process exits 0<br/>SAS: Session.ts:148-152 (original process)<br/>JS/PY/R: processProgram.ts:116-120 (fresh process) running --> completed: process exits 0<br/>SAS: Session.ts:148-152 (original process)<br/>JS/PY/R: processProgram.ts:124-129 (fresh process)
running --> failed: process exits non-zero<br/>SAS: Session.ts:153-163<br/>JS/PY/R: processProgram.ts:121-131<br/>failureReason = err.toString() running --> failed: process exits non-zero<br/>SAS: Session.ts:153-163<br/>JS/PY/R: processProgram.ts:130-135<br/>failureReason = err.toString()<br/>neither branch throws - see request-execution-flow.md
completed --> [*]: deleteSession()<br/>scheduleSessionDestroy(), Session.ts:194-236 completed --> [*]: deleteSession()<br/>scheduleSessionDestroy(), Session.ts:194-236
failed --> [*]: deleteSession()<br/>scheduleSessionDestroy() failed --> [*]: deleteSession()<br/>scheduleSessionDestroy()
@@ -55,7 +55,7 @@ stateDiagram-v2
| Reader | Location | Watches for | | Reader | Location | Watches for |
|---|---|---| |---|---|---|
| `waitForSession` | `Session.ts:175-192` | `failed` (breaks early) or the dummy SYSIN file disappearing (implies session survived init) | | `waitForSession` | `Session.ts:175-192` | `failed` (breaks early) or the dummy SYSIN file disappearing (implies session survived init) |
| `processProgram` (SAS branch poll loop) | `processProgram.ts:52-58` | `completed` (success exit) or `failed` (throws, carrying `session.failureReason`) | | `processProgram` (SAS branch poll loop) | `processProgram.ts:58-63` | `completed` or `failed` - either just stops the loop and returns normally; `failed` does **not** throw. `Execution.ts` reads `session.failureReason` afterward to fold the log into a normal (200) result, same as it already does for JS/PY/R. |
| `scheduleSessionDestroy` | `Session.ts:204-236` | `running` (extends death timer instead of destroying) | | `scheduleSessionDestroy` | `Session.ts:204-236` | `running` (extends death timer instead of destroying) |
## Asymmetry between runtimes ## Asymmetry between runtimes
@@ -65,6 +65,13 @@ stateDiagram-v2
`running`/`completed`/`failed` are all driven by that *same* process's `running`/`completed`/`failed` are all driven by that *same* process's
eventual exit. eventual exit.
- **JS/PY/R**: a session is cheap (folder + id, no process). The interpreter - **JS/PY/R**: a session is cheap (folder + id, no process). The interpreter
process is spawned fresh, per request, inside `processProgram.ts:115` and process is spawned fresh, per request, inside `processProgram.ts:124` and
its exit drives `completed`/`failed` directly in the same function - there its exit drives `completed`/`failed` directly in the same function - there
is no separate poll loop for these runtimes. is no separate poll loop for these runtimes (SAS needs one because it's
polling a state change happening in a process spawned earlier, at session
creation; JS/PY/R just `await` the process they spawn right there).
- **Both runtimes resolve `processProgram()` normally on `failed`, they
never throw for it** - a failed session (any runtime) is a normal outcome
of running arbitrary user code, not a request-shape/server problem. See
`request-execution-flow.md` for how `Execution.ts` turns that into a
response.
+97 -84
View File
@@ -40,7 +40,8 @@ components:
clientId: clientId:
type: string type: string
userId: userId:
type: string type: number
format: double
required: required:
- clientId - clientId
- userId - userId
@@ -314,8 +315,9 @@ components:
additionalProperties: false additionalProperties: false
UserResponse: UserResponse:
properties: properties:
uid: id:
type: string type: number
format: double
username: username:
type: string type: string
displayName: displayName:
@@ -323,7 +325,7 @@ components:
isAdmin: isAdmin:
type: boolean type: boolean
required: required:
- uid - id
- username - username
- displayName - displayName
- isAdmin - isAdmin
@@ -331,30 +333,32 @@ components:
additionalProperties: false additionalProperties: false
GroupResponse: GroupResponse:
properties: properties:
uid: groupId:
type: string type: number
format: double
name: name:
type: string type: string
description: description:
type: string type: string
required: required:
- uid - groupId
- name - name
- description - description
type: object type: object
additionalProperties: false additionalProperties: false
UserDetailsResponse: UserDetailsResponse:
properties: properties:
uid: id:
type: number
format: double
displayName:
type: string type: string
username: username:
type: string type: string
displayName:
type: string
isAdmin:
type: boolean
isActive: isActive:
type: boolean type: boolean
isAdmin:
type: boolean
autoExec: autoExec:
type: string type: string
groups: groups:
@@ -362,11 +366,11 @@ components:
$ref: '#/components/schemas/GroupResponse' $ref: '#/components/schemas/GroupResponse'
type: array type: array
required: required:
- uid - id
- username
- displayName - displayName
- isAdmin - username
- isActive - isActive
- isAdmin
type: object type: object
additionalProperties: false additionalProperties: false
UserPayload: UserPayload:
@@ -402,8 +406,9 @@ components:
additionalProperties: false additionalProperties: false
GroupDetailsResponse: GroupDetailsResponse:
properties: properties:
uid: groupId:
type: string type: number
format: double
name: name:
type: string type: string
description: description:
@@ -415,7 +420,7 @@ components:
$ref: '#/components/schemas/UserResponse' $ref: '#/components/schemas/UserResponse'
type: array type: array
required: required:
- uid - groupId
- name - name
- description - description
- isActive - isActive
@@ -484,8 +489,9 @@ components:
additionalProperties: false additionalProperties: false
PermissionDetailsResponse: PermissionDetailsResponse:
properties: properties:
uid: permissionId:
type: string type: number
format: double
path: path:
type: string type: string
type: type:
@@ -497,7 +503,7 @@ components:
group: group:
$ref: '#/components/schemas/GroupDetailsResponse' $ref: '#/components/schemas/GroupDetailsResponse'
required: required:
- uid - permissionId
- path - path
- type - type
- setting - setting
@@ -536,8 +542,10 @@ components:
description: 'Indicates the type of principal' description: 'Indicates the type of principal'
example: user example: user
principalId: principalId:
type: string type: number
format: double
description: 'The id of user or group to which a rule is assigned.' description: 'The id of user or group to which a rule is assigned.'
example: 123
required: required:
- path - path
- type - type
@@ -556,37 +564,25 @@ components:
- setting - setting
type: object type: object
additionalProperties: false 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: SessionResponse:
properties: properties:
id:
type: number
format: double
username: username:
type: string type: string
displayName: displayName:
type: string type: string
isAdmin: isAdmin:
type: boolean type: boolean
id:
type: string
needsToUpdatePassword: needsToUpdatePassword:
type: boolean type: boolean
required: required:
- id
- username - username
- displayName - displayName
- isAdmin - isAdmin
- id - needsToUpdatePassword
type: object type: object
additionalProperties: false additionalProperties: false
SessionState: SessionState:
@@ -1282,7 +1278,7 @@ paths:
type: array type: array
examples: examples:
'Example 1': '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.' summary: 'Get list of all users (username, displayname). All users can request this.'
tags: tags:
- User - User
@@ -1301,7 +1297,7 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse' $ref: '#/components/schemas/UserDetailsResponse'
examples: examples:
'Example 1': '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.' summary: 'Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.'
tags: tags:
- User - User
@@ -1352,7 +1348,7 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse' $ref: '#/components/schemas/UserDetailsResponse'
examples: examples:
'Example 1': '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.' summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
tags: tags:
- User - User
@@ -1403,7 +1399,7 @@ paths:
password: password:
type: string type: string
type: object type: object
'/SASjsApi/user/{uid}': '/SASjsApi/user/{userId}':
get: get:
operationId: GetUser operationId: GetUser
responses: responses:
@@ -1422,12 +1418,14 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'The user''s identifier'
in: path in: path
name: uid name: userId
required: true required: true
schema: schema:
type: string format: double
'/SASjsApi/user/{userId}': type: number
example: 1234
patch: patch:
operationId: UpdateUser operationId: UpdateUser
responses: responses:
@@ -1439,7 +1437,7 @@ paths:
$ref: '#/components/schemas/UserDetailsResponse' $ref: '#/components/schemas/UserDetailsResponse'
examples: examples:
'Example 1': '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.' summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
tags: tags:
- User - User
@@ -1453,7 +1451,8 @@ paths:
name: userId name: userId
required: true required: true
schema: schema:
type: string format: double
type: number
example: '1234' example: '1234'
requestBody: requestBody:
required: true required: true
@@ -1479,7 +1478,8 @@ paths:
name: userId name: userId
required: true required: true
schema: schema:
type: string format: double
type: number
example: 1234 example: 1234
requestBody: requestBody:
required: true required: true
@@ -1504,7 +1504,7 @@ paths:
type: array type: array
examples: examples:
'Example 1': '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.' summary: 'Get list of all groups (groupName and groupDescription). All users can request this.'
tags: tags:
- Group - Group
@@ -1523,7 +1523,7 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse' $ref: '#/components/schemas/GroupDetailsResponse'
examples: examples:
'Example 1': '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.' summary: 'Create a new group. Admin only.'
tags: tags:
- Group - Group
@@ -1539,7 +1539,7 @@ paths:
$ref: '#/components/schemas/GroupPayload' $ref: '#/components/schemas/GroupPayload'
'/SASjsApi/group/by/groupname/{name}': '/SASjsApi/group/by/groupname/{name}':
get: get:
operationId: GetGroupByName operationId: GetGroupByGroupName
responses: responses:
'200': '200':
description: Ok description: Ok
@@ -1561,7 +1561,7 @@ paths:
required: true required: true
schema: schema:
type: string type: string
'/SASjsApi/group/{uid}': '/SASjsApi/group/{groupId}':
get: get:
operationId: GetGroup operationId: GetGroup
responses: responses:
@@ -1581,11 +1581,12 @@ paths:
- -
description: 'The group''s identifier' description: 'The group''s identifier'
in: path in: path
name: uid name: groupId
required: true required: true
schema: schema:
type: string format: double
example: 12ByteString type: number
example: 1234
delete: delete:
operationId: DeleteGroup operationId: DeleteGroup
responses: responses:
@@ -1607,12 +1608,13 @@ paths:
- -
description: 'The group''s identifier' description: 'The group''s identifier'
in: path in: path
name: uid name: groupId
required: true required: true
schema: schema:
type: string format: double
example: 12ByteString type: number
'/SASjsApi/group/{groupUid}/{userUid}': example: 1234
'/SASjsApi/group/{groupId}/{userId}':
post: post:
operationId: AddUserToGroup operationId: AddUserToGroup
responses: responses:
@@ -1624,7 +1626,7 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse' $ref: '#/components/schemas/GroupDetailsResponse'
examples: examples:
'Example 1': '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.' summary: 'Add a user to a group. Admin task only.'
tags: tags:
- Group - Group
@@ -1635,18 +1637,21 @@ paths:
- -
description: 'The group''s identifier' description: 'The group''s identifier'
in: path in: path
name: groupUid name: groupId
required: true required: true
schema: schema:
type: string format: double
example: 12ByteString type: number
example: '1234'
- -
description: 'The user''s identifier' description: 'The user''s identifier'
in: path in: path
name: userUid name: userId
required: true required: true
schema: schema:
type: string format: double
type: number
example: '6789'
delete: delete:
operationId: RemoveUserFromGroup operationId: RemoveUserFromGroup
responses: responses:
@@ -1658,8 +1663,8 @@ paths:
$ref: '#/components/schemas/GroupDetailsResponse' $ref: '#/components/schemas/GroupDetailsResponse'
examples: examples:
'Example 1': '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: 'Remove a user from a group. Admin task only.' summary: 'Remove a user to a group. Admin task only.'
tags: tags:
- Group - Group
security: security:
@@ -1669,19 +1674,21 @@ paths:
- -
description: 'The group''s identifier' description: 'The group''s identifier'
in: path in: path
name: groupUid name: groupId
required: true required: true
schema: schema:
type: string format: double
example: 12ByteString type: number
example: '1234'
- -
description: 'The user''s identifier' description: 'The user''s identifier'
in: path in: path
name: userUid name: userId
required: true required: true
schema: schema:
type: string format: double
example: 12ByteString type: number
example: '6789'
/SASjsApi/info: /SASjsApi/info:
get: get:
operationId: Info operationId: Info
@@ -1732,7 +1739,7 @@ paths:
type: array type: array
examples: examples:
'Example 1': '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." 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.' summary: 'Get the list of permission rules. If the user is admin, all rules are returned.'
tags: tags:
@@ -1752,7 +1759,7 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse' $ref: '#/components/schemas/PermissionDetailsResponse'
examples: examples:
'Example 1': '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.' summary: 'Create a new permission. Admin only.'
tags: tags:
- Permission - Permission
@@ -1766,7 +1773,7 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/RegisterPermissionPayload' $ref: '#/components/schemas/RegisterPermissionPayload'
'/SASjsApi/permission/{uid}': '/SASjsApi/permission/{permissionId}':
patch: patch:
operationId: UpdatePermission operationId: UpdatePermission
responses: responses:
@@ -1778,7 +1785,7 @@ paths:
$ref: '#/components/schemas/PermissionDetailsResponse' $ref: '#/components/schemas/PermissionDetailsResponse'
examples: examples:
'Example 1': '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' summary: 'Update permission setting. Admin only'
tags: tags:
- Permission - Permission
@@ -1787,11 +1794,14 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'The permission''s identifier'
in: path in: path
name: uid name: permissionId
required: true required: true
schema: schema:
type: string format: double
type: number
example: 1234
requestBody: requestBody:
required: true required: true
content: content:
@@ -1811,11 +1821,14 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'The user''s identifier'
in: path in: path
name: uid name: permissionId
required: true required: true
schema: schema:
type: string format: double
type: number
example: 1234
/SASjsApi/session: /SASjsApi/session:
get: get:
operationId: Session operationId: Session
@@ -1828,7 +1841,7 @@ paths:
$ref: '#/components/schemas/SessionResponse' $ref: '#/components/schemas/SessionResponse'
examples: examples:
'Example 1': '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).' summary: 'Get session info (username).'
tags: tags:
- Session - Session
@@ -1999,7 +2012,7 @@ paths:
application/json: application/json:
schema: schema:
properties: 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: {needsToUpdatePassword: {type: boolean}, isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [needsToUpdatePassword, isAdmin, displayName, username, id], type: object}
loggedIn: {type: boolean} loggedIn: {type: boolean}
required: required:
- user - user
+3 -3
View File
@@ -27,14 +27,14 @@ import User from '../model/User'
@Tags('Auth') @Tags('Auth')
export class AuthController { export class AuthController {
static authCodes: { [key: string]: { [key: string]: string } } = {} 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]) if (AuthController.authCodes[userId])
return (AuthController.authCodes[userId][clientId] = code) return (AuthController.authCodes[userId][clientId] = code)
AuthController.authCodes[userId] = { [clientId]: code } AuthController.authCodes[userId] = { [clientId]: code }
return AuthController.authCodes[userId][clientId] return AuthController.authCodes[userId][clientId]
} }
static deleteCode = (userId: string, clientId: string) => static deleteCode = (userId: number, clientId: string) =>
delete AuthController.authCodes[userId][clientId] delete AuthController.authCodes[userId][clientId]
/** /**
@@ -159,7 +159,7 @@ const updatePassword = async (
) => { ) => {
const { currentPassword, newPassword } = data const { currentPassword, newPassword } = data
const userId = req.user?.userId const userId = req.user?.userId
const dbUser = await User.findOne({ _id: userId }) const dbUser = await User.findOne({ id: userId })
if (!dbUser) if (!dbUser)
throw { throw {
+1 -2
View File
@@ -114,8 +114,7 @@ const executeCode = async (
code: 400, code: 400,
status: 'failure', status: 'failure',
message: 'Job execution failed.', message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err, error: typeof err === 'object' ? err.toString() : err
log: err?.log
} }
} }
} }
+62 -60
View File
@@ -12,29 +12,28 @@ import {
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group' import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
import User from '../model/User' import User from '../model/User'
import { GetUserBy, UserResponse } from './user' import { AuthProviderType } from '../utils'
import { UserResponse } from './user'
export interface GroupResponse { export interface GroupResponse {
uid: string groupId: number
name: string name: string
description: string description: string
} }
export interface GroupDetailsResponse extends GroupResponse { export interface GroupDetailsResponse {
groupId: number
name: string
description: string
isActive: boolean isActive: boolean
users: UserResponse[] users: UserResponse[]
} }
interface GetGroupBy { interface GetGroupBy {
_id?: string groupId?: number
name?: string name?: string
} }
enum GroupAction {
AddUser = 'addUser',
RemoveUser = 'removeUser'
}
@Security('bearerAuth') @Security('bearerAuth')
@Route('SASjsApi/group') @Route('SASjsApi/group')
@Tags('Group') @Tags('Group')
@@ -45,7 +44,7 @@ export class GroupController {
*/ */
@Example<GroupResponse[]>([ @Example<GroupResponse[]>([
{ {
uid: 'groupIdString', groupId: 123,
name: 'DCGroup', name: 'DCGroup',
description: 'This group represents Data Controller Users' description: 'This group represents Data Controller Users'
} }
@@ -60,7 +59,7 @@ export class GroupController {
* *
*/ */
@Example<GroupDetailsResponse>({ @Example<GroupDetailsResponse>({
uid: 'groupIdString', groupId: 123,
name: 'DCGroup', name: 'DCGroup',
description: 'This group represents Data Controller Users', description: 'This group represents Data Controller Users',
isActive: true, isActive: true,
@@ -79,7 +78,7 @@ export class GroupController {
* @example dcgroup * @example dcgroup
*/ */
@Get('by/groupname/{name}') @Get('by/groupname/{name}')
public async getGroupByName( public async getGroupByGroupName(
@Path() name: string @Path() name: string
): Promise<GroupDetailsResponse> { ): Promise<GroupDetailsResponse> {
return getGroup({ name }) return getGroup({ name })
@@ -87,66 +86,68 @@ export class GroupController {
/** /**
* @summary Get list of members of a group (userName). All users can request this. * @summary Get list of members of a group (userName). All users can request this.
* @param uid The group's identifier * @param groupId The group's identifier
* @example uid "12ByteString" * @example groupId 1234
*/ */
@Get('{uid}') @Get('{groupId}')
public async getGroup(@Path() uid: string): Promise<GroupDetailsResponse> { public async getGroup(
return getGroup({ _id: uid }) @Path() groupId: number
): Promise<GroupDetailsResponse> {
return getGroup({ groupId })
} }
/** /**
* @summary Add a user to a group. Admin task only. * @summary Add a user to a group. Admin task only.
* @param groupUid The group's identifier * @param groupId The group's identifier
* @example groupUid "12ByteString" * @example groupId "1234"
* @param userUid The user's identifier * @param userId The user's identifier
* @example userId "12ByteString" * @example userId "6789"
*/ */
@Example<GroupDetailsResponse>({ @Example<GroupDetailsResponse>({
uid: 'groupIdString', groupId: 123,
name: 'DCGroup', name: 'DCGroup',
description: 'This group represents Data Controller Users', description: 'This group represents Data Controller Users',
isActive: true, isActive: true,
users: [] users: []
}) })
@Post('{groupUid}/{userUid}') @Post('{groupId}/{userId}')
public async addUserToGroup( public async addUserToGroup(
@Path() groupUid: string, @Path() groupId: number,
@Path() userUid: string @Path() userId: number
): Promise<GroupDetailsResponse> { ): Promise<GroupDetailsResponse> {
return addUserToGroup(groupUid, userUid) return addUserToGroup(groupId, userId)
} }
/** /**
* @summary Remove a user from a group. Admin task only. * @summary Remove a user to a group. Admin task only.
* @param groupUid The group's identifier * @param groupId The group's identifier
* @example groupUid "12ByteString" * @example groupId "1234"
* @param userUid The user's identifier * @param userId The user's identifier
* @example userUid "12ByteString" * @example userId "6789"
*/ */
@Example<GroupDetailsResponse>({ @Example<GroupDetailsResponse>({
uid: 'groupIdString', groupId: 123,
name: 'DCGroup', name: 'DCGroup',
description: 'This group represents Data Controller Users', description: 'This group represents Data Controller Users',
isActive: true, isActive: true,
users: [] users: []
}) })
@Delete('{groupUid}/{userUid}') @Delete('{groupId}/{userId}')
public async removeUserFromGroup( public async removeUserFromGroup(
@Path() groupUid: string, @Path() groupId: number,
@Path() userUid: string @Path() userId: number
): Promise<GroupDetailsResponse> { ): Promise<GroupDetailsResponse> {
return removeUserFromGroup(groupUid, userUid) return removeUserFromGroup(groupId, userId)
} }
/** /**
* @summary Delete a group. Admin task only. * @summary Delete a group. Admin task only.
* @param uid The group's identifier * @param groupId The group's identifier
* @example uid "12ByteString" * @example groupId 1234
*/ */
@Delete('{uid}') @Delete('{groupId}')
public async deleteGroup(@Path() uid: string) { public async deleteGroup(@Path() groupId: number) {
const group = await Group.findOne({ _id: uid }) const group = await Group.findOne({ groupId })
if (!group) if (!group)
throw { throw {
code: 404, code: 404,
@@ -159,7 +160,9 @@ export class GroupController {
} }
const getAllGroups = async (): Promise<GroupResponse[]> => 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 ({ const createGroup = async ({
name, name,
@@ -184,7 +187,7 @@ const createGroup = async ({
const savedGroup = await group.save() const savedGroup = await group.save()
return { return {
uid: savedGroup.uid, groupId: savedGroup.groupId,
name: savedGroup.name, name: savedGroup.name,
description: savedGroup.description, description: savedGroup.description,
isActive: savedGroup.isActive, isActive: savedGroup.isActive,
@@ -195,12 +198,11 @@ const createGroup = async ({
const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => { const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
const group = (await Group.findOne( const group = (await Group.findOne(
findBy, findBy,
'uid name description isActive users' 'groupId name description isActive users -_id'
).populate( ).populate(
'users', 'users',
'uid username displayName isAdmin' 'id username displayName isAdmin -_id'
)) as unknown as GroupDetailsResponse )) as unknown as GroupDetailsResponse
if (!group) if (!group)
throw { throw {
code: 404, code: 404,
@@ -209,7 +211,7 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
} }
return { return {
uid: group.uid, groupId: group.groupId,
name: group.name, name: group.name,
description: group.description, description: group.description,
isActive: group.isActive, isActive: group.isActive,
@@ -218,23 +220,23 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
} }
const addUserToGroup = async ( const addUserToGroup = async (
groupUid: string, groupId: number,
userUid: string userId: number
): Promise<GroupDetailsResponse> => ): Promise<GroupDetailsResponse> =>
updateUsersListInGroup(groupUid, userUid, GroupAction.AddUser) updateUsersListInGroup(groupId, userId, 'addUser')
const removeUserFromGroup = async ( const removeUserFromGroup = async (
groupUid: string, groupId: number,
userUid: string userId: number
): Promise<GroupDetailsResponse> => ): Promise<GroupDetailsResponse> =>
updateUsersListInGroup(groupUid, userUid, GroupAction.RemoveUser) updateUsersListInGroup(groupId, userId, 'removeUser')
const updateUsersListInGroup = async ( const updateUsersListInGroup = async (
groupUid: string, groupId: number,
userUid: string, userId: number,
action: GroupAction action: 'addUser' | 'removeUser'
): Promise<GroupDetailsResponse> => { ): Promise<GroupDetailsResponse> => {
const group = await Group.findOne({ _id: groupUid }) const group = await Group.findOne({ groupId })
if (!group) if (!group)
throw { throw {
code: 404, code: 404,
@@ -256,7 +258,7 @@ const updateUsersListInGroup = async (
message: `Can't add/remove user to group created by external auth provider.` 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) if (!user)
throw { throw {
code: 404, code: 404,
@@ -272,7 +274,7 @@ const updateUsersListInGroup = async (
} }
const updatedGroup = const updatedGroup =
action === GroupAction.AddUser action === 'addUser'
? await group.addUser(user) ? await group.addUser(user)
: await group.removeUser(user) : await group.removeUser(user)
@@ -284,7 +286,7 @@ const updateUsersListInGroup = async (
} }
return { return {
uid: updatedGroup.uid, groupId: updatedGroup.groupId,
name: updatedGroup.name, name: updatedGroup.name,
description: updatedGroup.description, description: updatedGroup.description,
isActive: updatedGroup.isActive, isActive: updatedGroup.isActive,
+5 -24
View File
@@ -15,24 +15,6 @@ export interface ExecutionVars {
[key: string]: string | number | undefined [key: string]: string | number | undefined
} }
// Thrown when the session itself fails (e.g. SAS exits abnormally via
// %abort;). Carries the complete log - by the time this is thrown, the
// session's process has already exited, so the log file it wrote is final,
// not a partial/truncated snapshot.
export class SessionExecutionError extends Error {
constructor(
message: string,
public log?: string
) {
super(message)
// required for `instanceof` to work when compiling to ES5, since the
// default __extends helper does not preserve the prototype chain for
// classes extending built-ins like Error
Object.setPrototypeOf(this, SessionExecutionError.prototype)
}
}
export interface ExecuteReturnRaw { export interface ExecuteReturnRaw {
httpHeaders: HTTPHeaders httpHeaders: HTTPHeaders
result: string | Buffer result: string | Buffer
@@ -106,7 +88,11 @@ export class ExecutionController {
preProgramVariables?.httpHeaders.join('\n') ?? '' preProgramVariables?.httpHeaders.join('\n') ?? ''
) )
try { // A failed session (e.g. SAS via %abort;, or a non-zero JS/PY/R exit)
// is a normal outcome of running arbitrary user code, not a
// request-shape/server problem - processProgram resolves rather than
// throwing in that case, and the session.failureReason check below
// folds the log into the same result shape a successful run returns.
await processProgram( await processProgram(
program, program,
preProgramVariables, preProgramVariables,
@@ -119,11 +105,6 @@ export class ExecutionController {
logPath, logPath,
otherArgs otherArgs
) )
} catch (err: any) {
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
throw new SessionExecutionError(err.message, log)
}
const log = (await fileExists(logPath)) ? await readFile(logPath) : '' const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
const headersContent = (await fileExists(headersPath)) const headersContent = (await fileExists(headersPath))
+11 -6
View File
@@ -48,12 +48,17 @@ export const processProgram = async (
await createFile(codePath + '.bkp', program) await createFile(codePath + '.bkp', program)
await moveFile(codePath + '.bkp', codePath) await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status // we now need to poll the session status. A failed session (e.g. from
while (session.state !== SessionState.completed) { // %abort;) is not a request-shape/server problem - it's a normal
if (session.state === SessionState.failed) { // outcome of running arbitrary user code, same as a SAS ERROR: in the
throw new Error(session.failureReason || 'SAS session failed') // log without %abort;. So we just stop polling rather than throwing;
} // Execution.ts already knows how to turn session.failureReason into a
// 200 response with the log embedded, matching how JS/PY/R (the else
// branch below) has always handled a failed session.
while (
session.state !== SessionState.completed &&
session.state !== SessionState.failed
) {
await delay(50) await delay(50)
} }
} else { } else {
@@ -2,13 +2,13 @@ import path from 'path'
import os from 'os' import os from 'os'
import { createFile, deleteFolder, generateTimestamp } from '@sasjs/utils' import { createFile, deleteFolder, generateTimestamp } from '@sasjs/utils'
import * as ProcessProgramModule from '../processProgram' import * as ProcessProgramModule from '../processProgram'
import { ExecutionController, SessionExecutionError } from '../Execution' import { ExecutionController } from '../Execution'
import { Session, SessionState, PreProgramVars } from '../../../types' import { Session, SessionState, PreProgramVars } from '../../../types'
import { RunTimeType } from '../../../utils' import { RunTimeType } from '../../../utils'
const preProgramVariables: PreProgramVars = { const preProgramVariables: PreProgramVars = {
username: 'testUser', username: 'testUser',
userId: '1', userId: 1,
displayName: 'Test User', displayName: 'Test User',
serverUrl: 'http://localhost:5000', serverUrl: 'http://localhost:5000',
httpHeaders: [] httpHeaders: []
@@ -83,8 +83,15 @@ describe('ExecutionController.executeProgram', () => {
}) })
}) })
// A failed SAS session (e.g. from %abort;) is a normal outcome of
// running arbitrary user code, not a request-shape/server problem - the
// same way a plain SAS ERROR: in the log (without %abort;) already
// returns 200 with the log embedded. This mirrors the JS/PY/R failure
// path above: resolve normally, don't throw, and let the existing
// session.failureReason check below produce the same result shape as a
// successful run.
describe('SAS failure path', () => { describe('SAS failure path', () => {
it('throws a SessionExecutionError carrying the complete log when the session fails', async () => { it('returns the complete log embedded in a normal result instead of throwing, when the session fails', async () => {
const logPath = path.join(session.path, 'log.log') const logPath = path.join(session.path, 'log.log')
const logContent = const logContent =
'NOTE: SAS session\nERROR: SAS session terminated. See log for details.\n' 'NOTE: SAS session\nERROR: SAS session terminated. See log for details.\n'
@@ -94,12 +101,17 @@ describe('ExecutionController.executeProgram', () => {
jest jest
.spyOn(ProcessProgramModule, 'processProgram') .spyOn(ProcessProgramModule, 'processProgram')
.mockImplementation(async () => { .mockImplementation(async () => {
throw new Error('ERROR: SAS session terminated. See log for details.') // mirrors the real SAS branch after the fix: sets
// state/failureReason and resolves, exactly like the JS/PY/R
// branch already does - it does not throw
session.state = SessionState.failed
session.failureReason =
'ERROR: SAS session terminated. See log for details.'
}) })
const controller = new ExecutionController() const controller = new ExecutionController()
const resultPromise = controller.executeProgram({ const { result } = await controller.executeProgram({
program: '%abort;', program: '%abort;',
preProgramVariables, preProgramVariables,
vars: {}, vars: {},
@@ -107,11 +119,8 @@ describe('ExecutionController.executeProgram', () => {
runTime: RunTimeType.SAS runTime: RunTimeType.SAS
}) })
await expect(resultPromise).rejects.toBeInstanceOf(SessionExecutionError) expect(session.state).toBe(SessionState.failed)
await expect(resultPromise).rejects.toMatchObject({ expect(result).toEqual(expect.stringContaining(logContent))
log: logContent,
message: expect.stringContaining('SAS session terminated')
})
}) })
}) })
}) })
@@ -11,7 +11,7 @@ import {
const preProgramVariables: PreProgramVars = { const preProgramVariables: PreProgramVars = {
username: 'testUser', username: 'testUser',
userId: '1', userId: 1,
displayName: 'Test User', displayName: 'Test User',
serverUrl: 'http://localhost:5000', serverUrl: 'http://localhost:5000',
httpHeaders: [] httpHeaders: []
@@ -65,7 +65,13 @@ describe('processProgram (SAS runtime)', () => {
await deleteFolder(session.path) await deleteFolder(session.path)
}) })
it('rejects instead of hanging when the session fails (e.g. %abort;)', async () => { it('resolves instead of hanging when the session fails (e.g. %abort;)', async () => {
// mirrors the JS/PY/R branch below: a session failure is a normal
// outcome of running arbitrary user code (like a SAS ERROR: in the
// log without %abort;), not a server-side/request-shape problem - so
// processProgram must not throw here, just stop polling. Execution.ts
// is responsible for turning session.failureReason into a 200 response
// with the log embedded, the same way it already does for JS/PY/R.
setTimeout(() => { setTimeout(() => {
session.state = SessionState.failed session.state = SessionState.failed
session.failureReason = session.failureReason =
@@ -84,7 +90,7 @@ describe('processProgram (SAS runtime)', () => {
RunTimeType.SAS, RunTimeType.SAS,
logPath logPath
) )
).rejects.toThrow(/SAS session terminated/) ).resolves.toBeUndefined()
}, 3000) }, 3000)
it('resolves without throwing when the session completes normally', async () => { it('resolves without throwing when the session completes normally', async () => {
+40 -34
View File
@@ -56,9 +56,9 @@ interface RegisterPermissionPayload {
principalType: PrincipalType principalType: PrincipalType
/** /**
* The id of user or group to which a rule is assigned. * The id of user or group to which a rule is assigned.
* @example 'groupIdString' * @example 123
*/ */
principalId: string principalId: number
} }
interface UpdatePermissionPayload { interface UpdatePermissionPayload {
@@ -70,7 +70,7 @@ interface UpdatePermissionPayload {
} }
export interface PermissionDetailsResponse { export interface PermissionDetailsResponse {
uid: string permissionId: number
path: string path: string
type: string type: string
setting: string setting: string
@@ -91,24 +91,24 @@ export class PermissionController {
*/ */
@Example<PermissionDetailsResponse[]>([ @Example<PermissionDetailsResponse[]>([
{ {
uid: 'permissionId1String', permissionId: 123,
path: '/SASjsApi/code/execute', path: '/SASjsApi/code/execute',
type: 'Route', type: 'Route',
setting: 'Grant', setting: 'Grant',
user: { user: {
uid: 'user1-id', id: 1,
username: 'johnSnow01', username: 'johnSnow01',
displayName: 'John Snow', displayName: 'John Snow',
isAdmin: false isAdmin: false
} }
}, },
{ {
uid: 'permissionId2String', permissionId: 124,
path: '/SASjsApi/code/execute', path: '/SASjsApi/code/execute',
type: 'Route', type: 'Route',
setting: 'Grant', setting: 'Grant',
group: { group: {
uid: 'group1-id', groupId: 1,
name: 'DCGroup', name: 'DCGroup',
description: 'This group represents Data Controller Users', description: 'This group represents Data Controller Users',
isActive: true, isActive: true,
@@ -128,12 +128,12 @@ export class PermissionController {
* *
*/ */
@Example<PermissionDetailsResponse>({ @Example<PermissionDetailsResponse>({
uid: 'permissionIdString', permissionId: 123,
path: '/SASjsApi/code/execute', path: '/SASjsApi/code/execute',
type: 'Route', type: 'Route',
setting: 'Grant', setting: 'Grant',
user: { user: {
uid: 'userIdString', id: 1,
username: 'johnSnow01', username: 'johnSnow01',
displayName: 'John Snow', displayName: 'John Snow',
isAdmin: false isAdmin: false
@@ -149,36 +149,36 @@ export class PermissionController {
/** /**
* @summary Update permission setting. Admin only * @summary Update permission setting. Admin only
* @param permissionId The permission's identifier * @param permissionId The permission's identifier
* @example permissionId "permissionIdString" * @example permissionId 1234
*/ */
@Example<PermissionDetailsResponse>({ @Example<PermissionDetailsResponse>({
uid: 'permissionIdString', permissionId: 123,
path: '/SASjsApi/code/execute', path: '/SASjsApi/code/execute',
type: 'Route', type: 'Route',
setting: 'Grant', setting: 'Grant',
user: { user: {
uid: 'userIdString', id: 1,
username: 'johnSnow01', username: 'johnSnow01',
displayName: 'John Snow', displayName: 'John Snow',
isAdmin: false isAdmin: false
} }
}) })
@Patch('{uid}') @Patch('{permissionId}')
public async updatePermission( public async updatePermission(
@Path() uid: string, @Path() permissionId: number,
@Body() body: UpdatePermissionPayload @Body() body: UpdatePermissionPayload
): Promise<PermissionDetailsResponse> { ): Promise<PermissionDetailsResponse> {
return updatePermission(uid, body) return updatePermission(permissionId, body)
} }
/** /**
* @summary Delete a permission. Admin only. * @summary Delete a permission. Admin only.
* @param permissionId The user's identifier * @param permissionId The user's identifier
* @example permissionId "permissionIdString" * @example permissionId 1234
*/ */
@Delete('{uid}') @Delete('{permissionId}')
public async deletePermission(@Path() uid: string) { public async deletePermission(@Path() permissionId: number) {
return deletePermission(uid) return deletePermission(permissionId)
} }
} }
@@ -191,7 +191,7 @@ const getAllPermissions = async (
else { else {
const permissions: PermissionDetailsResponse[] = [] const permissions: PermissionDetailsResponse[] = []
const dbUser = await User.findOne({ _id: user?.userId }) const dbUser = await User.findOne({ id: user?.userId })
if (!dbUser) if (!dbUser)
throw { throw {
code: 404, code: 404,
@@ -227,7 +227,7 @@ const createPermission = async ({
switch (principalType) { switch (principalType) {
case PrincipalType.user: { case PrincipalType.user: {
const userInDB = await User.findOne({ _id: principalId }) const userInDB = await User.findOne({ id: principalId })
if (!userInDB) if (!userInDB)
throw { throw {
code: 404, code: 404,
@@ -259,7 +259,7 @@ const createPermission = async ({
permission.user = userInDB._id permission.user = userInDB._id
user = { user = {
uid: userInDB.uid, id: userInDB.id,
username: userInDB.username, username: userInDB.username,
displayName: userInDB.displayName, displayName: userInDB.displayName,
isAdmin: userInDB.isAdmin isAdmin: userInDB.isAdmin
@@ -267,7 +267,7 @@ const createPermission = async ({
break break
} }
case PrincipalType.group: { case PrincipalType.group: {
const groupInDB = await Group.findOne({ _id: principalId }) const groupInDB = await Group.findOne({ groupId: principalId })
if (!groupInDB) if (!groupInDB)
throw { throw {
code: 404, code: 404,
@@ -291,13 +291,13 @@ const createPermission = async ({
permission.group = groupInDB._id permission.group = groupInDB._id
group = { group = {
uid: groupInDB.uid, groupId: groupInDB.groupId,
name: groupInDB.name, name: groupInDB.name,
description: groupInDB.description, description: groupInDB.description,
isActive: groupInDB.isActive, isActive: groupInDB.isActive,
users: groupInDB.populate({ users: groupInDB.populate({
path: 'users', path: 'users',
select: 'uid username displayName isAdmin -_id', select: 'id username displayName isAdmin -_id',
options: { limit: 15 } options: { limit: 15 }
}) as unknown as UserResponse[] }) as unknown as UserResponse[]
} }
@@ -314,7 +314,7 @@ const createPermission = async ({
const savedPermission = await permission.save() const savedPermission = await permission.save()
return { return {
uid: savedPermission.uid, permissionId: savedPermission.permissionId,
path: savedPermission.path, path: savedPermission.path,
type: savedPermission.type, type: savedPermission.type,
setting: savedPermission.setting, setting: savedPermission.setting,
@@ -324,21 +324,27 @@ const createPermission = async ({
} }
const updatePermission = async ( const updatePermission = async (
uid: string, id: number,
data: UpdatePermissionPayload data: UpdatePermissionPayload
): Promise<PermissionDetailsResponse> => { ): Promise<PermissionDetailsResponse> => {
const { setting } = data const { setting } = data
const updatedPermission = (await Permission.findOneAndUpdate( const updatedPermission = (await Permission.findOneAndUpdate(
{ _id: uid }, { permissionId: id },
{ setting }, { setting },
{ new: true } { new: true }
) )
.select('uid path type setting') .select({
.populate({ path: 'user', select: 'uid username displayName isAdmin' }) _id: 0,
permissionId: 1,
path: 1,
type: 1,
setting: 1
})
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
.populate({ .populate({
path: 'group', path: 'group',
select: 'groupId name description' select: 'groupId name description -_id'
})) as unknown as PermissionDetailsResponse })) as unknown as PermissionDetailsResponse
if (!updatedPermission) if (!updatedPermission)
throw { throw {
@@ -350,13 +356,13 @@ const updatePermission = async (
return updatedPermission return updatedPermission
} }
const deletePermission = async (uid: string) => { const deletePermission = async (id: number) => {
const permission = await Permission.findOne({ _id: uid }) const permission = await Permission.findOne({ permissionId: id })
if (!permission) if (!permission)
throw { throw {
code: 404, code: 404,
status: 'Not Found', status: 'Not Found',
message: 'Permission not found.' message: 'Permission not found.'
} }
await Permission.deleteOne({ _id: uid }) await Permission.deleteOne({ permissionId: id })
} }
+5 -6
View File
@@ -5,7 +5,7 @@ import { getSessionController } from './internal'
import { SessionState } from '../types' import { SessionState } from '../types'
interface SessionResponse extends UserResponse { interface SessionResponse extends UserResponse {
needsToUpdatePassword?: boolean needsToUpdatePassword: boolean
} }
@Security('bearerAuth') @Security('bearerAuth')
@@ -16,12 +16,11 @@ export class SessionController {
* @summary Get session info (username). * @summary Get session info (username).
* *
*/ */
@Example<SessionResponse>({ @Example<UserResponse>({
uid: 'userIdString', id: 123,
username: 'johnusername', username: 'johnusername',
displayName: 'John', displayName: 'John',
isAdmin: false, isAdmin: false
needsToUpdatePassword: false
}) })
@Get('/') @Get('/')
public async session( public async session(
@@ -44,7 +43,7 @@ export class SessionController {
} }
const session = (req: express.Request) => ({ const session = (req: express.Request) => ({
uid: req.user!.userId, id: req.user!.userId,
username: req.user!.username, username: req.user!.username,
displayName: req.user!.displayName, displayName: req.user!.displayName,
isAdmin: req.user!.isAdmin, isAdmin: req.user!.isAdmin,
+1 -2
View File
@@ -166,8 +166,7 @@ const execute = async (
code: 400, code: 400,
status: 'failure', status: 'failure',
message: 'Job execution failed.', message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err, error: typeof err === 'object' ? err.toString() : err
log: err?.log
} }
} }
} }
+37 -31
View File
@@ -26,14 +26,18 @@ import {
import { GroupController, GroupResponse } from './group' import { GroupController, GroupResponse } from './group'
export interface UserResponse { export interface UserResponse {
uid: string id: number
username: string username: string
displayName: string displayName: string
isAdmin: boolean isAdmin: boolean
} }
export interface UserDetailsResponse extends UserResponse { export interface UserDetailsResponse {
id: number
displayName: string
username: string
isActive: boolean isActive: boolean
isAdmin: boolean
autoExec?: string autoExec?: string
groups?: GroupResponse[] groups?: GroupResponse[]
} }
@@ -48,13 +52,13 @@ export class UserController {
*/ */
@Example<UserResponse[]>([ @Example<UserResponse[]>([
{ {
uid: 'userIdString', id: 123,
username: 'johnusername', username: 'johnusername',
displayName: 'John', displayName: 'John',
isAdmin: false isAdmin: false
}, },
{ {
uid: 'anotherUserIdString', id: 456,
username: 'starkusername', username: 'starkusername',
displayName: 'Stark', displayName: 'Stark',
isAdmin: true isAdmin: true
@@ -70,7 +74,7 @@ export class UserController {
* *
*/ */
@Example<UserDetailsResponse>({ @Example<UserDetailsResponse>({
uid: 'userIdString', id: 1234,
displayName: 'John Snow', displayName: 'John Snow',
username: 'johnSnow01', username: 'johnSnow01',
isAdmin: false, isAdmin: false,
@@ -107,20 +111,20 @@ export class UserController {
* Only Admin or user itself will get user autoExec code. * Only Admin or user itself will get user autoExec code.
* @summary Get user properties - such as group memberships, userName, displayName. * @summary Get user properties - such as group memberships, userName, displayName.
* @param userId The user's identifier * @param userId The user's identifier
* @example userId "userIdString" * @example userId 1234
*/ */
@Get('{uid}') @Get('{userId}')
public async getUser( public async getUser(
@Request() req: express.Request, @Request() req: express.Request,
@Path() uid: string @Path() userId: number
): Promise<UserDetailsResponse> { ): Promise<UserDetailsResponse> {
const { MODE } = process.env const { MODE } = process.env
if (MODE === ModeType.Desktop) return getDesktopAutoExec() if (MODE === ModeType.Desktop) return getDesktopAutoExec()
const { user } = req const { user } = req
const getAutoExec = user!.isAdmin || user!.userId === uid const getAutoExec = user!.isAdmin || user!.userId == userId
return getUser({ _id: uid }, getAutoExec) return getUser({ id: userId }, getAutoExec)
} }
/** /**
@@ -129,7 +133,7 @@ export class UserController {
* @example username "johnSnow01" * @example username "johnSnow01"
*/ */
@Example<UserDetailsResponse>({ @Example<UserDetailsResponse>({
uid: 'userIdString', id: 1234,
displayName: 'John Snow', displayName: 'John Snow',
username: 'johnSnow01', username: 'johnSnow01',
isAdmin: false, isAdmin: false,
@@ -154,7 +158,7 @@ export class UserController {
* @example userId "1234" * @example userId "1234"
*/ */
@Example<UserDetailsResponse>({ @Example<UserDetailsResponse>({
uid: 'userIdString', id: 1234,
displayName: 'John Snow', displayName: 'John Snow',
username: 'johnSnow01', username: 'johnSnow01',
isAdmin: false, isAdmin: false,
@@ -162,7 +166,7 @@ export class UserController {
}) })
@Patch('{userId}') @Patch('{userId}')
public async updateUser( public async updateUser(
@Path() userId: string, @Path() userId: number,
@Body() body: UserPayload @Body() body: UserPayload
): Promise<UserDetailsResponse> { ): Promise<UserDetailsResponse> {
const { MODE } = process.env const { MODE } = process.env
@@ -170,7 +174,7 @@ export class UserController {
if (MODE === ModeType.Desktop) if (MODE === ModeType.Desktop)
return updateDesktopAutoExec(body.autoExec ?? '') return updateDesktopAutoExec(body.autoExec ?? '')
return updateUser({ _id: userId }, body) return updateUser({ id: userId }, body)
} }
/** /**
@@ -194,16 +198,18 @@ export class UserController {
*/ */
@Delete('{userId}') @Delete('{userId}')
public async deleteUser( public async deleteUser(
@Path() userId: string, @Path() userId: number,
@Body() body: { password?: string }, @Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false @Query() @Hidden() isAdmin: boolean = false
) { ) {
return deleteUser({ _id: userId }, isAdmin, body) return deleteUser({ id: userId }, isAdmin, body)
} }
} }
const getAllUsers = async (): Promise<UserResponse[]> => 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 createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive, autoExec } = data const { displayName, username, password, isAdmin, isActive, autoExec } = data
@@ -233,15 +239,15 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const groupController = new GroupController() const groupController = new GroupController()
const allUsersGroup = await groupController const allUsersGroup = await groupController
.getGroupByName(ALL_USERS_GROUP.name) .getGroupByGroupName(ALL_USERS_GROUP.name)
.catch(() => {}) .catch(() => {})
if (allUsersGroup) { if (allUsersGroup) {
await groupController.addUserToGroup(allUsersGroup.uid, savedUser.uid) await groupController.addUserToGroup(allUsersGroup.groupId, savedUser.id)
} }
return { return {
uid: savedUser.uid, id: savedUser.id,
displayName: savedUser.displayName, displayName: savedUser.displayName,
username: savedUser.username, username: savedUser.username,
isActive: savedUser.isActive, isActive: savedUser.isActive,
@@ -250,8 +256,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
} }
} }
export interface GetUserBy { interface GetUserBy {
_id?: string id?: number
username?: string username?: string
} }
@@ -261,10 +267,10 @@ const getUser = async (
): Promise<UserDetailsResponse> => { ): Promise<UserDetailsResponse> => {
const user = (await User.findOne( const user = (await User.findOne(
findBy, findBy,
`uid displayName username isActive isAdmin autoExec` `id displayName username isActive isAdmin autoExec -_id`
).populate( ).populate(
'groups', 'groups',
'uid name description' 'groupId name description -_id'
)) as unknown as UserDetailsResponse )) as unknown as UserDetailsResponse
if (!user) if (!user)
@@ -274,7 +280,7 @@ const getUser = async (
} }
return { return {
uid: user.uid, id: user.id,
displayName: user.displayName, displayName: user.displayName,
username: user.username, username: user.username,
isActive: user.isActive, isActive: user.isActive,
@@ -287,7 +293,7 @@ const getUser = async (
const getDesktopAutoExec = async () => { const getDesktopAutoExec = async () => {
return { return {
...desktopUser, ...desktopUser,
uid: desktopUser.userId, id: desktopUser.userId,
autoExec: await getUserAutoExec() autoExec: await getUserAutoExec()
} }
} }
@@ -323,8 +329,8 @@ const updateUser = async (
const usernameExist = await User.findOne({ username }) const usernameExist = await User.findOne({ username })
if (usernameExist) { if (usernameExist) {
if ( if (
(findBy._id && usernameExist.uid !== findBy._id) || (findBy.id && usernameExist.id != findBy.id) ||
(findBy.username && usernameExist.username !== findBy.username) (findBy.username && usernameExist.username != findBy.username)
) )
throw { throw {
code: 409, code: 409,
@@ -344,11 +350,11 @@ const updateUser = async (
if (!updatedUser) if (!updatedUser)
throw { throw {
code: 404, code: 404,
message: `Unable to find user with ${findBy._id || findBy.username}` message: `Unable to find user with ${findBy.id || findBy.username}`
} }
return { return {
uid: updatedUser.uid, id: updatedUser.id,
username: updatedUser.username, username: updatedUser.username,
displayName: updatedUser.displayName, displayName: updatedUser.displayName,
isAdmin: updatedUser.isAdmin, isAdmin: updatedUser.isAdmin,
@@ -361,7 +367,7 @@ const updateDesktopAutoExec = async (autoExec: string) => {
await updateUserAutoExec(autoExec) await updateUserAutoExec(autoExec)
return { return {
...desktopUser, ...desktopUser,
uid: desktopUser.userId, id: desktopUser.userId,
autoExec autoExec
} }
} }
+2 -2
View File
@@ -121,7 +121,7 @@ const login = async (
req.session.loggedIn = true req.session.loggedIn = true
req.session.user = { req.session.user = {
userId: user.uid, userId: user.id,
clientId: 'web_app', clientId: 'web_app',
username: user.username, username: user.username,
displayName: user.displayName, displayName: user.displayName,
@@ -134,7 +134,7 @@ const login = async (
return { return {
loggedIn: true, loggedIn: true,
user: { user: {
uid: user.uid, id: user.id,
username: user.username, username: user.username,
displayName: user.displayName, displayName: user.displayName,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
+1 -1
View File
@@ -76,7 +76,7 @@ const authenticateToken = async (
const { MODE } = process.env const { MODE } = process.env
if (MODE === ModeType.Desktop) { if (MODE === ModeType.Desktop) {
req.user = { req.user = {
userId: '1234', userId: 1234,
clientId: 'desktopModeClientId', clientId: 'desktopModeClientId',
username: 'desktopModeUsername', username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName', displayName: 'desktopModeDisplayName',
+1 -1
View File
@@ -18,7 +18,7 @@ export const authorize: RequestHandler = async (req, res, next) => {
// no need to check for permissions when route is Public // no need to check for permissions when route is Public
if (await isPublicRoute(req)) return next() 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) if (!dbUser) return res.sendStatus(401)
const path = getPath(req) const path = getPath(req)
+1 -1
View File
@@ -28,7 +28,7 @@ export const desktopRestrict: RequestHandler = (req, res, next) => {
} }
export const desktopUser: RequestUser = { export const desktopUser: RequestUser = {
userId: '12345', userId: 12345,
clientId: 'desktop_app', clientId: 'desktop_app',
username: userInfo().username, username: userInfo().username,
displayName: userInfo().username, displayName: userInfo().username,
+2 -2
View File
@@ -8,8 +8,8 @@ export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
if (!user?.isAdmin) { if (!user?.isAdmin) {
let adminAccountRequired: boolean = true let adminAccountRequired: boolean = true
if (req.params.uid) { if (req.params.userId) {
adminAccountRequired = user?.userId !== req.params.uid adminAccountRequired = user?.userId !== parseInt(req.params.userId)
} else if (req.params.username) { } else if (req.params.username) {
adminAccountRequired = user?.username !== req.params.username adminAccountRequired = user?.username !== req.params.username
} }
+15
View File
@@ -0,0 +1,15 @@
import mongoose, { Schema } from 'mongoose'
const CounterSchema = new Schema({
id: {
type: String,
required: true,
unique: true
},
seq: {
type: Number,
required: true
}
})
export default mongoose.model('Counter', CounterSchema)
+15 -23
View File
@@ -1,9 +1,9 @@
import { Schema, model, Document, Model } from 'mongoose' import { Schema, model, Document, Model } from 'mongoose'
import { GroupDetailsResponse } from '../controllers' import { GroupDetailsResponse } from '../controllers'
import User, { IUser } from './User' import User, { IUser } from './User'
import { AuthProviderType } from '../utils' import { AuthProviderType, getSequenceNextValue } from '../utils'
export const PUBLIC_GROUP_NAME = 'public' export const PUBLIC_GROUP_NAME = 'Public'
export interface GroupPayload { export interface GroupPayload {
/** /**
@@ -24,12 +24,10 @@ export interface GroupPayload {
} }
interface IGroupDocument extends GroupPayload, Document { interface IGroupDocument extends GroupPayload, Document {
groupId: number
isActive: boolean isActive: boolean
users: Schema.Types.ObjectId[] users: Schema.Types.ObjectId[]
authProvider?: AuthProviderType authProvider?: AuthProviderType
// Declare virtual properties as read-only properties
readonly uid: string
} }
interface IGroup extends IGroupDocument { interface IGroup extends IGroupDocument {
@@ -39,23 +37,16 @@ interface IGroup extends IGroupDocument {
} }
interface IGroupModel extends Model<IGroup> {} interface IGroupModel extends Model<IGroup> {}
const opts = { const groupSchema = new Schema<IGroupDocument>({
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: { name: {
type: String, type: String,
required: true, required: true,
unique: true unique: true
}, },
groupId: {
type: Number,
unique: true
},
description: { description: {
type: String, type: String,
default: 'Group description.' default: 'Group description.'
@@ -69,16 +60,17 @@ const groupSchema = new Schema<IGroupDocument>(
default: true default: true
}, },
users: [{ type: Schema.Types.ObjectId, ref: 'User' }] users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
}, })
opts
)
groupSchema.virtual('uid').get(function () { // Hooks
return this._id.toString() groupSchema.pre('save', async function () {
if (this.isNew) {
this.groupId = await getSequenceNextValue('groupId')
}
}) })
groupSchema.post('save', function (group: IGroup, next: Function) { 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() next()
}) })
}) })
+23 -25
View File
@@ -1,5 +1,6 @@
import { Schema, model, Document, Model } from 'mongoose' import { Schema, model, Document, Model } from 'mongoose'
import { PermissionDetailsResponse } from '../controllers' import { PermissionDetailsResponse } from '../controllers'
import { getSequenceNextValue } from '../utils'
interface GetPermissionBy { interface GetPermissionBy {
user?: Schema.Types.ObjectId user?: Schema.Types.ObjectId
@@ -10,11 +11,9 @@ interface IPermissionDocument extends Document {
path: string path: string
type: string type: string
setting: string setting: string
permissionId: number
user: Schema.Types.ObjectId user: Schema.Types.ObjectId
group: Schema.Types.ObjectId group: Schema.Types.ObjectId
// Declare virtual properties as read-only properties
readonly uid: string
} }
interface IPermission extends IPermissionDocument {} interface IPermission extends IPermissionDocument {}
@@ -23,19 +22,11 @@ interface IPermissionModel extends Model<IPermission> {
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]> get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
} }
const opts = { const permissionSchema = new Schema<IPermissionDocument>({
toJSON: { permissionId: {
virtuals: true, type: Number,
transform: function (doc: any, ret: any, options: any) { unique: true
delete ret._id },
delete ret.id
return ret
}
}
}
const permissionSchema = new Schema<IPermissionDocument>(
{
path: { path: {
type: String, type: String,
required: true required: true
@@ -50,12 +41,13 @@ const permissionSchema = new Schema<IPermissionDocument>(
}, },
user: { type: Schema.Types.ObjectId, ref: 'User' }, user: { type: Schema.Types.ObjectId, ref: 'User' },
group: { type: Schema.Types.ObjectId, ref: 'Group' } group: { type: Schema.Types.ObjectId, ref: 'Group' }
}, })
opts
)
permissionSchema.virtual('uid').get(function () { // Hooks
return this._id.toString() permissionSchema.pre('save', async function () {
if (this.isNew) {
this.permissionId = await getSequenceNextValue('permissionId')
}
}) })
// Static Methods // Static Methods
@@ -63,14 +55,20 @@ permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
PermissionDetailsResponse[] PermissionDetailsResponse[]
> { > {
return (await this.find(getBy) return (await this.find(getBy)
.select('uid path type setting') .select({
.populate({ path: 'user', select: 'uid username displayName isAdmin' }) _id: 0,
permissionId: 1,
path: 1,
type: 1,
setting: 1
})
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
.populate({ .populate({
path: 'group', path: 'group',
select: 'uid name description', select: 'groupId name description -_id',
populate: { populate: {
path: 'users', path: 'users',
select: 'uid username displayName isAdmin', select: 'id username displayName isAdmin -_id',
options: { limit: 15 } options: { limit: 15 }
} }
})) as unknown as PermissionDetailsResponse[] })) as unknown as PermissionDetailsResponse[]
+16 -22
View File
@@ -1,6 +1,6 @@
import { Schema, model, Document, Model, ObjectId } from 'mongoose' import { Schema, model, Document, Model } from 'mongoose'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
import { AuthProviderType } from '../utils' import { AuthProviderType, getSequenceNextValue } from '../utils'
export interface UserPayload { export interface UserPayload {
/** /**
@@ -36,6 +36,7 @@ export interface UserPayload {
interface IUserDocument extends UserPayload, Document { interface IUserDocument extends UserPayload, Document {
_id: Schema.Types.ObjectId _id: Schema.Types.ObjectId
id: number
isAdmin: boolean isAdmin: boolean
isActive: boolean isActive: boolean
needsToUpdatePassword: boolean needsToUpdatePassword: boolean
@@ -43,9 +44,6 @@ interface IUserDocument extends UserPayload, Document {
groups: Schema.Types.ObjectId[] groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }] tokens: [{ [key: string]: string }]
authProvider?: AuthProviderType authProvider?: AuthProviderType
// Declare virtual properties as read-only properties
readonly uid: string
} }
export interface IUser extends IUserDocument { export interface IUser extends IUserDocument {
@@ -56,19 +54,8 @@ export interface IUser extends IUserDocument {
interface IUserModel extends Model<IUser> { interface IUserModel extends Model<IUser> {
hashPassword(password: string): string 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>( const userSchema = new Schema<IUserDocument>({
{
displayName: { displayName: {
type: String, type: String,
required: true required: true
@@ -78,6 +65,10 @@ const userSchema = new Schema<IUserDocument>(
required: true, required: true,
unique: true unique: true
}, },
id: {
type: Number,
unique: true
},
password: { password: {
type: String, type: String,
required: true required: true
@@ -118,12 +109,15 @@ const userSchema = new Schema<IUserDocument>(
} }
} }
] ]
}, })
opts
)
userSchema.virtual('uid').get(function () { // Hooks
return this._id.toString() userSchema.pre('save', async function (next) {
if (this.isNew) {
this.id = await getSequenceNextValue('id')
}
next()
}) })
// Static Methods // Static Methods
+20 -24
View File
@@ -1,11 +1,7 @@
import express from 'express' import express from 'express'
import { GroupController } from '../../controllers/' import { GroupController } from '../../controllers/'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares' import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
import { import { getGroupValidation, registerGroupValidation } from '../../utils'
getGroupValidation,
registerGroupValidation,
uidValidation
} from '../../utils'
const groupRouter = express.Router() const groupRouter = express.Router()
@@ -37,15 +33,12 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
} }
}) })
groupRouter.get('/:uid', authenticateAccessToken, async (req, res) => { groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const { error: uidError, value: params } = uidValidation(req.params) const { groupId } = req.params
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.getGroup(uid) const response = await controller.getGroup(parseInt(groupId))
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -63,7 +56,7 @@ groupRouter.get(
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.getGroupByName(name) const response = await controller.getGroupByGroupName(name)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -72,15 +65,18 @@ groupRouter.get(
) )
groupRouter.post( groupRouter.post(
'/:groupUid/:userUid', '/:groupId/:userId',
authenticateAccessToken, authenticateAccessToken,
verifyAdmin, verifyAdmin,
async (req, res) => { async (req, res) => {
const { groupUid, userUid } = req.params const { groupId, userId } = req.params
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.addUserToGroup(groupUid, userUid) const response = await controller.addUserToGroup(
parseInt(groupId),
parseInt(userId)
)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -89,15 +85,18 @@ groupRouter.post(
) )
groupRouter.delete( groupRouter.delete(
'/:groupUid/:userUid', '/:groupId/:userId',
authenticateAccessToken, authenticateAccessToken,
verifyAdmin, verifyAdmin,
async (req, res) => { async (req, res) => {
const { groupUid, userUid } = req.params const { groupId, userId } = req.params
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.removeUserFromGroup(groupUid, userUid) const response = await controller.removeUserFromGroup(
parseInt(groupId),
parseInt(userId)
)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -106,18 +105,15 @@ groupRouter.delete(
) )
groupRouter.delete( groupRouter.delete(
'/:uid', '/:groupId',
authenticateAccessToken, authenticateAccessToken,
verifyAdmin, verifyAdmin,
async (req, res) => { async (req, res) => {
const { error: uidError, value: params } = uidValidation(req.params) const { groupId } = req.params
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
const controller = new GroupController() const controller = new GroupController()
try { try {
await controller.deleteGroup(uid) await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!') res.status(200).send('Group Deleted!')
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
+11 -13
View File
@@ -3,7 +3,6 @@ import { PermissionController } from '../../controllers/'
import { verifyAdmin } from '../../middlewares' import { verifyAdmin } from '../../middlewares'
import { import {
registerPermissionValidation, registerPermissionValidation,
uidValidation,
updatePermissionValidation updatePermissionValidation
} from '../../utils' } from '../../utils'
@@ -35,17 +34,14 @@ permissionRouter.post('/', verifyAdmin, async (req, res) => {
} }
}) })
permissionRouter.patch('/:uid', verifyAdmin, async (req: any, res) => { permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => {
const { error: uidError, value: params } = uidValidation(req.params) const { permissionId } = req.params
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
const { error, value: body } = updatePermissionValidation(req.body) const { error, value: body } = updatePermissionValidation(req.body)
if (error) return res.status(400).send(error.details[0].message) if (error) return res.status(400).send(error.details[0].message)
try { try {
const response = await controller.updatePermission(uid, body) const response = await controller.updatePermission(permissionId, body)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
const statusCode = err.code const statusCode = err.code
@@ -54,18 +50,20 @@ permissionRouter.patch('/:uid', verifyAdmin, async (req: any, res) => {
} }
}) })
permissionRouter.delete('/:uid', verifyAdmin, async (req: any, res) => { permissionRouter.delete(
const { error: uidError, value: params } = uidValidation(req.params) '/:permissionId',
if (uidError) return res.status(400).send(uidError.details[0].message) verifyAdmin,
async (req: any, res) => {
const { permissionId } = req.params
const { uid } = params
try { try {
await controller.deletePermission(uid) await controller.deletePermission(permissionId)
res.status(200).send('Permission Deleted!') res.status(200).send('Permission Deleted!')
} catch (err: any) { } catch (err: any) {
const statusCode = err.code const statusCode = err.code
delete err.code delete err.code
res.status(statusCode).send(err.message) res.status(statusCode).send(err.message)
} }
}) }
)
export default permissionRouter export default permissionRouter
+6 -6
View File
@@ -13,7 +13,6 @@ import {
generateAccessToken, generateAccessToken,
generateAuthCode, generateAuthCode,
generateRefreshToken, generateRefreshToken,
randomBytesHexString,
saveTokensInDB, saveTokensInDB,
verifyTokenInDB verifyTokenInDB
} from '../../../utils' } from '../../../utils'
@@ -21,6 +20,7 @@ import {
const clientId = 'someclientID' const clientId = 'someclientID'
const clientSecret = 'someclientSecret' const clientSecret = 'someclientSecret'
const user = { const user = {
id: 1234,
displayName: 'Test User', displayName: 'Test User',
username: 'testUsername', username: 'testUsername',
password: '87654321', password: '87654321',
@@ -52,7 +52,7 @@ describe('auth', () => {
describe('token', () => { describe('token', () => {
const userInfo: InfoJWT = { const userInfo: InfoJWT = {
clientId, clientId,
userId: randomBytesHexString(12) userId: user.id
} }
beforeAll(async () => { beforeAll(async () => {
await userController.createUser(user) await userController.createUser(user)
@@ -151,10 +151,10 @@ describe('auth', () => {
currentUser = await userController.createUser(user) currentUser = await userController.createUser(user)
refreshToken = generateRefreshToken({ refreshToken = generateRefreshToken({
clientId, clientId,
userId: currentUser.uid userId: currentUser.id
}) })
await saveTokensInDB( await saveTokensInDB(
currentUser.uid, currentUser.id,
clientId, clientId,
'accessToken', 'accessToken',
refreshToken refreshToken
@@ -202,11 +202,11 @@ describe('auth', () => {
currentUser = await userController.createUser(user) currentUser = await userController.createUser(user)
accessToken = generateAccessToken({ accessToken = generateAccessToken({
clientId, clientId,
userId: currentUser.uid userId: currentUser.id
}) })
await saveTokensInDB( await saveTokensInDB(
currentUser.uid, currentUser.id,
clientId, clientId,
accessToken, accessToken,
'refreshToken' 'refreshToken'
+6 -6
View File
@@ -40,10 +40,10 @@ describe('client', () => {
const dbUser = await userController.createUser(adminUser) const dbUser = await userController.createUser(adminUser)
adminAccessToken = generateAccessToken({ adminAccessToken = generateAccessToken({
clientId: client.clientId, clientId: client.clientId,
userId: dbUser.uid userId: dbUser.id
}) })
await saveTokensInDB( await saveTokensInDB(
dbUser.uid, dbUser.id,
client.clientId, client.clientId,
adminAccessToken, adminAccessToken,
'refreshToken' 'refreshToken'
@@ -95,10 +95,10 @@ describe('client', () => {
const dbUser = await userController.createUser(user) const dbUser = await userController.createUser(user)
const accessToken = generateAccessToken({ const accessToken = generateAccessToken({
clientId: client.clientId, clientId: client.clientId,
userId: dbUser.uid userId: dbUser.id
}) })
await saveTokensInDB( await saveTokensInDB(
dbUser.uid, dbUser.id,
client.clientId, client.clientId,
accessToken, accessToken,
'refreshToken' 'refreshToken'
@@ -212,10 +212,10 @@ describe('client', () => {
const dbUser = await userController.createUser(user) const dbUser = await userController.createUser(user)
const accessToken = generateAccessToken({ const accessToken = generateAccessToken({
clientId: client.clientId, clientId: client.clientId,
userId: dbUser.uid userId: dbUser.id
}) })
await saveTokensInDB( await saveTokensInDB(
dbUser.uid, dbUser.id,
client.clientId, client.clientId,
accessToken, accessToken,
'refreshToken' 'refreshToken'
+16 -10
View File
@@ -71,13 +71,13 @@ describe('code', () => {
permissionController = new PermissionController() permissionController = new PermissionController()
const dbUser = await userController.createUser(user) const dbUser = await userController.createUser(user)
accessToken = await generateAndSaveToken(dbUser.uid) accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({ await permissionController.createPermission({
path: '/SASjsApi/code/execute', path: '/SASjsApi/code/execute',
type: PermissionType.route, type: PermissionType.route,
principalType: PrincipalType.user, principalType: PrincipalType.user,
principalId: dbUser.uid, principalId: dbUser.id,
setting: PermissionSettingForRoute.grant setting: PermissionSettingForRoute.grant
}) })
@@ -104,25 +104,31 @@ describe('code', () => {
) )
}, 30000) }, 30000)
it('returns a prompt 400 (not a hang) with the complete log when the SAS session fails (%abort;)', async () => { // A failed SAS session (e.g. %abort;) is a normal outcome of running
// arbitrary user code, not a request-shape/server problem - the HTTP
// request itself was fine, so this must respond exactly like a
// successful run (200, same body shape, no hang), with the log simply
// reflecting what happened. Regression test for #388's follow-up: the
// original #388 fix stopped the hang but over-corrected into a 400
// with a bespoke error shape, which broke Studio's log-tab rendering.
it('returns 200 with the same shape as a successful run when the SAS session fails (%abort;), not a hang or an error shape', async () => {
const response = await request(app) const response = await request(app)
.post('/SASjsApi/code/execute') .post('/SASjsApi/code/execute')
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send({ code: '%abort;', runTime: 'sas' }) .send({ code: '%abort;', runTime: 'sas' })
.expect(400) .expect(200)
expect(response.body).toMatchObject({ expect(response.text).toEqual(
status: 'failure',
message: 'Job execution failed.'
})
expect(response.body.log).toEqual(
expect.stringContaining('mock SAS execution') expect.stringContaining('mock SAS execution')
) )
expect(response.text).toEqual(
expect.stringContaining('SAS session terminated')
)
}, 30000) }, 30000)
}) })
}) })
const generateAndSaveToken = async (userId: string) => { const generateAndSaveToken = async (userId: number) => {
const accessToken = generateAccessToken({ const accessToken = generateAccessToken({
clientId, clientId,
userId userId
+7 -7
View File
@@ -80,31 +80,31 @@ describe('drive', () => {
con = await mongoose.connect(mongoServer.getUri()) con = await mongoose.connect(mongoServer.getUri())
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
accessToken = await generateAndSaveToken(dbUser.uid) accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/deploy', path: '/SASjsApi/drive/deploy',
principalId: dbUser.uid principalId: dbUser.id
}) })
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/deploy/upload', path: '/SASjsApi/drive/deploy/upload',
principalId: dbUser.uid principalId: dbUser.id
}) })
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/file', path: '/SASjsApi/drive/file',
principalId: dbUser.uid principalId: dbUser.id
}) })
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/folder', path: '/SASjsApi/drive/folder',
principalId: dbUser.uid principalId: dbUser.id
}) })
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/rename', path: '/SASjsApi/drive/rename',
principalId: dbUser.uid principalId: dbUser.id
}) })
}) })
@@ -1206,7 +1206,7 @@ const getExampleService = (): ServiceMember =>
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember) ((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
.members[0] as ServiceMember .members[0] as ServiceMember
const generateAndSaveToken = async (userId: string) => { const generateAndSaveToken = async (userId: number) => {
const adminAccessToken = generateAccessToken({ const adminAccessToken = generateAccessToken({
clientId, clientId,
userId userId
+10 -2
View File
@@ -93,11 +93,19 @@ if (!readResult.ok) {
const code = readResult.value const code = readResult.value
// real SAS writes errors/aborts directly into the log file itself, not
// just to stderr - mirror that so tests asserting on log content (what
// the API actually returns to the caller) are meaningful
const isAbort = code.includes('%abort;')
const logContent = isAbort
? `NOTE: mock SAS execution\n${code}\nERROR: SAS session terminated. See log for details.\n`
: `NOTE: mock SAS execution\n${code}\n`
if (logPath) { if (logPath) {
retry(() => fs.writeFileSync(logPath, `NOTE: mock SAS execution\n${code}\n`)) retry(() => fs.writeFileSync(logPath, logContent))
} }
if (code.includes('%abort;')) { if (isAbort) {
process.stderr.write('ERROR: SAS session terminated. See log for details.\n') process.stderr.write('ERROR: SAS session terminated. See log for details.\n')
process.exit(1) process.exit(1)
} }
+47 -60
View File
@@ -11,7 +11,6 @@ import {
} from '../../../utils' } from '../../../utils'
import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group' import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group'
import User from '../../../model/User' import User from '../../../model/User'
import { randomBytes } from 'crypto'
const clientId = 'someclientID' const clientId = 'someclientID'
const adminUser = { const adminUser = {
@@ -76,7 +75,7 @@ describe('group', () => {
.send(group) .send(group)
.expect(200) .expect(200)
expect(res.body.uid).toBeTruthy() expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -156,7 +155,7 @@ describe('group', () => {
const dbGroup = await groupController.createGroup(group) const dbGroup = await groupController.createGroup(group)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}`) .delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -175,17 +174,17 @@ describe('group', () => {
username: 'deletegroup2' username: 'deletegroup2'
}) })
await groupController.addUserToGroup(dbGroup.uid, dbUser1.uid) await groupController.addUserToGroup(dbGroup.groupId, dbUser1.id)
await groupController.addUserToGroup(dbGroup.uid, dbUser2.uid) await groupController.addUserToGroup(dbGroup.groupId, dbUser2.id)
await request(app) await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}`) .delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
const res1 = await request(app) const res1 = await request(app)
.get(`/SASjsApi/user/${dbUser1.uid}`) .get(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -193,7 +192,7 @@ describe('group', () => {
expect(res1.body.groups).toEqual([]) expect(res1.body.groups).toEqual([])
const res2 = await request(app) const res2 = await request(app)
.get(`/SASjsApi/user/${dbUser2.uid}`) .get(`/SASjsApi/user/${dbUser2.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -202,10 +201,8 @@ describe('group', () => {
}) })
it('should respond with Not Found if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${hexValue}`) .delete(`/SASjsApi/group/1234`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -232,7 +229,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}`) .delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send() .send()
.expect(401) .expect(401)
@@ -248,15 +245,15 @@ describe('group', () => {
}) })
it('should respond with group', async () => { it('should respond with group', async () => {
const { uid } = await groupController.createGroup(group) const { groupId } = await groupController.createGroup(group)
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/group/${uid}`) .get(`/SASjsApi/group/${groupId}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.uid).toBeTruthy() expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -269,15 +266,15 @@ describe('group', () => {
username: 'get' + user.username username: 'get' + user.username
}) })
const { uid } = await groupController.createGroup(group) const { groupId } = await groupController.createGroup(group)
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/group/${uid}`) .get(`/SASjsApi/group/${groupId}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.uid).toBeTruthy() expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -295,10 +292,8 @@ describe('group', () => {
}) })
it('should respond with Not Found if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/group/${hexValue}`) .get('/SASjsApi/group/1234')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -317,7 +312,7 @@ describe('group', () => {
.send() .send()
.expect(200) .expect(200)
expect(res.body.uid).toBeTruthy() expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -338,7 +333,7 @@ describe('group', () => {
.send() .send()
.expect(200) .expect(200)
expect(res.body.uid).toBeTruthy() expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -384,7 +379,7 @@ describe('group', () => {
expect(res.body).toEqual([ expect(res.body).toEqual([
{ {
uid: expect.anything(), groupId: expect.anything(),
name: group.name, name: group.name,
description: group.description description: group.description
} }
@@ -406,7 +401,7 @@ describe('group', () => {
expect(res.body).toEqual([ expect(res.body).toEqual([
{ {
uid: expect.anything(), groupId: expect.anything(),
name: group.name, name: group.name,
description: group.description description: group.description
} }
@@ -431,18 +426,18 @@ describe('group', () => {
const dbUser = await userController.createUser(user) const dbUser = await userController.createUser(user)
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`) .post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.uid).toBeTruthy() expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([ expect(res.body.users).toEqual([
{ {
uid: expect.anything(), id: expect.anything(),
username: user.username, username: user.username,
displayName: user.displayName displayName: user.displayName
} }
@@ -457,20 +452,20 @@ describe('group', () => {
}) })
await request(app) await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`) .post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/user/${dbUser.uid}`) .get(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.groups).toEqual([ expect(res.body.groups).toEqual([
{ {
uid: expect.anything(), groupId: expect.anything(),
name: group.name, name: group.name,
description: group.description description: group.description
} }
@@ -483,21 +478,21 @@ describe('group', () => {
...user, ...user,
username: 'addUserRandomUser' username: 'addUserRandomUser'
}) })
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid) await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`) .post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.uid).toBeTruthy() expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([ expect(res.body.users).toEqual([
{ {
uid: expect.anything(), id: expect.anything(),
username: 'addUserRandomUser', username: 'addUserRandomUser',
displayName: user.displayName displayName: user.displayName
} }
@@ -531,10 +526,8 @@ describe('group', () => {
}) })
it('should respond with Not Found if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${hexValue}/123`) .post('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -545,10 +538,8 @@ describe('group', () => {
it('should respond with Not Found if userId is incorrect', async () => { it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group) const dbGroup = await groupController.createGroup(group)
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${hexValue}`) .post(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -565,7 +556,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`) .post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(400) .expect(400)
@@ -586,7 +577,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`) .post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(405) .expect(405)
@@ -605,7 +596,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`) .post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(405) .expect(405)
@@ -627,15 +618,15 @@ describe('group', () => {
...user, ...user,
username: 'removeUserRandomUser' username: 'removeUserRandomUser'
}) })
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid) await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`) .delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
expect(res.body.uid).toBeTruthy() expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name) expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description) expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true) expect(res.body.isActive).toEqual(true)
@@ -648,16 +639,16 @@ describe('group', () => {
...user, ...user,
username: 'removeGroupFromUser' username: 'removeGroupFromUser'
}) })
await groupController.addUserToGroup(dbGroup.uid, dbUser.uid) await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
await request(app) await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`) .delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/user/${dbUser.uid}`) .get(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -676,7 +667,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`) .delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(405) .expect(405)
@@ -695,7 +686,7 @@ describe('group', () => {
}) })
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}/${dbUser.uid}`) .delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(405) .expect(405)
@@ -732,10 +723,8 @@ describe('group', () => {
}) })
it('should respond with Not Found if groupId is incorrect', async () => { it('should respond with Not Found if groupId is incorrect', async () => {
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${hexValue}/123`) .delete('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -746,10 +735,8 @@ describe('group', () => {
it('should respond with Not Found if userId is incorrect', async () => { it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group) const dbGroup = await groupController.createGroup(group)
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.uid}/${hexValue}`) .delete(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -765,10 +752,10 @@ const generateSaveTokenAndCreateUser = async (
): Promise<string> => { ): Promise<string> => {
const dbUser = await userController.createUser(someUser ?? adminUser) 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({ const adminAccessToken = generateAccessToken({
clientId, clientId,
userId userId
+32 -38
View File
@@ -17,7 +17,6 @@ import {
PermissionDetailsResponse PermissionDetailsResponse
} from '../../../controllers' } from '../../../controllers'
import { generateAccessToken, saveTokensInDB } from '../../../utils' import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { randomBytes } from 'crypto'
const deployPayload = { const deployPayload = {
appLoc: 'string', appLoc: 'string',
@@ -104,10 +103,10 @@ describe('permission', () => {
const res = await request(app) const res = await request(app)
.post('/SASjsApi/permission') .post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ ...permission, principalId: dbUser.uid }) .send({ ...permission, principalId: dbUser.id })
.expect(200) .expect(200)
expect(res.body.uid).toBeTruthy() expect(res.body.permissionId).toBeTruthy()
expect(res.body.path).toEqual(permission.path) expect(res.body.path).toEqual(permission.path)
expect(res.body.type).toEqual(permission.type) expect(res.body.type).toEqual(permission.type)
expect(res.body.setting).toEqual(permission.setting) expect(res.body.setting).toEqual(permission.setting)
@@ -123,11 +122,11 @@ describe('permission', () => {
.send({ .send({
...permission, ...permission,
principalType: 'group', principalType: 'group',
principalId: dbGroup.uid principalId: dbGroup.groupId
}) })
.expect(200) .expect(200)
expect(res.body.uid).toBeTruthy() expect(res.body.permissionId).toBeTruthy()
expect(res.body.path).toEqual(permission.path) expect(res.body.path).toEqual(permission.path)
expect(res.body.type).toEqual(permission.type) expect(res.body.type).toEqual(permission.type)
expect(res.body.setting).toEqual(permission.setting) 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 () => { 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) const res = await request(app)
.post('/SASjsApi/permission') .post('/SASjsApi/permission')
@@ -282,19 +281,17 @@ describe('permission', () => {
expect(res.body).toEqual({}) 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) const res = await request(app)
.post('/SASjsApi/permission') .post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ .send({
...permission, ...permission,
principalId: randomBytes(10).toString('hex') principalId: 'someCharacters'
}) })
.expect(400) .expect(400)
expect(res.text).toEqual( expect(res.text).toEqual('"principalId" must be a number')
'"principalId" length must be 24 characters long'
)
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -310,7 +307,7 @@ describe('permission', () => {
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ .send({
...permission, ...permission,
principalId: adminUser.uid principalId: adminUser.id
}) })
.expect(400) .expect(400)
@@ -324,7 +321,7 @@ describe('permission', () => {
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ .send({
...permission, ...permission,
principalId: randomBytes(12).toString('hex') principalId: 123
}) })
.expect(404) .expect(404)
@@ -339,7 +336,7 @@ describe('permission', () => {
.send({ .send({
...permission, ...permission,
principalType: 'group', principalType: 'group',
principalId: randomBytes(12).toString('hex') principalId: 123
}) })
.expect(404) .expect(404)
@@ -350,13 +347,13 @@ describe('permission', () => {
it('should respond with Conflict (409) if permission already exists', async () => { it('should respond with Conflict (409) if permission already exists', async () => {
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
principalId: dbUser.uid principalId: dbUser.id
}) })
const res = await request(app) const res = await request(app)
.post('/SASjsApi/permission') .post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ ...permission, principalId: dbUser.uid }) .send({ ...permission, principalId: dbUser.id })
.expect(409) .expect(409)
expect(res.text).toEqual( expect(res.text).toEqual(
@@ -371,7 +368,7 @@ describe('permission', () => {
beforeAll(async () => { beforeAll(async () => {
dbPermission = await permissionController.createPermission({ dbPermission = await permissionController.createPermission({
...permission, ...permission,
principalId: dbUser.uid principalId: dbUser.id
}) })
}) })
@@ -381,7 +378,7 @@ describe('permission', () => {
it('should respond with updated permission', async () => { it('should respond with updated permission', async () => {
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.uid}`) .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ setting: PermissionSettingForRoute.deny }) .send({ setting: PermissionSettingForRoute.deny })
.expect(200) .expect(200)
@@ -391,7 +388,7 @@ describe('permission', () => {
it('should respond with Unauthorized if access token is not present', async () => { it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.uid}`) .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.send() .send()
.expect(401) .expect(401)
@@ -406,7 +403,7 @@ describe('permission', () => {
}) })
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.uid}`) .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send() .send()
.expect(401) .expect(401)
@@ -417,7 +414,7 @@ describe('permission', () => {
it('should respond with Bad Request if setting is missing', async () => { it('should respond with Bad Request if setting is missing', async () => {
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.uid}`) .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(400) .expect(400)
@@ -428,7 +425,7 @@ describe('permission', () => {
it('should respond with Bad Request if setting is invalid', async () => { it('should respond with Bad Request if setting is invalid', async () => {
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.uid}`) .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ .send({
setting: 'invalid' setting: 'invalid'
@@ -440,9 +437,8 @@ describe('permission', () => {
}) })
it('should respond with not found (404) if permission with provided id does not exist', async () => { 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) const res = await request(app)
.patch(`/SASjsApi/permission/${hexValue}`) .patch('/SASjsApi/permission/123')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ .send({
setting: PermissionSettingForRoute.deny setting: PermissionSettingForRoute.deny
@@ -458,10 +454,10 @@ describe('permission', () => {
it('should delete permission', async () => { it('should delete permission', async () => {
const dbPermission = await permissionController.createPermission({ const dbPermission = await permissionController.createPermission({
...permission, ...permission,
principalId: dbUser.uid principalId: dbUser.id
}) })
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/permission/${dbPermission?.uid}`) .delete(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -470,10 +466,8 @@ describe('permission', () => {
}) })
it('should respond with not found (404) if permission with provided id does not exists', async () => { 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) const res = await request(app)
.delete(`/SASjsApi/permission/${hexValue}`) .delete('/SASjsApi/permission/123')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -487,12 +481,12 @@ describe('permission', () => {
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/test-1', path: '/test-1',
principalId: dbUser.uid principalId: dbUser.id
}) })
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/test-2', path: '/test-2',
principalId: dbUser.uid principalId: dbUser.id
}) })
}) })
@@ -511,12 +505,12 @@ describe('permission', () => {
...user, ...user,
username: 'get' + user.username username: 'get' + user.username
}) })
const accessToken = await generateAndSaveToken(nonAdminUser.uid) const accessToken = await generateAndSaveToken(nonAdminUser.id)
await permissionController.createPermission({ await permissionController.createPermission({
path: '/test-1', path: '/test-1',
type: PermissionType.route, type: PermissionType.route,
principalType: PrincipalType.user, principalType: PrincipalType.user,
principalId: nonAdminUser.uid, principalId: nonAdminUser.id,
setting: PermissionSettingForRoute.grant setting: PermissionSettingForRoute.grant
}) })
@@ -537,7 +531,7 @@ describe('permission', () => {
await permissionController.createPermission({ await permissionController.createPermission({
...permission, ...permission,
path: '/SASjsApi/drive/deploy', path: '/SASjsApi/drive/deploy',
principalId: dbUser.uid principalId: dbUser.id
}) })
}) })
@@ -557,7 +551,7 @@ describe('permission', () => {
}) })
it('should create files in SASJS drive', async () => { it('should create files in SASJS drive', async () => {
const accessToken = await generateAndSaveToken(dbUser.uid) const accessToken = await generateAndSaveToken(dbUser.id)
await request(app) await request(app)
.get('/SASjsApi/drive/deploy') .get('/SASjsApi/drive/deploy')
@@ -567,7 +561,7 @@ describe('permission', () => {
}) })
it('should respond unauthorized', async () => { it('should respond unauthorized', async () => {
const accessToken = await generateAndSaveToken(dbUser.uid) const accessToken = await generateAndSaveToken(dbUser.id)
await request(app) await request(app)
.get('/SASjsApi/drive/deploy/upload') .get('/SASjsApi/drive/deploy/upload')
@@ -583,10 +577,10 @@ const generateSaveTokenAndCreateUser = async (
): Promise<string> => { ): Promise<string> => {
const dbUser = await userController.createUser(someUser ?? adminUser) 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({ const adminAccessToken = generateAccessToken({
clientId, clientId,
userId userId
+3 -3
View File
@@ -58,12 +58,12 @@ describe('stp', () => {
mongoServer = await MongoMemoryServer.create() mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri()) con = await mongoose.connect(mongoServer.getUri())
const dbUser = await userController.createUser(user) const dbUser = await userController.createUser(user)
accessToken = await generateAndSaveToken(dbUser.uid) accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({ await permissionController.createPermission({
path: '/SASjsApi/stp/execute', path: '/SASjsApi/stp/execute',
type: PermissionType.route, type: PermissionType.route,
principalType: PrincipalType.user, principalType: PrincipalType.user,
principalId: dbUser.uid, principalId: dbUser.id,
setting: PermissionSettingForRoute.grant setting: PermissionSettingForRoute.grant
}) })
}) })
@@ -456,7 +456,7 @@ const makeRequestAndAssert = async (
) )
} }
const generateAndSaveToken = async (userId: string) => { const generateAndSaveToken = async (userId: number) => {
const accessToken = generateAccessToken({ const accessToken = generateAccessToken({
clientId, clientId,
userId userId
+40 -43
View File
@@ -1,4 +1,3 @@
import { randomBytes } from 'crypto'
import { Express } from 'express' import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose' import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server' import { MongoMemoryServer } from 'mongodb-memory-server'
@@ -102,9 +101,9 @@ describe('user', () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = generateAccessToken({ const accessToken = generateAccessToken({
clientId, 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) const res = await request(app)
.post('/SASjsApi/user') .post('/SASjsApi/user')
@@ -188,7 +187,7 @@ describe('user', () => {
const newDisplayName = 'My new display Name' const newDisplayName = 'My new display Name'
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/user/${dbUser.uid}`) .patch(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ ...user, displayName: newDisplayName }) .send({ ...user, displayName: newDisplayName })
.expect(200) .expect(200)
@@ -201,11 +200,11 @@ describe('user', () => {
it('should respond with updated user when user himself requests', async () => { it('should respond with updated user when user himself requests', async () => {
const dbUser = await controller.createUser(user) 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 newDisplayName = 'My new display Name'
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/user/${dbUser.uid}`) .patch(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send({ .send({
displayName: newDisplayName, displayName: newDisplayName,
@@ -222,11 +221,11 @@ describe('user', () => {
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => { it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
const dbUser = await controller.createUser(user) 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 newDisplayName = 'My new display Name'
await request(app) await request(app)
.patch(`/SASjsApi/user/${dbUser.uid}`) .patch(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send({ ...user, displayName: newDisplayName }) .send({ ...user, displayName: newDisplayName })
.expect(400) .expect(400)
@@ -278,10 +277,10 @@ describe('user', () => {
...user, ...user,
username: 'randomUser' username: 'randomUser'
}) })
const accessToken = await generateAndSaveToken(dbUser2.uid) const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.uid}`) .patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send(user) .send(user)
.expect(401) .expect(401)
@@ -298,7 +297,7 @@ describe('user', () => {
}) })
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.uid}`) .patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username }) .send({ username: dbUser2.username })
.expect(409) .expect(409)
@@ -326,7 +325,7 @@ describe('user', () => {
it('should respond with updated user when user himself requests', async () => { it('should respond with updated user when user himself requests', async () => {
const dbUser = await controller.createUser(user) 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 newDisplayName = 'My new display Name'
const res = await request(app) const res = await request(app)
@@ -347,7 +346,7 @@ describe('user', () => {
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => { it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
const dbUser = await controller.createUser(user) 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 newDisplayName = 'My new display Name'
await request(app) await request(app)
@@ -373,10 +372,10 @@ describe('user', () => {
...user, ...user,
username: 'randomUser' username: 'randomUser'
}) })
const accessToken = await generateAndSaveToken(dbUser2.uid) const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.uid}`) .patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send(user) .send(user)
.expect(401) .expect(401)
@@ -419,7 +418,7 @@ describe('user', () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.uid}`) .delete(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(200) .expect(200)
@@ -429,10 +428,10 @@ describe('user', () => {
it('should respond with OK when user himself requests', async () => { it('should respond with OK when user himself requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid) const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.uid}`) .delete(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send({ password: user.password }) .send({ password: user.password })
.expect(200) .expect(200)
@@ -442,10 +441,10 @@ describe('user', () => {
it('should respond with Bad Request when user himself requests and password is missing', async () => { it('should respond with Bad Request when user himself requests and password is missing', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid) const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.uid}`) .delete(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send() .send()
.expect(400) .expect(400)
@@ -470,10 +469,10 @@ describe('user', () => {
...user, ...user,
username: 'randomUser' username: 'randomUser'
}) })
const accessToken = await generateAndSaveToken(dbUser2.uid) const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/${dbUser1.uid}`) .delete(`/SASjsApi/user/${dbUser1.id}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send(user) .send(user)
.expect(401) .expect(401)
@@ -484,10 +483,10 @@ describe('user', () => {
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => { it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid) const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/${dbUser.uid}`) .delete(`/SASjsApi/user/${dbUser.id}`)
.auth(accessToken, { type: 'bearer' }) .auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' }) .send({ password: 'incorrectpassword' })
.expect(401) .expect(401)
@@ -511,7 +510,7 @@ describe('user', () => {
it('should respond with OK when user himself requests', async () => { it('should respond with OK when user himself requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid) const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`) .delete(`/SASjsApi/user/by/username/${dbUser.username}`)
@@ -524,7 +523,7 @@ describe('user', () => {
it('should respond with Bad Request when user himself requests and password is missing', async () => { it('should respond with Bad Request when user himself requests and password is missing', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid) const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`) .delete(`/SASjsApi/user/by/username/${dbUser.username}`)
@@ -552,7 +551,7 @@ describe('user', () => {
...user, ...user,
username: 'randomUser' username: 'randomUser'
}) })
const accessToken = await generateAndSaveToken(dbUser2.uid) const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser1.username}`) .delete(`/SASjsApi/user/by/username/${dbUser1.username}`)
@@ -566,7 +565,7 @@ describe('user', () => {
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => { it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.uid) const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app) const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`) .delete(`/SASjsApi/user/by/username/${dbUser.username}`)
@@ -593,7 +592,7 @@ describe('user', () => {
it('should respond with user autoExec when same user requests', async () => { it('should respond with user autoExec when same user requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.uid const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId) const accessToken = await generateAndSaveToken(userId)
const res = await request(app) const res = await request(app)
@@ -612,7 +611,7 @@ describe('user', () => {
it('should respond with user autoExec when admin user requests', async () => { it('should respond with user autoExec when admin user requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.uid const userId = dbUser.id
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/user/${userId}`) .get(`/SASjsApi/user/${userId}`)
@@ -635,7 +634,7 @@ describe('user', () => {
}) })
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.uid const userId = dbUser.id
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/user/${userId}`) .get(`/SASjsApi/user/${userId}`)
@@ -653,7 +652,7 @@ describe('user', () => {
it('should respond with user along with associated groups', async () => { it('should respond with user along with associated groups', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.uid const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId) const accessToken = await generateAndSaveToken(userId)
const group = { const group = {
@@ -662,7 +661,7 @@ describe('user', () => {
} }
const groupController = new GroupController() const groupController = new GroupController()
const dbGroup = await groupController.createGroup(group) 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) const res = await request(app)
.get(`/SASjsApi/user/${userId}`) .get(`/SASjsApi/user/${userId}`)
@@ -691,10 +690,8 @@ describe('user', () => {
it('should respond with Not Found if userId is incorrect', async () => { it('should respond with Not Found if userId is incorrect', async () => {
await controller.createUser(user) await controller.createUser(user)
const hexValue = randomBytes(12).toString('hex')
const res = await request(app) const res = await request(app)
.get(`/SASjsApi/user/${hexValue}`) .get('/SASjsApi/user/1234')
.auth(adminAccessToken, { type: 'bearer' }) .auth(adminAccessToken, { type: 'bearer' })
.send() .send()
.expect(404) .expect(404)
@@ -706,7 +703,7 @@ describe('user', () => {
describe('by username', () => { describe('by username', () => {
it('should respond with user autoExec when same user requests', async () => { it('should respond with user autoExec when same user requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.uid const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId) const accessToken = await generateAndSaveToken(userId)
const res = await request(app) const res = await request(app)
@@ -806,13 +803,13 @@ describe('user', () => {
expect(res.body).toEqual([ expect(res.body).toEqual([
{ {
uid: expect.anything(), id: expect.anything(),
username: adminUser.username, username: adminUser.username,
displayName: adminUser.displayName, displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin isAdmin: adminUser.isAdmin
}, },
{ {
uid: expect.anything(), id: expect.anything(),
username: user.username, username: user.username,
displayName: user.displayName, displayName: user.displayName,
isAdmin: user.isAdmin isAdmin: user.isAdmin
@@ -834,13 +831,13 @@ describe('user', () => {
expect(res.body).toEqual([ expect(res.body).toEqual([
{ {
uid: expect.anything(), id: expect.anything(),
username: adminUser.username, username: adminUser.username,
displayName: adminUser.displayName, displayName: adminUser.displayName,
isAdmin: adminUser.isAdmin isAdmin: adminUser.isAdmin
}, },
{ {
uid: expect.anything(), id: expect.anything(),
username: 'randomUser', username: 'randomUser',
displayName: user.displayName, displayName: user.displayName,
isAdmin: user.isAdmin isAdmin: user.isAdmin
@@ -862,10 +859,10 @@ const generateSaveTokenAndCreateUser = async (
): Promise<string> => { ): Promise<string> => {
const dbUser = await controller.createUser(someUser ?? adminUser) 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({ const adminAccessToken = generateAccessToken({
clientId, clientId,
userId userId
+1 -1
View File
@@ -145,7 +145,7 @@ describe('web', () => {
expect(res.body.loggedIn).toBeTruthy() expect(res.body.loggedIn).toBeTruthy()
expect(res.body.user).toEqual({ expect(res.body.user).toEqual({
uid: expect.any(String), id: expect.any(Number),
username: user.username, username: user.username,
displayName: user.displayName, displayName: user.displayName,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
+9 -21
View File
@@ -9,7 +9,6 @@ import {
deleteUserValidation, deleteUserValidation,
getUserValidation, getUserValidation,
registerUserValidation, registerUserValidation,
uidValidation,
updateUserValidation updateUserValidation
} from '../../utils' } from '../../utils'
@@ -57,15 +56,12 @@ userRouter.get(
} }
) )
userRouter.get('/:uid', authenticateAccessToken, async (req, res) => { userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
const { error, value: params } = uidValidation(req.params) const { userId } = req.params
if (error) return res.status(400).send(error.details[0].message)
const { uid } = params
const controller = new UserController() const controller = new UserController()
try { try {
const response = await controller.getUser(req, uid) const response = await controller.getUser(req, parseInt(userId))
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -101,16 +97,12 @@ userRouter.patch(
) )
userRouter.patch( userRouter.patch(
'/:uid', '/:userId',
authenticateAccessToken, authenticateAccessToken,
verifyAdminIfNeeded, verifyAdminIfNeeded,
async (req, res) => { async (req, res) => {
const { user } = req const { user } = req
const { userId } = req.params
const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
// only an admin can update `isActive` and `isAdmin` fields // only an admin can update `isActive` and `isAdmin` fields
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin) const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
@@ -118,7 +110,7 @@ userRouter.patch(
const controller = new UserController() const controller = new UserController()
try { try {
const response = await controller.updateUser(uid, body) const response = await controller.updateUser(parseInt(userId), body)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
@@ -155,16 +147,12 @@ userRouter.delete(
) )
userRouter.delete( userRouter.delete(
'/:uid', '/:userId',
authenticateAccessToken, authenticateAccessToken,
verifyAdminIfNeeded, verifyAdminIfNeeded,
async (req, res) => { async (req, res) => {
const { user } = req const { user } = req
const { userId } = req.params
const { error: uidError, value: params } = uidValidation(req.params)
if (uidError) return res.status(400).send(uidError.details[0].message)
const { uid } = params
// only an admin can delete user without providing password // only an admin can delete user without providing password
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin) const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
@@ -172,7 +160,7 @@ userRouter.delete(
const controller = new UserController() const controller = new UserController()
try { try {
await controller.deleteUser(uid, data, user!.isAdmin) await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
res.status(200).send('Account Deleted!') res.status(200).send('Account Deleted!')
} catch (err: any) { } catch (err: any) {
res.status(err.code).send(err.message) res.status(err.code).send(err.message)
+1 -1
View File
@@ -1,4 +1,4 @@
export interface InfoJWT { export interface InfoJWT {
clientId: string clientId: string
userId: string userId: number
} }
+1 -1
View File
@@ -1,6 +1,6 @@
export interface PreProgramVars { export interface PreProgramVars {
username: string username: string
userId: string userId: number
displayName: string displayName: string
serverUrl: string serverUrl: string
httpHeaders: string[] httpHeaders: string[]
+1 -1
View File
@@ -1,5 +1,5 @@
export interface RequestUser { export interface RequestUser {
userId: string userId: number
clientId: string clientId: string
username: string username: string
displayName: string displayName: string
-4
View File
@@ -1,4 +0,0 @@
import { randomBytes } from 'crypto'
export const randomBytesHexString = (bytesCount: number) =>
randomBytes(bytesCount).toString('hex')
+1 -1
View File
@@ -22,7 +22,7 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
//So this is workaround. //So this is workaround.
return { return {
username: user ? user.username : 'demo', username: user ? user.username : 'demo',
userId: user ? user.userId : 'demoId', userId: user ? user.userId : 0,
displayName: user ? user.displayName : 'demo', displayName: user ? user.displayName : 'demo',
serverUrl: protocol + host, serverUrl: protocol + host,
httpHeaders httpHeaders
+15
View File
@@ -0,0 +1,15 @@
import Counter from '../model/Counter'
export const getSequenceNextValue = async (seqName: string) => {
const seqDoc = await Counter.findOne({ id: seqName })
if (!seqDoc) {
await Counter.create({ id: seqName, seq: 1 })
return 1
}
seqDoc.seq += 1
await seqDoc.save()
return seqDoc.seq
}
+3 -3
View File
@@ -5,7 +5,7 @@ import { InfoJWT } from '../types/InfoJWT'
const isValidToken = async ( const isValidToken = async (
token: string, token: string,
key: string, key: string,
userId: string, userId: number,
clientId: string clientId: string
) => { ) => {
const promise = new Promise<boolean>((resolve, reject) => const promise = new Promise<boolean>((resolve, reject) =>
@@ -24,8 +24,8 @@ const isValidToken = async (
return await promise.then(() => true).catch(() => false) return await promise.then(() => true).catch(() => false)
} }
export const getTokensFromDB = async (userId: string, clientId: string) => { export const getTokensFromDB = async (userId: number, clientId: string) => {
const user = await User.findOne({ _id: userId }) const user = await User.findOne({ id: userId })
if (!user) return if (!user) return
const currentTokenObj = user.tokens.find( const currentTokenObj = user.tokens.find(
+1 -1
View File
@@ -2,7 +2,6 @@ export * from './appStreamConfig'
export * from './connectDB' export * from './connectDB'
export * from './copySASjsCore' export * from './copySASjsCore'
export * from './createWeboutSasFile' export * from './createWeboutSasFile'
export * from './crypto'
export * from './desktopAutoExec' export * from './desktopAutoExec'
export * from './extractHeaders' export * from './extractHeaders'
export * from './extractName' export * from './extractName'
@@ -15,6 +14,7 @@ export * from './getCertificates'
export * from './getDesktopFields' export * from './getDesktopFields'
export * from './getPreProgramVariables' export * from './getPreProgramVariables'
export * from './getRunTimeAndFilePath' export * from './getRunTimeAndFilePath'
export * from './getSequenceNextValue'
export * from './getServerUrl' export * from './getServerUrl'
export * from './getTokensFromDB' export * from './getTokensFromDB'
export * from './instantiateLogger' export * from './instantiateLogger'
+1 -1
View File
@@ -22,7 +22,7 @@ export const isPublicRoute = async (req: Request): Promise<boolean> => {
} }
export const publicUser: RequestUser = { export const publicUser: RequestUser = {
userId: 'public_user_id', userId: 0,
clientId: 'public_app', clientId: 'public_app',
username: 'publicUser', username: 'publicUser',
displayName: 'Public User', displayName: 'Public User',
+2 -2
View File
@@ -1,7 +1,7 @@
import User from '../model/User' import User from '../model/User'
export const removeTokensInDB = async (userId: string, clientId: string) => { export const removeTokensInDB = async (userId: number, clientId: string) => {
const user = await User.findOne({ _id: userId }) const user = await User.findOne({ id: userId })
if (!user) return if (!user) return
const tokenObjIndex = user.tokens.findIndex( const tokenObjIndex = user.tokens.findIndex(
+2 -2
View File
@@ -1,12 +1,12 @@
import User from '../model/User' import User from '../model/User'
export const saveTokensInDB = async ( export const saveTokensInDB = async (
userId: string, userId: number,
clientId: string, clientId: string,
accessToken: string, accessToken: string,
refreshToken: string refreshToken: string
) => { ) => {
const user = await User.findOne({ _id: userId }) const user = await User.findOne({ id: userId })
if (!user) return if (!user) return
const currentTokenObj = user.tokens.find( const currentTokenObj = user.tokens.find(
+1 -1
View File
@@ -82,7 +82,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
} }
export const ALL_USERS_GROUP = { export const ALL_USERS_GROUP = {
name: 'all-users', name: 'AllUsers',
description: 'Group contains all users' description: 'Group contains all users'
} }
+1 -6
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 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 => export const getUserValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
username: usernameSchema.required() username: usernameSchema.required()
@@ -118,7 +113,7 @@ export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
principalType: Joi.string() principalType: Joi.string()
.required() .required()
.valid(...Object.values(PrincipalType)), .valid(...Object.values(PrincipalType)),
principalId: Joi.string().length(24).hex().required() principalId: Joi.number().required()
}).validate(data) }).validate(data)
export const updatePermissionValidation = (data: any): Joi.ValidationResult => export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
+4 -4
View File
@@ -4,7 +4,7 @@ import { RequestUser } from '../types'
export const fetchLatestAutoExec = async ( export const fetchLatestAutoExec = async (
reqUser: RequestUser reqUser: RequestUser
): Promise<RequestUser | undefined> => { ): Promise<RequestUser | undefined> => {
const dbUser = await User.findOne({ _id: reqUser.userId }) const dbUser = await User.findOne({ id: reqUser.userId })
if (!dbUser) return undefined if (!dbUser) return undefined
@@ -21,12 +21,12 @@ export const fetchLatestAutoExec = async (
} }
export const verifyTokenInDB = async ( export const verifyTokenInDB = async (
userId: string, userId: number,
clientId: string, clientId: string,
token: string, token: string,
tokenType: 'accessToken' | 'refreshToken' tokenType: 'accessToken' | 'refreshToken'
): Promise<RequestUser | undefined> => { ): Promise<RequestUser | undefined> => {
const dbUser = await User.findOne({ _id: userId }) const dbUser = await User.findOne({ id: userId })
if (!dbUser) return undefined if (!dbUser) return undefined
@@ -36,7 +36,7 @@ export const verifyTokenInDB = async (
return currentTokenObj?.[tokenType] === token return currentTokenObj?.[tokenType] === token
? { ? {
userId: dbUser.uid, userId: dbUser.id,
clientId, clientId,
username: dbUser.username, username: dbUser.username,
displayName: dbUser.displayName, displayName: dbUser.displayName,
+1 -1
View File
@@ -40,7 +40,7 @@ const Login = () => {
}) })
if (loggedIn) { if (loggedIn) {
appContext.setUserId?.(user.uid) appContext.setUserId?.(user.id)
appContext.setUsername?.(user.username) appContext.setUsername?.(user.username)
appContext.setDisplayName?.(user.displayName) appContext.setDisplayName?.(user.displayName)
appContext.setIsAdmin?.(user.isAdmin) appContext.setIsAdmin?.(user.isAdmin)
@@ -99,8 +99,8 @@ const AddPermissionModal = ({
principalType: principalType.toLowerCase(), principalType: principalType.toLowerCase(),
principalId: principalId:
principalType.toLowerCase() === 'user' principalType.toLowerCase() === 'user'
? userPrincipal?.uid ? userPrincipal?.id
: groupPrincipal?.uid : groupPrincipal?.groupId
} }
permissions.push(addPermissionPayload) permissions.push(addPermissionPayload)
@@ -61,7 +61,7 @@ const PermissionTable = ({
</TableHead> </TableHead>
<TableBody> <TableBody>
{permissions.map((permission) => ( {permissions.map((permission) => (
<TableRow key={permission.uid}> <TableRow key={permission.permissionId}>
<BootstrapTableCell>{permission.path}</BootstrapTableCell> <BootstrapTableCell>{permission.path}</BootstrapTableCell>
<BootstrapTableCell>{permission.type}</BootstrapTableCell> <BootstrapTableCell>{permission.type}</BootstrapTableCell>
<BootstrapTableCell> <BootstrapTableCell>
@@ -69,7 +69,7 @@ const useAddPermission = () => {
for (const permission of updatingPermissions) { for (const permission of updatingPermissions) {
await axios await axios
.patch(`/SASjsApi/permission/${permission.uid}`, { .patch(`/SASjsApi/permission/${permission.permissionId}`, {
setting: permission.setting === 'Grant' ? 'Deny' : 'Grant' setting: permission.setting === 'Grant' ? 'Deny' : 'Grant'
}) })
.then((res) => { .then((res) => {
@@ -24,7 +24,7 @@ const useDeletePermissionModal = () => {
setDeleteConfirmationModalOpen(false) setDeleteConfirmationModalOpen(false)
setIsLoading(true) setIsLoading(true)
axios axios
.delete(`/SASjsApi/permission/${selectedPermission?.uid}`) .delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
.then((res: any) => { .then((res: any) => {
fetchPermissions() fetchPermissions()
setSnackbarMessage('Permission deleted!') setSnackbarMessage('Permission deleted!')
@@ -62,17 +62,21 @@ const useFilterPermissions = () => {
: permissions : permissions
let filteredArray = uriFilteredPermissions.filter((permission) => let filteredArray = uriFilteredPermissions.filter((permission) =>
principalFilteredPermissions.some((item) => item.uid === permission.uid) principalFilteredPermissions.some(
(item) => item.permissionId === permission.permissionId
)
) )
filteredArray = filteredArray.filter((permission) => filteredArray = filteredArray.filter((permission) =>
principalTypeFilteredPermissions.some( principalTypeFilteredPermissions.some(
(item) => item.uid === permission.uid (item) => item.permissionId === permission.permissionId
) )
) )
filteredArray = filteredArray.filter((permission) => filteredArray = filteredArray.filter((permission) =>
settingFilteredPermissions.some((item) => item.uid === permission.uid) settingFilteredPermissions.some(
(item) => item.permissionId === permission.permissionId
)
) )
setFilteredPermissions(filteredArray) setFilteredPermissions(filteredArray)
@@ -24,7 +24,7 @@ const useUpdatePermissionModal = () => {
setUpdatePermissionModalOpen(false) setUpdatePermissionModalOpen(false)
setIsLoading(true) setIsLoading(true)
axios axios
.patch(`/SASjsApi/permission/${selectedPermission?.uid}`, { .patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, {
setting setting
}) })
.then((res: any) => { .then((res: any) => {
-2
View File
@@ -26,7 +26,6 @@ const Profile = () => {
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false) const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
useEffect(() => { useEffect(() => {
if (appContext.userId) {
setIsLoading(true) setIsLoading(true)
axios axios
.get(`/SASjsApi/user/${appContext.userId}`) .get(`/SASjsApi/user/${appContext.userId}`)
@@ -39,7 +38,6 @@ const Profile = () => {
.finally(() => { .finally(() => {
setIsLoading(false) setIsLoading(false)
}) })
}
}, [appContext.userId]) }, [appContext.userId])
const handleChange = (event: any) => { const handleChange = (event: any) => {
+19 -12
View File
@@ -24,32 +24,39 @@ export enum RunTimeType {
interface AppContextProps { interface AppContextProps {
checkingSession: boolean checkingSession: boolean
loggedIn: boolean loggedIn: boolean
setLoggedIn?: Dispatch<SetStateAction<boolean>> setLoggedIn: Dispatch<SetStateAction<boolean>> | null
needsToUpdatePassword: boolean needsToUpdatePassword: boolean
setNeedsToUpdatePassword?: Dispatch<SetStateAction<boolean>> setNeedsToUpdatePassword: Dispatch<SetStateAction<boolean>> | null
userId?: string userId: number
setUserId?: Dispatch<SetStateAction<string | undefined>> setUserId: Dispatch<SetStateAction<number>> | null
username: string username: string
setUsername?: Dispatch<SetStateAction<string>> setUsername: Dispatch<SetStateAction<string>> | null
displayName: string displayName: string
setDisplayName?: Dispatch<SetStateAction<string>> setDisplayName: Dispatch<SetStateAction<string>> | null
isAdmin: boolean isAdmin: boolean
setIsAdmin?: Dispatch<SetStateAction<boolean>> setIsAdmin: Dispatch<SetStateAction<boolean>> | null
mode: ModeType mode: ModeType
runTimes: RunTimeType[] runTimes: RunTimeType[]
logout?: () => void logout: (() => void) | null
} }
export const AppContext = createContext<AppContextProps>({ export const AppContext = createContext<AppContextProps>({
checkingSession: false, checkingSession: false,
loggedIn: false, loggedIn: false,
setLoggedIn: null,
needsToUpdatePassword: false, needsToUpdatePassword: false,
userId: '', setNeedsToUpdatePassword: null,
userId: 0,
setUserId: null,
username: '', username: '',
setUsername: null,
displayName: '', displayName: '',
setDisplayName: null,
isAdmin: false, isAdmin: false,
setIsAdmin: null,
mode: ModeType.Server, mode: ModeType.Server,
runTimes: [] runTimes: [],
logout: null
}) })
const AppContextProvider = (props: { children: ReactNode }) => { const AppContextProvider = (props: { children: ReactNode }) => {
@@ -57,7 +64,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
const [checkingSession, setCheckingSession] = useState(false) const [checkingSession, setCheckingSession] = useState(false)
const [loggedIn, setLoggedIn] = useState(false) const [loggedIn, setLoggedIn] = useState(false)
const [needsToUpdatePassword, setNeedsToUpdatePassword] = useState(false) const [needsToUpdatePassword, setNeedsToUpdatePassword] = useState(false)
const [userId, setUserId] = useState<string>() const [userId, setUserId] = useState(0)
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('') const [displayName, setDisplayName] = useState('')
const [isAdmin, setIsAdmin] = useState(false) const [isAdmin, setIsAdmin] = useState(false)
@@ -72,7 +79,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
.then((res) => res.data) .then((res) => res.data)
.then((data: any) => { .then((data: any) => {
setCheckingSession(false) setCheckingSession(false)
setUserId(data.uid) setUserId(data.id)
setUsername(data.username) setUsername(data.username)
setDisplayName(data.displayName) setDisplayName(data.displayName)
setIsAdmin(data.isAdmin) setIsAdmin(data.isAdmin)
+4 -4
View File
@@ -6,13 +6,13 @@ export const findExistingPermission = (
) => { ) => {
for (const permission of existingPermissions) { for (const permission of existingPermissions) {
if ( if (
permission.user?.uid === newPermission.principalId && permission.user?.id === newPermission.principalId &&
hasSameCombination(permission, newPermission) hasSameCombination(permission, newPermission)
) )
return permission return permission
if ( if (
permission.group?.uid === newPermission.principalId && permission.group?.groupId === newPermission.principalId &&
hasSameCombination(permission, newPermission) hasSameCombination(permission, newPermission)
) )
return permission return permission
@@ -27,13 +27,13 @@ export const findUpdatingPermission = (
) => { ) => {
for (const permission of existingPermissions) { for (const permission of existingPermissions) {
if ( if (
permission.user?.uid === newPermission.principalId && permission.user?.id === newPermission.principalId &&
hasDifferentSetting(permission, newPermission) hasDifferentSetting(permission, newPermission)
) )
return permission return permission
if ( if (
permission.group?.uid === newPermission.principalId && permission.group?.groupId === newPermission.principalId &&
hasDifferentSetting(permission, newPermission) hasDifferentSetting(permission, newPermission)
) )
return permission return permission
+4 -4
View File
@@ -1,12 +1,12 @@
export interface UserResponse { export interface UserResponse {
uid: string id: number
username: string username: string
displayName: string displayName: string
isAdmin: boolean isAdmin: boolean
} }
export interface GroupResponse { export interface GroupResponse {
uid: string groupId: number
name: string name: string
description: string description: string
} }
@@ -17,7 +17,7 @@ export interface GroupDetailsResponse extends GroupResponse {
} }
export interface PermissionResponse { export interface PermissionResponse {
uid: string permissionId: number
path: string path: string
type: string type: string
setting: string setting: string
@@ -30,7 +30,7 @@ export interface RegisterPermissionPayload {
type: string type: string
setting: string setting: string
principalType: string principalType: string
principalId: string principalId: number
} }
export interface TreeNode { export interface TreeNode {