mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78bea7c154 | ||
|
|
9c3b155c12 | ||
|
|
98e501334f | ||
|
|
bbfd53e79e | ||
| 254bc07da7 | |||
| f978814ca7 | |||
| 68515f95a6 | |||
| d3a516c36e | |||
| c3e3befc17 | |||
|
|
275de9478e | ||
|
|
1a3ef62cb2 | ||
|
|
9eb5f3ca4d | ||
|
|
916947dffa | ||
| 79b7827b7c | |||
| 37e1aa9b61 | |||
| 7e504008b7 | |||
| 5d5a9d3788 | |||
|
|
7c79d6479c | ||
|
|
3e635f422a | ||
|
|
77db14c690 | ||
| b7dff341f0 | |||
| 8a3054e19a | |||
|
|
a531de2adb | ||
|
|
c458d94493 | ||
| 706e228a8e | |||
| 7681722e5a | |||
| 8de032b543 | |||
|
|
998ef213e9 | ||
|
|
f8b0f98678 | ||
| 9640f65264 | |||
| c574b42235 | |||
| 468d1a929d | |||
| 7cdffe30e3 | |||
| 3b1fcb937d | |||
| 3c987c61dd | |||
| 0a780697da | |||
| 83d819df53 |
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,3 +1,63 @@
|
|||||||
|
# [0.14.0](https://github.com/sasjs/server/compare/v0.13.3...v0.14.0) (2022-08-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add restriction on add/remove user to public group ([d3a516c](https://github.com/sasjs/server/commit/d3a516c36e45aa1cc76c30c744e6a0e5bd553165))
|
||||||
|
* call jwt.verify in synchronous way ([254bc07](https://github.com/sasjs/server/commit/254bc07da744a9708109bfb792be70aa3f6284f4))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add public group to DB on seed ([c3e3bef](https://github.com/sasjs/server/commit/c3e3befc17102ee1754e1403193040b4f79fb2a7))
|
||||||
|
* bypass authentication when route is enabled for public group ([68515f9](https://github.com/sasjs/server/commit/68515f95a65d422e29c0ed6028f3ea0ae8d9b1bf))
|
||||||
|
|
||||||
|
## [0.13.3](https://github.com/sasjs/server/compare/v0.13.2...v0.13.3) (2022-08-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* show non-admin user his own permissions only ([8a3054e](https://github.com/sasjs/server/commit/8a3054e19ade82e2792cfb0f2a8af9e502c5eb52))
|
||||||
|
* update schema of Permission ([5d5a9d3](https://github.com/sasjs/server/commit/5d5a9d3788281d75c56f68f0dff231abc9c9c275))
|
||||||
|
|
||||||
|
## [0.13.2](https://github.com/sasjs/server/compare/v0.13.1...v0.13.2) (2022-08-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding ls=max to reduce log size and improve readability ([916947d](https://github.com/sasjs/server/commit/916947dffacd902ff23ac3e899d1bf5ab6238b75))
|
||||||
|
|
||||||
|
## [0.13.1](https://github.com/sasjs/server/compare/v0.13.0...v0.13.1) (2022-07-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding options to prevent unwanted windows on windows. Closes [#244](https://github.com/sasjs/server/issues/244) ([77db14c](https://github.com/sasjs/server/commit/77db14c690e18145d733ac2b0d646ab0dbe4d521))
|
||||||
|
|
||||||
|
# [0.13.0](https://github.com/sasjs/server/compare/v0.12.1...v0.13.0) (2022-07-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* autofocus input field and submit on enter ([7681722](https://github.com/sasjs/server/commit/7681722e5afdc2df0c9eed201b05add3beda92a7))
|
||||||
|
* move api button to user menu ([8de032b](https://github.com/sasjs/server/commit/8de032b5431b47daabcf783c47ff078bf817247d))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add action and command to editor ([706e228](https://github.com/sasjs/server/commit/706e228a8e1924786fd9dc97de387974eda504b1))
|
||||||
|
|
||||||
|
## [0.12.1](https://github.com/sasjs/server/compare/v0.12.0...v0.12.1) (2022-07-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** disable launch icon button when file content is not saved ([c574b42](https://github.com/sasjs/server/commit/c574b4223591c4a6cd3ef5e146ce99cd8f7c9190))
|
||||||
|
* **web:** saveAs functionality fixed in studio page ([3c987c6](https://github.com/sasjs/server/commit/3c987c61ddc258f991e2bf38c1f16a0c4248d6ae))
|
||||||
|
* **web:** show original name as default name in rename file/folder modal ([9640f65](https://github.com/sasjs/server/commit/9640f6526496f3564664ccb1f834d0f659dcad4e))
|
||||||
|
* **web:** webout tab item fixed in studio page ([7cdffe3](https://github.com/sasjs/server/commit/7cdffe30e36e5cad0284f48ea97925958e12704c))
|
||||||
|
* **web:** when no file is selected save the editor content to local storage ([3b1fcb9](https://github.com/sasjs/server/commit/3b1fcb937d06d02ab99c9e8dbe307012d48a7a3a))
|
||||||
|
|
||||||
# [0.12.0](https://github.com/sasjs/server/compare/v0.11.5...v0.12.0) (2022-07-26)
|
# [0.12.0](https://github.com/sasjs/server/compare/v0.11.5...v0.12.0) (2022-07-26)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -470,12 +470,89 @@ components:
|
|||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
AuthorizedRoutesResponse:
|
AuthorizedRoutesResponse:
|
||||||
properties:
|
properties:
|
||||||
URIs:
|
paths:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
- URIs
|
- paths
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
PermissionDetailsResponse:
|
||||||
|
properties:
|
||||||
|
permissionId:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
setting:
|
||||||
|
type: string
|
||||||
|
user:
|
||||||
|
$ref: '#/components/schemas/UserResponse'
|
||||||
|
group:
|
||||||
|
$ref: '#/components/schemas/GroupDetailsResponse'
|
||||||
|
required:
|
||||||
|
- permissionId
|
||||||
|
- path
|
||||||
|
- type
|
||||||
|
- setting
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
PermissionType:
|
||||||
|
enum:
|
||||||
|
- Route
|
||||||
|
type: string
|
||||||
|
PermissionSettingForRoute:
|
||||||
|
enum:
|
||||||
|
- Grant
|
||||||
|
- Deny
|
||||||
|
type: string
|
||||||
|
PrincipalType:
|
||||||
|
enum:
|
||||||
|
- user
|
||||||
|
- group
|
||||||
|
type: string
|
||||||
|
RegisterPermissionPayload:
|
||||||
|
properties:
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
description: 'Name of affected resource'
|
||||||
|
example: /SASjsApi/code/execute
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/PermissionType'
|
||||||
|
description: 'Type of affected resource'
|
||||||
|
example: Route
|
||||||
|
setting:
|
||||||
|
$ref: '#/components/schemas/PermissionSettingForRoute'
|
||||||
|
description: 'The indication of whether (and to what extent) access is provided'
|
||||||
|
example: Grant
|
||||||
|
principalType:
|
||||||
|
$ref: '#/components/schemas/PrincipalType'
|
||||||
|
description: 'Indicates the type of principal'
|
||||||
|
example: user
|
||||||
|
principalId:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: 'The id of user or group to which a rule is assigned.'
|
||||||
|
example: 123
|
||||||
|
required:
|
||||||
|
- path
|
||||||
|
- type
|
||||||
|
- setting
|
||||||
|
- principalType
|
||||||
|
- principalId
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
UpdatePermissionPayload:
|
||||||
|
properties:
|
||||||
|
setting:
|
||||||
|
$ref: '#/components/schemas/PermissionSettingForRoute'
|
||||||
|
description: 'The indication of whether (and to what extent) access is provided'
|
||||||
|
example: Grant
|
||||||
|
required:
|
||||||
|
- setting
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
ExecuteReturnJsonPayload:
|
ExecuteReturnJsonPayload:
|
||||||
@@ -521,71 +598,6 @@ components:
|
|||||||
- clientId
|
- clientId
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
PermissionDetailsResponse:
|
|
||||||
properties:
|
|
||||||
permissionId:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
uri:
|
|
||||||
type: string
|
|
||||||
setting:
|
|
||||||
type: string
|
|
||||||
user:
|
|
||||||
$ref: '#/components/schemas/UserResponse'
|
|
||||||
group:
|
|
||||||
$ref: '#/components/schemas/GroupDetailsResponse'
|
|
||||||
required:
|
|
||||||
- permissionId
|
|
||||||
- uri
|
|
||||||
- setting
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
PermissionSetting:
|
|
||||||
enum:
|
|
||||||
- Grant
|
|
||||||
- Deny
|
|
||||||
type: string
|
|
||||||
PrincipalType:
|
|
||||||
enum:
|
|
||||||
- user
|
|
||||||
- group
|
|
||||||
type: string
|
|
||||||
RegisterPermissionPayload:
|
|
||||||
properties:
|
|
||||||
uri:
|
|
||||||
type: string
|
|
||||||
description: 'Name of affected resource'
|
|
||||||
example: /SASjsApi/code/execute
|
|
||||||
setting:
|
|
||||||
$ref: '#/components/schemas/PermissionSetting'
|
|
||||||
description: 'The indication of whether (and to what extent) access is provided'
|
|
||||||
example: Grant
|
|
||||||
principalType:
|
|
||||||
$ref: '#/components/schemas/PrincipalType'
|
|
||||||
description: 'Indicates the type of principal'
|
|
||||||
example: user
|
|
||||||
principalId:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
description: 'The id of user or group to which a rule is assigned.'
|
|
||||||
example: 123
|
|
||||||
required:
|
|
||||||
- uri
|
|
||||||
- setting
|
|
||||||
- principalType
|
|
||||||
- principalId
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
UpdatePermissionPayload:
|
|
||||||
properties:
|
|
||||||
setting:
|
|
||||||
$ref: '#/components/schemas/PermissionSetting'
|
|
||||||
description: 'The indication of whether (and to what extent) access is provided'
|
|
||||||
example: Grant
|
|
||||||
required:
|
|
||||||
- setting
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
type: http
|
type: http
|
||||||
@@ -1598,12 +1610,165 @@ paths:
|
|||||||
$ref: '#/components/schemas/AuthorizedRoutesResponse'
|
$ref: '#/components/schemas/AuthorizedRoutesResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: { URIs: [/AppStream, /SASjsApi/stp/execute] }
|
value: { paths: [/AppStream, /SASjsApi/stp/execute] }
|
||||||
summary: 'Get authorized routes.'
|
summary: 'Get the list of available routes to which permissions can be applied. Used to populate the dialog in the URI Permissions feature.'
|
||||||
tags:
|
tags:
|
||||||
- Info
|
- Info
|
||||||
security: []
|
security: []
|
||||||
parameters: []
|
parameters: []
|
||||||
|
/SASjsApi/permission:
|
||||||
|
get:
|
||||||
|
operationId: GetAllPermissions
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/PermissionDetailsResponse'
|
||||||
|
type: array
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
permissionId: 123,
|
||||||
|
path: /SASjsApi/code/execute,
|
||||||
|
type: Route,
|
||||||
|
setting: Grant,
|
||||||
|
user:
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
username: johnSnow01,
|
||||||
|
displayName: 'John Snow',
|
||||||
|
isAdmin: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
permissionId: 124,
|
||||||
|
path: /SASjsApi/code/execute,
|
||||||
|
type: Route,
|
||||||
|
setting: Grant,
|
||||||
|
group:
|
||||||
|
{
|
||||||
|
groupId: 1,
|
||||||
|
name: DCGroup,
|
||||||
|
description: 'This group represents Data Controller Users',
|
||||||
|
isActive: true,
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
description: "Get the list of permission rules applicable the authenticated user.\nIf the user is an admin, all rules are returned."
|
||||||
|
summary: 'Get the list of permission rules. If the user is admin, all rules are returned.'
|
||||||
|
tags:
|
||||||
|
- Permission
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
post:
|
||||||
|
operationId: CreatePermission
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PermissionDetailsResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value:
|
||||||
|
{
|
||||||
|
permissionId: 123,
|
||||||
|
path: /SASjsApi/code/execute,
|
||||||
|
type: Route,
|
||||||
|
setting: Grant,
|
||||||
|
user:
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
username: johnSnow01,
|
||||||
|
displayName: 'John Snow',
|
||||||
|
isAdmin: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
summary: 'Create a new permission. Admin only.'
|
||||||
|
tags:
|
||||||
|
- Permission
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/RegisterPermissionPayload'
|
||||||
|
'/SASjsApi/permission/{permissionId}':
|
||||||
|
patch:
|
||||||
|
operationId: UpdatePermission
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PermissionDetailsResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value:
|
||||||
|
{
|
||||||
|
permissionId: 123,
|
||||||
|
path: /SASjsApi/code/execute,
|
||||||
|
type: Route,
|
||||||
|
setting: Grant,
|
||||||
|
user:
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
username: johnSnow01,
|
||||||
|
displayName: 'John Snow',
|
||||||
|
isAdmin: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
summary: 'Update permission setting. Admin only'
|
||||||
|
tags:
|
||||||
|
- Permission
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- description: "The permission's identifier"
|
||||||
|
in: path
|
||||||
|
name: permissionId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
format: double
|
||||||
|
type: number
|
||||||
|
example: 1234
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UpdatePermissionPayload'
|
||||||
|
delete:
|
||||||
|
operationId: DeletePermission
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: 'No content'
|
||||||
|
summary: 'Delete a permission. Admin only.'
|
||||||
|
tags:
|
||||||
|
- Permission
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- description: "The user's identifier"
|
||||||
|
in: path
|
||||||
|
name: permissionId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
format: double
|
||||||
|
type: number
|
||||||
|
example: 1234
|
||||||
/SASjsApi/session:
|
/SASjsApi/session:
|
||||||
get:
|
get:
|
||||||
operationId: Session
|
operationId: Session
|
||||||
@@ -1788,154 +1953,6 @@ paths:
|
|||||||
- Web
|
- Web
|
||||||
security: []
|
security: []
|
||||||
parameters: []
|
parameters: []
|
||||||
/SASjsApi/permission:
|
|
||||||
get:
|
|
||||||
operationId: GetAllPermissions
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/PermissionDetailsResponse'
|
|
||||||
type: array
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value:
|
|
||||||
[
|
|
||||||
{
|
|
||||||
permissionId: 123,
|
|
||||||
uri: /SASjsApi/code/execute,
|
|
||||||
setting: Grant,
|
|
||||||
user:
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
username: johnSnow01,
|
|
||||||
displayName: 'John Snow',
|
|
||||||
isAdmin: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permissionId: 124,
|
|
||||||
uri: /SASjsApi/code/execute,
|
|
||||||
setting: Grant,
|
|
||||||
group:
|
|
||||||
{
|
|
||||||
groupId: 1,
|
|
||||||
name: DCGroup,
|
|
||||||
description: 'This group represents Data Controller Users',
|
|
||||||
isActive: true,
|
|
||||||
users: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
summary: 'Get list of all permissions (uri, setting and userDetail).'
|
|
||||||
tags:
|
|
||||||
- Permission
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters: []
|
|
||||||
post:
|
|
||||||
operationId: CreatePermission
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/PermissionDetailsResponse'
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value:
|
|
||||||
{
|
|
||||||
permissionId: 123,
|
|
||||||
uri: /SASjsApi/code/execute,
|
|
||||||
setting: Grant,
|
|
||||||
user:
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
username: johnSnow01,
|
|
||||||
displayName: 'John Snow',
|
|
||||||
isAdmin: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
summary: 'Create a new permission. Admin only.'
|
|
||||||
tags:
|
|
||||||
- Permission
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/RegisterPermissionPayload'
|
|
||||||
'/SASjsApi/permission/{permissionId}':
|
|
||||||
patch:
|
|
||||||
operationId: UpdatePermission
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/PermissionDetailsResponse'
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value:
|
|
||||||
{
|
|
||||||
permissionId: 123,
|
|
||||||
uri: /SASjsApi/code/execute,
|
|
||||||
setting: Grant,
|
|
||||||
user:
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
username: johnSnow01,
|
|
||||||
displayName: 'John Snow',
|
|
||||||
isAdmin: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
summary: 'Update permission setting. Admin only'
|
|
||||||
tags:
|
|
||||||
- Permission
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- description: "The permission's identifier"
|
|
||||||
in: path
|
|
||||||
name: permissionId
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
format: double
|
|
||||||
type: number
|
|
||||||
example: 1234
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/UpdatePermissionPayload'
|
|
||||||
delete:
|
|
||||||
operationId: DeletePermission
|
|
||||||
responses:
|
|
||||||
'204':
|
|
||||||
description: 'No content'
|
|
||||||
summary: 'Delete a permission. Admin only.'
|
|
||||||
tags:
|
|
||||||
- Permission
|
|
||||||
security:
|
|
||||||
- bearerAuth: []
|
|
||||||
parameters:
|
|
||||||
- description: "The user's identifier"
|
|
||||||
in: path
|
|
||||||
name: permissionId
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
format: double
|
|
||||||
type: number
|
|
||||||
example: 1234
|
|
||||||
servers:
|
servers:
|
||||||
- url: /
|
- url: /
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { apiRoot, sysInitCompiledPath } from '../src/utils/file'
|
|||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
const compiledSystemInit = async (systemInit: string) =>
|
const compiledSystemInit = async (systemInit: string) =>
|
||||||
'options ps=max;\n' +
|
'options ls=max ps=max;\n' +
|
||||||
(await loadDependenciesFile({
|
(await loadDependenciesFile({
|
||||||
fileContent: systemInit,
|
fileContent: systemInit,
|
||||||
type: SASJsFileType.job,
|
type: SASJsFileType.job,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Body
|
Body
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
|
|
||||||
import Group, { GroupPayload } from '../model/Group'
|
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import { UserResponse } from './user'
|
import { UserResponse } from './user'
|
||||||
|
|
||||||
@@ -241,6 +241,13 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'Group not found.'
|
message: 'Group not found.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (group.name === PUBLIC_GROUP_NAME)
|
||||||
|
throw {
|
||||||
|
code: 400,
|
||||||
|
status: 'Bad Request',
|
||||||
|
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||||
|
}
|
||||||
|
|
||||||
const user = await User.findOne({ id: userId })
|
const user = await User.findOne({ id: userId })
|
||||||
if (!user)
|
if (!user)
|
||||||
throw {
|
throw {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Route, Tags, Example, Get } from 'tsoa'
|
import { Route, Tags, Example, Get } from 'tsoa'
|
||||||
import { getAuthorizedRoutes } from '../utils'
|
import { getAuthorizedRoutes } from '../utils'
|
||||||
export interface AuthorizedRoutesResponse {
|
export interface AuthorizedRoutesResponse {
|
||||||
URIs: string[]
|
paths: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InfoResponse {
|
export interface InfoResponse {
|
||||||
@@ -42,16 +42,16 @@ export class InfoController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Get authorized routes.
|
* @summary Get the list of available routes to which permissions can be applied. Used to populate the dialog in the URI Permissions feature.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<AuthorizedRoutesResponse>({
|
@Example<AuthorizedRoutesResponse>({
|
||||||
URIs: ['/AppStream', '/SASjsApi/stp/execute']
|
paths: ['/AppStream', '/SASjsApi/stp/execute']
|
||||||
})
|
})
|
||||||
@Get('/authorizedRoutes')
|
@Get('/authorizedRoutes')
|
||||||
public authorizedRoutes(): AuthorizedRoutesResponse {
|
public authorizedRoutes(): AuthorizedRoutesResponse {
|
||||||
const response = {
|
const response = {
|
||||||
URIs: getAuthorizedRoutes()
|
paths: getAuthorizedRoutes()
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ ${autoExecContent}`
|
|||||||
autoExecPath,
|
autoExecPath,
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
||||||
isWindows() ? '-nologo' : ''
|
isWindows() ? '-nologo' : ''
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import express from 'express'
|
||||||
import {
|
import {
|
||||||
Security,
|
Security,
|
||||||
Route,
|
Route,
|
||||||
@@ -8,7 +9,8 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
Patch,
|
Patch,
|
||||||
Delete,
|
Delete,
|
||||||
Body
|
Body,
|
||||||
|
Request
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
|
|
||||||
import Permission from '../model/Permission'
|
import Permission from '../model/Permission'
|
||||||
@@ -17,12 +19,16 @@ import Group from '../model/Group'
|
|||||||
import { UserResponse } from './user'
|
import { UserResponse } from './user'
|
||||||
import { GroupDetailsResponse } from './group'
|
import { GroupDetailsResponse } from './group'
|
||||||
|
|
||||||
|
export enum PermissionType {
|
||||||
|
route = 'Route'
|
||||||
|
}
|
||||||
|
|
||||||
export enum PrincipalType {
|
export enum PrincipalType {
|
||||||
user = 'user',
|
user = 'user',
|
||||||
group = 'group'
|
group = 'group'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PermissionSetting {
|
export enum PermissionSettingForRoute {
|
||||||
grant = 'Grant',
|
grant = 'Grant',
|
||||||
deny = 'Deny'
|
deny = 'Deny'
|
||||||
}
|
}
|
||||||
@@ -32,12 +38,17 @@ interface RegisterPermissionPayload {
|
|||||||
* Name of affected resource
|
* Name of affected resource
|
||||||
* @example "/SASjsApi/code/execute"
|
* @example "/SASjsApi/code/execute"
|
||||||
*/
|
*/
|
||||||
uri: string
|
path: string
|
||||||
|
/**
|
||||||
|
* Type of affected resource
|
||||||
|
* @example "Route"
|
||||||
|
*/
|
||||||
|
type: PermissionType
|
||||||
/**
|
/**
|
||||||
* The indication of whether (and to what extent) access is provided
|
* The indication of whether (and to what extent) access is provided
|
||||||
* @example "Grant"
|
* @example "Grant"
|
||||||
*/
|
*/
|
||||||
setting: PermissionSetting
|
setting: PermissionSettingForRoute
|
||||||
/**
|
/**
|
||||||
* Indicates the type of principal
|
* Indicates the type of principal
|
||||||
* @example "user"
|
* @example "user"
|
||||||
@@ -55,12 +66,13 @@ interface UpdatePermissionPayload {
|
|||||||
* The indication of whether (and to what extent) access is provided
|
* The indication of whether (and to what extent) access is provided
|
||||||
* @example "Grant"
|
* @example "Grant"
|
||||||
*/
|
*/
|
||||||
setting: PermissionSetting
|
setting: PermissionSettingForRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PermissionDetailsResponse {
|
export interface PermissionDetailsResponse {
|
||||||
permissionId: number
|
permissionId: number
|
||||||
uri: string
|
path: string
|
||||||
|
type: string
|
||||||
setting: string
|
setting: string
|
||||||
user?: UserResponse
|
user?: UserResponse
|
||||||
group?: GroupDetailsResponse
|
group?: GroupDetailsResponse
|
||||||
@@ -71,13 +83,17 @@ export interface PermissionDetailsResponse {
|
|||||||
@Tags('Permission')
|
@Tags('Permission')
|
||||||
export class PermissionController {
|
export class PermissionController {
|
||||||
/**
|
/**
|
||||||
* @summary Get list of all permissions (uri, setting and userDetail).
|
* Get the list of permission rules applicable the authenticated user.
|
||||||
|
* If the user is an admin, all rules are returned.
|
||||||
|
*
|
||||||
|
* @summary Get the list of permission rules. If the user is admin, all rules are returned.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<PermissionDetailsResponse[]>([
|
@Example<PermissionDetailsResponse[]>([
|
||||||
{
|
{
|
||||||
permissionId: 123,
|
permissionId: 123,
|
||||||
uri: '/SASjsApi/code/execute',
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
setting: 'Grant',
|
setting: 'Grant',
|
||||||
user: {
|
user: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -88,7 +104,8 @@ export class PermissionController {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
permissionId: 124,
|
permissionId: 124,
|
||||||
uri: '/SASjsApi/code/execute',
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
setting: 'Grant',
|
setting: 'Grant',
|
||||||
group: {
|
group: {
|
||||||
groupId: 1,
|
groupId: 1,
|
||||||
@@ -100,8 +117,10 @@ export class PermissionController {
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
@Get('/')
|
@Get('/')
|
||||||
public async getAllPermissions(): Promise<PermissionDetailsResponse[]> {
|
public async getAllPermissions(
|
||||||
return getAllPermissions()
|
@Request() request: express.Request
|
||||||
|
): Promise<PermissionDetailsResponse[]> {
|
||||||
|
return getAllPermissions(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,7 +129,8 @@ export class PermissionController {
|
|||||||
*/
|
*/
|
||||||
@Example<PermissionDetailsResponse>({
|
@Example<PermissionDetailsResponse>({
|
||||||
permissionId: 123,
|
permissionId: 123,
|
||||||
uri: '/SASjsApi/code/execute',
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
setting: 'Grant',
|
setting: 'Grant',
|
||||||
user: {
|
user: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -133,7 +153,8 @@ export class PermissionController {
|
|||||||
*/
|
*/
|
||||||
@Example<PermissionDetailsResponse>({
|
@Example<PermissionDetailsResponse>({
|
||||||
permissionId: 123,
|
permissionId: 123,
|
||||||
uri: '/SASjsApi/code/execute',
|
path: '/SASjsApi/code/execute',
|
||||||
|
type: 'Route',
|
||||||
setting: 'Grant',
|
setting: 'Grant',
|
||||||
user: {
|
user: {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -161,33 +182,43 @@ export class PermissionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllPermissions = async (): Promise<PermissionDetailsResponse[]> =>
|
const getAllPermissions = async (
|
||||||
(await Permission.find({})
|
req: express.Request
|
||||||
.select({
|
): Promise<PermissionDetailsResponse[]> => {
|
||||||
_id: 0,
|
const { user } = req
|
||||||
permissionId: 1,
|
|
||||||
uri: 1,
|
if (user?.isAdmin) return await Permission.get({})
|
||||||
setting: 1
|
else {
|
||||||
})
|
const permissions: PermissionDetailsResponse[] = []
|
||||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
|
||||||
.populate({
|
const dbUser = await User.findOne({ id: user?.userId })
|
||||||
path: 'group',
|
if (!dbUser)
|
||||||
select: 'groupId name description -_id',
|
throw {
|
||||||
populate: {
|
code: 404,
|
||||||
path: 'users',
|
status: 'Not Found',
|
||||||
select: 'id username displayName isAdmin -_id',
|
message: 'User not found.'
|
||||||
options: { limit: 15 }
|
|
||||||
}
|
}
|
||||||
})) as unknown as PermissionDetailsResponse[]
|
|
||||||
|
permissions.push(...(await Permission.get({ user: dbUser._id })))
|
||||||
|
|
||||||
|
for (const group of dbUser.groups) {
|
||||||
|
permissions.push(...(await Permission.get({ group })))
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createPermission = async ({
|
const createPermission = async ({
|
||||||
uri,
|
path,
|
||||||
|
type,
|
||||||
setting,
|
setting,
|
||||||
principalType,
|
principalType,
|
||||||
principalId
|
principalId
|
||||||
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
|
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
|
||||||
const permission = new Permission({
|
const permission = new Permission({
|
||||||
uri,
|
path,
|
||||||
|
type,
|
||||||
setting
|
setting
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -212,7 +243,8 @@ const createPermission = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alreadyExists = await Permission.findOne({
|
const alreadyExists = await Permission.findOne({
|
||||||
uri,
|
path,
|
||||||
|
type,
|
||||||
user: userInDB._id
|
user: userInDB._id
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -220,7 +252,8 @@ const createPermission = async ({
|
|||||||
throw {
|
throw {
|
||||||
code: 409,
|
code: 409,
|
||||||
status: 'Conflict',
|
status: 'Conflict',
|
||||||
message: 'Permission already exists with provided URI and User.'
|
message:
|
||||||
|
'Permission already exists with provided Path, Type and User.'
|
||||||
}
|
}
|
||||||
|
|
||||||
permission.user = userInDB._id
|
permission.user = userInDB._id
|
||||||
@@ -243,14 +276,16 @@ const createPermission = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alreadyExists = await Permission.findOne({
|
const alreadyExists = await Permission.findOne({
|
||||||
uri,
|
path,
|
||||||
|
type,
|
||||||
group: groupInDB._id
|
group: groupInDB._id
|
||||||
})
|
})
|
||||||
if (alreadyExists)
|
if (alreadyExists)
|
||||||
throw {
|
throw {
|
||||||
code: 409,
|
code: 409,
|
||||||
status: 'Conflict',
|
status: 'Conflict',
|
||||||
message: 'Permission already exists with provided URI and Group.'
|
message:
|
||||||
|
'Permission already exists with provided Path, Type and Group.'
|
||||||
}
|
}
|
||||||
|
|
||||||
permission.group = groupInDB._id
|
permission.group = groupInDB._id
|
||||||
@@ -280,7 +315,8 @@ const createPermission = async ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
permissionId: savedPermission.permissionId,
|
permissionId: savedPermission.permissionId,
|
||||||
uri: savedPermission.uri,
|
path: savedPermission.path,
|
||||||
|
type: savedPermission.type,
|
||||||
setting: savedPermission.setting,
|
setting: savedPermission.setting,
|
||||||
user,
|
user,
|
||||||
group
|
group
|
||||||
@@ -301,7 +337,8 @@ const updatePermission = async (
|
|||||||
.select({
|
.select({
|
||||||
_id: 0,
|
_id: 0,
|
||||||
permissionId: 1,
|
permissionId: 1,
|
||||||
uri: 1,
|
path: 1,
|
||||||
|
type: 1,
|
||||||
setting: 1
|
setting: 1
|
||||||
})
|
})
|
||||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
fetchLatestAutoExec,
|
fetchLatestAutoExec,
|
||||||
ModeType,
|
ModeType,
|
||||||
verifyTokenInDB,
|
verifyTokenInDB,
|
||||||
isAuthorizingRoute
|
isAuthorizingRoute,
|
||||||
|
isPublicRoute,
|
||||||
|
publicUser
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { desktopUser } from './desktop'
|
import { desktopUser } from './desktop'
|
||||||
import { authorize } from './authorize'
|
import { authorize } from './authorize'
|
||||||
@@ -41,7 +43,7 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticateToken(
|
await authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
nextFunction,
|
nextFunction,
|
||||||
@@ -50,8 +52,12 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
export const authenticateRefreshToken: RequestHandler = async (
|
||||||
authenticateToken(
|
req,
|
||||||
|
res,
|
||||||
|
next
|
||||||
|
) => {
|
||||||
|
await authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next,
|
next,
|
||||||
@@ -60,7 +66,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticateToken = (
|
const authenticateToken = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction,
|
next: NextFunction,
|
||||||
@@ -83,12 +89,12 @@ const authenticateToken = (
|
|||||||
|
|
||||||
const authHeader = req.headers['authorization']
|
const authHeader = req.headers['authorization']
|
||||||
const token = authHeader?.split(' ')[1]
|
const token = authHeader?.split(' ')[1]
|
||||||
if (!token) return res.sendStatus(401)
|
|
||||||
|
|
||||||
jwt.verify(token, key, async (err: any, data: any) => {
|
try {
|
||||||
if (err) return res.sendStatus(401)
|
if (!token) throw 'Unauthorized'
|
||||||
|
|
||||||
|
const data: any = jwt.verify(token, key)
|
||||||
|
|
||||||
// verify this valid token's entry in DB
|
|
||||||
const user = await verifyTokenInDB(
|
const user = await verifyTokenInDB(
|
||||||
data?.userId,
|
data?.userId,
|
||||||
data?.clientId,
|
data?.clientId,
|
||||||
@@ -101,8 +107,16 @@ const authenticateToken = (
|
|||||||
req.user = user
|
req.user = user
|
||||||
if (tokenType === 'accessToken') req.accessToken = token
|
if (tokenType === 'accessToken') req.accessToken = token
|
||||||
return next()
|
return next()
|
||||||
} else return res.sendStatus(401)
|
} else throw 'Unauthorized'
|
||||||
}
|
}
|
||||||
return res.sendStatus(401)
|
|
||||||
})
|
throw 'Unauthorized'
|
||||||
|
} catch (error) {
|
||||||
|
if (await isPublicRoute(req)) {
|
||||||
|
req.user = publicUser
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(401)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { RequestHandler } from 'express'
|
import { RequestHandler } from 'express'
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import Permission from '../model/Permission'
|
import Permission from '../model/Permission'
|
||||||
import { PermissionSetting } from '../controllers/permission'
|
import {
|
||||||
import { getUri } from '../utils'
|
PermissionSettingForRoute,
|
||||||
|
PermissionType
|
||||||
|
} from '../controllers/permission'
|
||||||
|
import { getPath, isPublicRoute } from '../utils'
|
||||||
|
|
||||||
export const authorize: RequestHandler = async (req, res, next) => {
|
export const authorize: RequestHandler = async (req, res, next) => {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
@@ -14,23 +17,35 @@ export const authorize: RequestHandler = async (req, res, next) => {
|
|||||||
// no need to check for permissions when user is admin
|
// no need to check for permissions when user is admin
|
||||||
if (user.isAdmin) return next()
|
if (user.isAdmin) return next()
|
||||||
|
|
||||||
|
// no need to check for permissions when route is Public
|
||||||
|
if (await isPublicRoute(req)) return next()
|
||||||
|
|
||||||
const dbUser = await User.findOne({ id: user.userId })
|
const dbUser = await User.findOne({ id: user.userId })
|
||||||
if (!dbUser) return res.sendStatus(401)
|
if (!dbUser) return res.sendStatus(401)
|
||||||
|
|
||||||
const uri = getUri(req)
|
const path = getPath(req)
|
||||||
|
|
||||||
// find permission w.r.t user
|
// find permission w.r.t user
|
||||||
const permission = await Permission.findOne({ uri, user: dbUser._id })
|
const permission = await Permission.findOne({
|
||||||
|
path,
|
||||||
|
type: PermissionType.route,
|
||||||
|
user: dbUser._id
|
||||||
|
})
|
||||||
|
|
||||||
if (permission) {
|
if (permission) {
|
||||||
if (permission.setting === PermissionSetting.grant) return next()
|
if (permission.setting === PermissionSettingForRoute.grant) return next()
|
||||||
else return res.sendStatus(401)
|
else return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|
||||||
// find permission w.r.t user's groups
|
// find permission w.r.t user's groups
|
||||||
for (const group of dbUser.groups) {
|
for (const group of dbUser.groups) {
|
||||||
const groupPermission = await Permission.findOne({ uri, group })
|
const groupPermission = await Permission.findOne({
|
||||||
if (groupPermission?.setting === PermissionSetting.grant) return next()
|
path,
|
||||||
|
type: PermissionType.route,
|
||||||
|
group
|
||||||
|
})
|
||||||
|
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
||||||
|
return next()
|
||||||
}
|
}
|
||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { GroupDetailsResponse } from '../controllers'
|
|||||||
import User, { IUser } from './User'
|
import User, { IUser } from './User'
|
||||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||||
|
|
||||||
|
export const PUBLIC_GROUP_NAME = 'Public'
|
||||||
|
|
||||||
export interface GroupPayload {
|
export interface GroupPayload {
|
||||||
/**
|
/**
|
||||||
* Name of the group
|
* Name of the group
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||||
|
import { PermissionDetailsResponse } from '../controllers'
|
||||||
|
|
||||||
|
interface GetPermissionBy {
|
||||||
|
user?: Schema.Types.ObjectId
|
||||||
|
group?: Schema.Types.ObjectId
|
||||||
|
}
|
||||||
|
|
||||||
interface IPermissionDocument extends Document {
|
interface IPermissionDocument extends Document {
|
||||||
uri: string
|
path: string
|
||||||
|
type: string
|
||||||
setting: string
|
setting: string
|
||||||
permissionId: number
|
permissionId: number
|
||||||
user: Schema.Types.ObjectId
|
user: Schema.Types.ObjectId
|
||||||
@@ -11,10 +18,16 @@ interface IPermissionDocument extends Document {
|
|||||||
|
|
||||||
interface IPermission extends IPermissionDocument {}
|
interface IPermission extends IPermissionDocument {}
|
||||||
|
|
||||||
interface IPermissionModel extends Model<IPermission> {}
|
interface IPermissionModel extends Model<IPermission> {
|
||||||
|
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
|
||||||
|
}
|
||||||
|
|
||||||
const permissionSchema = new Schema<IPermissionDocument>({
|
const permissionSchema = new Schema<IPermissionDocument>({
|
||||||
uri: {
|
path: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
@@ -28,6 +41,30 @@ const permissionSchema = new Schema<IPermissionDocument>({
|
|||||||
|
|
||||||
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
|
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
|
||||||
|
|
||||||
|
// Static Methods
|
||||||
|
permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
|
||||||
|
PermissionDetailsResponse[]
|
||||||
|
> {
|
||||||
|
return (await this.find(getBy)
|
||||||
|
.select({
|
||||||
|
_id: 0,
|
||||||
|
permissionId: 1,
|
||||||
|
path: 1,
|
||||||
|
type: 1,
|
||||||
|
setting: 1
|
||||||
|
})
|
||||||
|
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
||||||
|
.populate({
|
||||||
|
path: 'group',
|
||||||
|
select: 'groupId name description -_id',
|
||||||
|
populate: {
|
||||||
|
path: 'users',
|
||||||
|
select: 'id username displayName isAdmin -_id',
|
||||||
|
options: { limit: 15 }
|
||||||
|
}
|
||||||
|
})) as unknown as PermissionDetailsResponse[]
|
||||||
|
})
|
||||||
|
|
||||||
export const Permission: IPermissionModel = model<
|
export const Permission: IPermissionModel = model<
|
||||||
IPermission,
|
IPermission,
|
||||||
IPermissionModel
|
IPermissionModel
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const controller = new PermissionController()
|
|||||||
|
|
||||||
permissionRouter.get('/', async (req, res) => {
|
permissionRouter.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await controller.getAllPermissions()
|
const response = await controller.getAllPermissions(req)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
const statusCode = err.code
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ import appPromise from '../../../app'
|
|||||||
import {
|
import {
|
||||||
UserController,
|
UserController,
|
||||||
PermissionController,
|
PermissionController,
|
||||||
PermissionSetting,
|
PermissionType,
|
||||||
|
PermissionSettingForRoute,
|
||||||
PrincipalType
|
PrincipalType
|
||||||
} from '../../../controllers/'
|
} from '../../../controllers/'
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
@@ -48,6 +49,12 @@ const user = {
|
|||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const permission = {
|
||||||
|
type: PermissionType.route,
|
||||||
|
principalType: PrincipalType.user,
|
||||||
|
setting: PermissionSettingForRoute.grant
|
||||||
|
}
|
||||||
|
|
||||||
describe('drive', () => {
|
describe('drive', () => {
|
||||||
let app: Express
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
@@ -66,34 +73,29 @@ describe('drive', () => {
|
|||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
accessToken = await generateAndSaveToken(dbUser.id)
|
accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/drive/deploy',
|
...permission,
|
||||||
principalType: PrincipalType.user,
|
path: '/SASjsApi/drive/deploy',
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/drive/deploy/upload',
|
...permission,
|
||||||
principalType: PrincipalType.user,
|
path: '/SASjsApi/drive/deploy/upload',
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/drive/file',
|
...permission,
|
||||||
principalType: PrincipalType.user,
|
path: '/SASjsApi/drive/file',
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/drive/folder',
|
...permission,
|
||||||
principalType: PrincipalType.user,
|
path: '/SASjsApi/drive/folder',
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/drive/rename',
|
...permission,
|
||||||
principalType: PrincipalType.user,
|
path: '/SASjsApi/drive/rename',
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import request from 'supertest'
|
|||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController, GroupController } from '../../../controllers/'
|
import { UserController, GroupController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
import { PUBLIC_GROUP_NAME } from '../../../model/Group'
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
@@ -27,6 +28,12 @@ const group = {
|
|||||||
description: 'DC group for testing purposes.'
|
description: 'DC group for testing purposes.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PUBLIC_GROUP = {
|
||||||
|
name: PUBLIC_GROUP_NAME,
|
||||||
|
description:
|
||||||
|
'A special group that can be used to bypass authentication for particular routes.'
|
||||||
|
}
|
||||||
|
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const groupController = new GroupController()
|
const groupController = new GroupController()
|
||||||
|
|
||||||
@@ -535,6 +542,24 @@ describe('group', () => {
|
|||||||
expect(res.text).toEqual('User not found.')
|
expect(res.text).toEqual('User not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request when adding user to Public group', async () => {
|
||||||
|
const dbGroup = await groupController.createGroup(PUBLIC_GROUP)
|
||||||
|
const dbUser = await userController.createUser({
|
||||||
|
...user,
|
||||||
|
username: 'publicUser'
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(
|
||||||
|
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('RemoveUser', () => {
|
describe('RemoveUser', () => {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import {
|
|||||||
DriveController,
|
DriveController,
|
||||||
UserController,
|
UserController,
|
||||||
GroupController,
|
GroupController,
|
||||||
ClientController,
|
|
||||||
PermissionController,
|
PermissionController,
|
||||||
PrincipalType,
|
PrincipalType,
|
||||||
PermissionSetting
|
PermissionType,
|
||||||
|
PermissionSettingForRoute
|
||||||
} from '../../../controllers/'
|
} from '../../../controllers/'
|
||||||
import {
|
import {
|
||||||
UserDetailsResponse,
|
UserDetailsResponse,
|
||||||
@@ -56,10 +56,10 @@ const user = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const permission = {
|
const permission = {
|
||||||
uri: '/SASjsApi/code/execute',
|
path: '/SASjsApi/code/execute',
|
||||||
setting: PermissionSetting.grant,
|
type: PermissionType.route,
|
||||||
principalType: PrincipalType.user,
|
setting: PermissionSettingForRoute.grant,
|
||||||
principalId: 123
|
principalType: PrincipalType.user
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = {
|
const group = {
|
||||||
@@ -69,7 +69,6 @@ const group = {
|
|||||||
|
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const groupController = new GroupController()
|
const groupController = new GroupController()
|
||||||
const clientController = new ClientController()
|
|
||||||
const permissionController = new PermissionController()
|
const permissionController = new PermissionController()
|
||||||
|
|
||||||
describe('permission', () => {
|
describe('permission', () => {
|
||||||
@@ -108,7 +107,8 @@ describe('permission', () => {
|
|||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.permissionId).toBeTruthy()
|
expect(res.body.permissionId).toBeTruthy()
|
||||||
expect(res.body.uri).toEqual(permission.uri)
|
expect(res.body.path).toEqual(permission.path)
|
||||||
|
expect(res.body.type).toEqual(permission.type)
|
||||||
expect(res.body.setting).toEqual(permission.setting)
|
expect(res.body.setting).toEqual(permission.setting)
|
||||||
expect(res.body.user).toBeTruthy()
|
expect(res.body.user).toBeTruthy()
|
||||||
})
|
})
|
||||||
@@ -127,7 +127,8 @@ describe('permission', () => {
|
|||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.permissionId).toBeTruthy()
|
expect(res.body.permissionId).toBeTruthy()
|
||||||
expect(res.body.uri).toEqual(permission.uri)
|
expect(res.body.path).toEqual(permission.path)
|
||||||
|
expect(res.body.type).toEqual(permission.type)
|
||||||
expect(res.body.setting).toEqual(permission.setting)
|
expect(res.body.setting).toEqual(permission.setting)
|
||||||
expect(res.body.group).toBeTruthy()
|
expect(res.body.group).toBeTruthy()
|
||||||
})
|
})
|
||||||
@@ -142,53 +143,74 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not of an admin account even if user has permission', async () => {
|
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
await permissionController.createPermission({
|
|
||||||
uri: '/SASjsApi/permission',
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: dbUser.id,
|
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.send()
|
.send(permission)
|
||||||
.expect(401)
|
.expect(401)
|
||||||
|
|
||||||
expect(res.text).toEqual('Admin account required')
|
expect(res.text).toEqual('Admin account required')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if uri is missing', async () => {
|
it('should respond with Bad Request if path is missing', 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,
|
||||||
uri: undefined
|
path: undefined
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual(`"uri" is required`)
|
expect(res.text).toEqual(`"path" is required`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if uri is not valid', async () => {
|
it('should respond with Bad Request if path is not valid', 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,
|
||||||
uri: '/some/random/api/endpoint'
|
path: '/some/random/api/endpoint'
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if type is not valid', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
type: 'invalid'
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('"type" must be [Route]')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if type is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
type: undefined
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"type" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is missing', async () => {
|
it('should respond with Bad Request if setting is missing', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
@@ -203,6 +225,20 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if setting is not valid', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/permission')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
...permission,
|
||||||
|
setting: 'invalid'
|
||||||
|
})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if principalType is missing', async () => {
|
it('should respond with Bad Request if principalType is missing', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
@@ -217,20 +253,6 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if principalId is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalId: undefined
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"principalId" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if principal type is not valid', async () => {
|
it('should respond with Bad Request if principal type is not valid', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.post('/SASjsApi/permission')
|
||||||
@@ -245,17 +267,17 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is not valid', async () => {
|
it('should respond with Bad Request if principalId is missing', 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,
|
||||||
setting: 'invalid'
|
principalId: undefined
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
|
expect(res.text).toEqual(`"principalId" is required`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -313,7 +335,8 @@ describe('permission', () => {
|
|||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
...permission,
|
...permission,
|
||||||
principalType: 'group'
|
principalType: 'group',
|
||||||
|
principalId: 123
|
||||||
})
|
})
|
||||||
.expect(404)
|
.expect(404)
|
||||||
|
|
||||||
@@ -334,7 +357,7 @@ describe('permission', () => {
|
|||||||
.expect(409)
|
.expect(409)
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
expect(res.text).toEqual(
|
||||||
'Permission already exists with provided URI and User.'
|
'Permission already exists with provided Path, Type and User.'
|
||||||
)
|
)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
@@ -357,7 +380,7 @@ describe('permission', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({ setting: 'Deny' })
|
.send({ setting: PermissionSettingForRoute.deny })
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.setting).toEqual('Deny')
|
expect(res.body.setting).toEqual('Deny')
|
||||||
@@ -366,7 +389,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?.permissionId}`)
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
.send(permission)
|
.send()
|
||||||
.expect(401)
|
.expect(401)
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
expect(res.text).toEqual('Unauthorized')
|
||||||
@@ -400,12 +423,11 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is not valid', async () => {
|
it('should respond with Bad Request if setting is invalid', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/permission')
|
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
...permission,
|
|
||||||
setting: 'invalid'
|
setting: 'invalid'
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
@@ -414,12 +436,12 @@ describe('permission', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
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 exist', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch('/SASjsApi/permission/123')
|
.patch('/SASjsApi/permission/123')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
setting: PermissionSetting.deny
|
setting: PermissionSettingForRoute.deny
|
||||||
})
|
})
|
||||||
.expect(404)
|
.expect(404)
|
||||||
|
|
||||||
@@ -458,12 +480,12 @@ describe('permission', () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
...permission,
|
...permission,
|
||||||
uri: '/test-1',
|
path: '/test-1',
|
||||||
principalId: dbUser.id
|
principalId: dbUser.id
|
||||||
})
|
})
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
...permission,
|
...permission,
|
||||||
uri: '/test-2',
|
path: '/test-2',
|
||||||
principalId: dbUser.id
|
principalId: dbUser.id
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -478,34 +500,37 @@ describe('permission', () => {
|
|||||||
expect(res.body).toHaveLength(2)
|
expect(res.body).toHaveLength(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should give a list of all permissions when user is not admin', async () => {
|
it(`should give a list of user's own permissions when user is not admin`, async () => {
|
||||||
const dbUser = await userController.createUser({
|
const nonAdminUser = await userController.createUser({
|
||||||
...user,
|
...user,
|
||||||
username: 'get' + user.username
|
username: 'get' + user.username
|
||||||
})
|
})
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
const accessToken = await generateAndSaveToken(nonAdminUser.id)
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/permission',
|
path: '/test-1',
|
||||||
|
type: PermissionType.route,
|
||||||
principalType: PrincipalType.user,
|
principalType: PrincipalType.user,
|
||||||
principalId: dbUser.id,
|
principalId: nonAdminUser.id,
|
||||||
setting: PermissionSetting.grant
|
setting: PermissionSettingForRoute.grant
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const permissionCount = 1
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/permission/')
|
.get('/SASjsApi/permission/')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.send()
|
.send()
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body).toHaveLength(3)
|
expect(res.body).toHaveLength(permissionCount)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.only('verify', () => {
|
describe('verify', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
...permission,
|
...permission,
|
||||||
uri: '/SASjsApi/drive/deploy',
|
path: '/SASjsApi/drive/deploy',
|
||||||
principalId: dbUser.id
|
principalId: dbUser.id
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import appPromise from '../../../app'
|
|||||||
import {
|
import {
|
||||||
UserController,
|
UserController,
|
||||||
PermissionController,
|
PermissionController,
|
||||||
PermissionSetting,
|
PermissionType,
|
||||||
|
PermissionSettingForRoute,
|
||||||
PrincipalType
|
PrincipalType
|
||||||
} from '../../../controllers/'
|
} from '../../../controllers/'
|
||||||
import {
|
import {
|
||||||
@@ -56,10 +57,11 @@ describe('stp', () => {
|
|||||||
const dbUser = await userController.createUser(user)
|
const dbUser = await userController.createUser(user)
|
||||||
accessToken = await generateAndSaveToken(dbUser.id)
|
accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
await permissionController.createPermission({
|
await permissionController.createPermission({
|
||||||
uri: '/SASjsApi/stp/execute',
|
path: '/SASjsApi/stp/execute',
|
||||||
|
type: PermissionType.route,
|
||||||
principalType: PrincipalType.user,
|
principalType: PrincipalType.user,
|
||||||
principalId: dbUser.id,
|
principalId: dbUser.id,
|
||||||
setting: PermissionSetting.grant
|
setting: PermissionSettingForRoute.grant
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ const StaticAuthorizedRoutes = [
|
|||||||
'/SASjsApi/drive/file',
|
'/SASjsApi/drive/file',
|
||||||
'/SASjsApi/drive/folder',
|
'/SASjsApi/drive/folder',
|
||||||
'/SASjsApi/drive/fileTree',
|
'/SASjsApi/drive/fileTree',
|
||||||
'/SASjsApi/drive/rename',
|
'/SASjsApi/drive/rename'
|
||||||
'/SASjsApi/permission'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const getAuthorizedRoutes = () => {
|
export const getAuthorizedRoutes = () => {
|
||||||
@@ -19,7 +18,7 @@ export const getAuthorizedRoutes = () => {
|
|||||||
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
|
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUri = (req: Request) => {
|
export const getPath = (req: Request) => {
|
||||||
const { baseUrl, path: reqPath } = req
|
const { baseUrl, path: reqPath } = req
|
||||||
|
|
||||||
if (baseUrl === '/AppStream') {
|
if (baseUrl === '/AppStream') {
|
||||||
@@ -33,4 +32,4 @@ export const getUri = (req: Request) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isAuthorizingRoute = (req: Request): boolean =>
|
export const isAuthorizingRoute = (req: Request): boolean =>
|
||||||
getAuthorizedRoutes().includes(getUri(req))
|
getAuthorizedRoutes().includes(getPath(req))
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export * from './getRunTimeAndFilePath'
|
|||||||
export * from './getServerUrl'
|
export * from './getServerUrl'
|
||||||
export * from './instantiateLogger'
|
export * from './instantiateLogger'
|
||||||
export * from './isDebugOn'
|
export * from './isDebugOn'
|
||||||
|
export * from './isPublicRoute'
|
||||||
export * from './zipped'
|
export * from './zipped'
|
||||||
export * from './parseLogToArray'
|
export * from './parseLogToArray'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
|
|||||||
31
api/src/utils/isPublicRoute.ts
Normal file
31
api/src/utils/isPublicRoute.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Request } from 'express'
|
||||||
|
import { getPath } from './getAuthorizedRoutes'
|
||||||
|
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
||||||
|
import Permission from '../model/Permission'
|
||||||
|
import { PermissionSettingForRoute } from '../controllers'
|
||||||
|
import { RequestUser } from '../types'
|
||||||
|
|
||||||
|
export const isPublicRoute = async (req: Request): Promise<boolean> => {
|
||||||
|
const group = await Group.findOne({ name: PUBLIC_GROUP_NAME })
|
||||||
|
if (group) {
|
||||||
|
const path = getPath(req)
|
||||||
|
|
||||||
|
const groupPermission = await Permission.findOne({
|
||||||
|
path,
|
||||||
|
group: group?._id
|
||||||
|
})
|
||||||
|
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publicUser: RequestUser = {
|
||||||
|
userId: 0,
|
||||||
|
clientId: 'public_app',
|
||||||
|
username: 'publicUser',
|
||||||
|
displayName: 'Public User',
|
||||||
|
isAdmin: false,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Client from '../model/Client'
|
import Client from '../model/Client'
|
||||||
import Group from '../model/Group'
|
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import Configuration, { ConfigurationType } from '../model/Configuration'
|
import Configuration, { ConfigurationType } from '../model/Configuration'
|
||||||
|
|
||||||
@@ -31,6 +31,15 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
console.log(`DB Seed - Group created: ${GROUP.name}`)
|
console.log(`DB Seed - Group created: ${GROUP.name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checking if 'Public' Group is already in the database
|
||||||
|
const publicGroupExist = await Group.findOne({ name: PUBLIC_GROUP.name })
|
||||||
|
if (!publicGroupExist) {
|
||||||
|
const group = new Group(PUBLIC_GROUP)
|
||||||
|
await group.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||||
if (!usernameExist) {
|
if (!usernameExist) {
|
||||||
@@ -68,6 +77,13 @@ const GROUP = {
|
|||||||
name: 'AllUsers',
|
name: 'AllUsers',
|
||||||
description: 'Group contains all users'
|
description: 'Group contains all users'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PUBLIC_GROUP = {
|
||||||
|
name: PUBLIC_GROUP_NAME,
|
||||||
|
description:
|
||||||
|
'A special group that can be used to bypass authentication for particular routes.'
|
||||||
|
}
|
||||||
|
|
||||||
const CLIENT = {
|
const CLIENT = {
|
||||||
clientId: 'clientID1',
|
clientId: 'clientID1',
|
||||||
clientSecret: 'clientSecret'
|
clientSecret: 'clientSecret'
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import Joi from 'joi'
|
import Joi from 'joi'
|
||||||
import { PermissionSetting, PrincipalType } from '../controllers/permission'
|
import {
|
||||||
|
PermissionType,
|
||||||
|
PermissionSettingForRoute,
|
||||||
|
PrincipalType
|
||||||
|
} from '../controllers/permission'
|
||||||
import { getAuthorizedRoutes } from './getAuthorizedRoutes'
|
import { getAuthorizedRoutes } from './getAuthorizedRoutes'
|
||||||
|
|
||||||
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||||
@@ -89,12 +93,15 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
|||||||
|
|
||||||
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
uri: Joi.string()
|
path: Joi.string()
|
||||||
.required()
|
.required()
|
||||||
.valid(...getAuthorizedRoutes()),
|
.valid(...getAuthorizedRoutes()),
|
||||||
|
type: Joi.string()
|
||||||
|
.required()
|
||||||
|
.valid(...Object.values(PermissionType)),
|
||||||
setting: Joi.string()
|
setting: Joi.string()
|
||||||
.required()
|
.required()
|
||||||
.valid(...Object.values(PermissionSetting)),
|
.valid(...Object.values(PermissionSettingForRoute)),
|
||||||
principalType: Joi.string()
|
principalType: Joi.string()
|
||||||
.required()
|
.required()
|
||||||
.valid(...Object.values(PrincipalType)),
|
.valid(...Object.values(PrincipalType)),
|
||||||
@@ -105,7 +112,7 @@ export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
|
|||||||
Joi.object({
|
Joi.object({
|
||||||
setting: Joi.string()
|
setting: Joi.string()
|
||||||
.required()
|
.required()
|
||||||
.valid(...Object.values(PermissionSetting))
|
.valid(...Object.values(PermissionSettingForRoute))
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const deployValidation = (data: any): Joi.ValidationResult =>
|
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
|||||||
@@ -22,8 +22,14 @@ const FilePathInputModal = ({
|
|||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
const regex = /\.(exe|sh|htaccess)$/i
|
|
||||||
if (regex.test(value)) {
|
const specialChars = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>?~]/
|
||||||
|
const fileExtension = /\.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
|
if (specialChars.test(value)) {
|
||||||
|
setHasError(true)
|
||||||
|
setErrorText('can not have special characters')
|
||||||
|
} else if (fileExtension.test(value)) {
|
||||||
setHasError(true)
|
setHasError(true)
|
||||||
setErrorText('can not save file with extensions [exe, sh, htaccess]')
|
setErrorText('can not save file with extensions [exe, sh, htaccess]')
|
||||||
} else {
|
} else {
|
||||||
@@ -33,21 +39,30 @@ const FilePathInputModal = ({
|
|||||||
setFilePath(value)
|
setFilePath(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (hasError || !filePath) return
|
||||||
|
saveFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
||||||
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||||
Save File
|
Save File
|
||||||
</BootstrapDialogTitle>
|
</BootstrapDialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<TextField
|
<form onSubmit={handleSubmit}>
|
||||||
fullWidth
|
<TextField
|
||||||
variant="outlined"
|
fullWidth
|
||||||
label="File Path"
|
autoFocus
|
||||||
value={filePath}
|
variant="outlined"
|
||||||
onChange={handleChange}
|
label="File Path"
|
||||||
error={hasError}
|
value={filePath}
|
||||||
helperText={errorText}
|
onChange={handleChange}
|
||||||
/>
|
error={hasError}
|
||||||
|
helperText={errorText}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="contained" onClick={() => setOpen(false)}>
|
<Button variant="contained" onClick={() => setOpen(false)}>
|
||||||
@@ -55,9 +70,7 @@ const FilePathInputModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => {
|
onClick={() => saveFile(filePath)}
|
||||||
saveFile(filePath)
|
|
||||||
}}
|
|
||||||
disabled={hasError || !filePath}
|
disabled={hasError || !filePath}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
|
|||||||
@@ -90,17 +90,6 @@ const Header = (props: any) => {
|
|||||||
component={Link}
|
component={Link}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Button
|
|
||||||
href={`${baseUrl}/SASjsApi`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
endIcon={<OpenInNewIcon />}
|
|
||||||
>
|
|
||||||
API Docs
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
href={`${baseUrl}/AppStream`}
|
href={`${baseUrl}/AppStream`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -110,7 +99,7 @@ const Header = (props: any) => {
|
|||||||
size="large"
|
size="large"
|
||||||
endIcon={<OpenInNewIcon />}
|
endIcon={<OpenInNewIcon />}
|
||||||
>
|
>
|
||||||
App Stream
|
Apps
|
||||||
</Button>
|
</Button>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -138,18 +127,6 @@ const Header = (props: any) => {
|
|||||||
open={!!anchorEl}
|
open={!!anchorEl}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
>
|
>
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
href={'https://server.sasjs.io'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
@@ -162,6 +139,32 @@ const Header = (props: any) => {
|
|||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
href={'https://server.sasjs.io'}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
endIcon={<OpenInNewIcon />}
|
||||||
|
>
|
||||||
|
Docs
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
href={`${baseUrl}/SASjsApi`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
endIcon={<OpenInNewIcon />}
|
||||||
|
>
|
||||||
|
API
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||||
<Button variant="contained" color="primary">
|
<Button variant="contained" color="primary">
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
||||||
import { Button, DialogActions, DialogContent, TextField } from '@mui/material'
|
import { Button, DialogActions, DialogContent, TextField } from '@mui/material'
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ type NameInputModalProps = {
|
|||||||
isFolder: boolean
|
isFolder: boolean
|
||||||
actionLabel: string
|
actionLabel: string
|
||||||
action: (name: string) => void
|
action: (name: string) => void
|
||||||
|
defaultName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const NameInputModal = ({
|
const NameInputModal = ({
|
||||||
@@ -20,12 +21,25 @@ const NameInputModal = ({
|
|||||||
title,
|
title,
|
||||||
isFolder,
|
isFolder,
|
||||||
actionLabel,
|
actionLabel,
|
||||||
action
|
action,
|
||||||
|
defaultName
|
||||||
}: NameInputModalProps) => {
|
}: NameInputModalProps) => {
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [hasError, setHasError] = useState(false)
|
const [hasError, setHasError] = useState(false)
|
||||||
const [errorText, setErrorText] = useState('')
|
const [errorText, setErrorText] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultName) setName(defaultName)
|
||||||
|
}, [defaultName])
|
||||||
|
|
||||||
|
const handleFocus = (
|
||||||
|
event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement, Element>
|
||||||
|
) => {
|
||||||
|
if (defaultName) {
|
||||||
|
event.target.select()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
|
|
||||||
@@ -49,21 +63,32 @@ const NameInputModal = ({
|
|||||||
setName(value)
|
setName(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (hasError || !name) return
|
||||||
|
action(name)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
||||||
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||||
{title}
|
{title}
|
||||||
</BootstrapDialogTitle>
|
</BootstrapDialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<TextField
|
<form onSubmit={handleSubmit}>
|
||||||
fullWidth
|
<TextField
|
||||||
variant="outlined"
|
id="input-box"
|
||||||
label={isFolder ? 'Folder Name' : 'File Name'}
|
fullWidth
|
||||||
value={name}
|
autoFocus
|
||||||
onChange={handleChange}
|
onFocus={handleFocus}
|
||||||
error={hasError}
|
variant="outlined"
|
||||||
helperText={errorText}
|
label={isFolder ? 'Folder Name' : 'File Name'}
|
||||||
/>
|
value={name}
|
||||||
|
onChange={handleChange}
|
||||||
|
error={hasError}
|
||||||
|
helperText={errorText}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="contained" onClick={() => setOpen(false)}>
|
<Button variant="contained" onClick={() => setOpen(false)}>
|
||||||
@@ -71,9 +96,7 @@ const NameInputModal = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => {
|
onClick={() => action(name)}
|
||||||
action(name)
|
|
||||||
}}
|
|
||||||
disabled={hasError || !name}
|
disabled={hasError || !name}
|
||||||
>
|
>
|
||||||
{actionLabel}
|
{actionLabel}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const TreeViewNode = ({
|
|||||||
useState(false)
|
useState(false)
|
||||||
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
||||||
useState('')
|
useState('')
|
||||||
|
const [defaultInputModalName, setDefaultInputModalName] = useState('')
|
||||||
const [nameInputModalOpen, setNameInputModalOpen] = useState(false)
|
const [nameInputModalOpen, setNameInputModalOpen] = useState(false)
|
||||||
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
||||||
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
||||||
@@ -129,6 +130,7 @@ const TreeViewNode = ({
|
|||||||
setNameInputModalTitle('Add Folder')
|
setNameInputModalTitle('Add Folder')
|
||||||
setNameInputModalActionLabel('Add')
|
setNameInputModalActionLabel('Add')
|
||||||
setNameInputModalForFolder(true)
|
setNameInputModalForFolder(true)
|
||||||
|
setDefaultInputModalName('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewFileItemClick = () => {
|
const handleNewFileItemClick = () => {
|
||||||
@@ -137,6 +139,7 @@ const TreeViewNode = ({
|
|||||||
setNameInputModalTitle('Add File')
|
setNameInputModalTitle('Add File')
|
||||||
setNameInputModalActionLabel('Add')
|
setNameInputModalActionLabel('Add')
|
||||||
setNameInputModalForFolder(false)
|
setNameInputModalForFolder(false)
|
||||||
|
setDefaultInputModalName('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const addFileFolder = (name: string) => {
|
const addFileFolder = (name: string) => {
|
||||||
@@ -152,6 +155,7 @@ const TreeViewNode = ({
|
|||||||
setNameInputModalTitle('Rename')
|
setNameInputModalTitle('Rename')
|
||||||
setNameInputModalActionLabel('Rename')
|
setNameInputModalActionLabel('Rename')
|
||||||
setNameInputModalForFolder(node.isFolder)
|
setNameInputModalForFolder(node.isFolder)
|
||||||
|
setDefaultInputModalName(node.relativePath.split('/').pop() ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const renameFileFolder = (name: string) => {
|
const renameFileFolder = (name: string) => {
|
||||||
@@ -208,6 +212,7 @@ const TreeViewNode = ({
|
|||||||
action={
|
action={
|
||||||
nameInputModalActionLabel === 'Add' ? addFileFolder : renameFileFolder
|
nameInputModalActionLabel === 'Add' ? addFileFolder : renameFileFolder
|
||||||
}
|
}
|
||||||
|
defaultName={defaultInputModalName}
|
||||||
/>
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
open={contextMenu !== null}
|
open={contextMenu !== null}
|
||||||
|
|||||||
@@ -40,10 +40,11 @@ const AddPermissionModal = ({
|
|||||||
handleOpen,
|
handleOpen,
|
||||||
addPermission
|
addPermission
|
||||||
}: AddPermissionModalProps) => {
|
}: AddPermissionModalProps) => {
|
||||||
const [URIs, setURIs] = useState<string[]>([])
|
const [paths, setPaths] = useState<string[]>([])
|
||||||
const [loadingURIs, setLoadingURIs] = useState(false)
|
const [loadingPaths, setLoadingPaths] = useState(false)
|
||||||
const [uri, setUri] = useState<string>()
|
const [path, setPath] = useState<string>()
|
||||||
const [principalType, setPrincipalType] = useState('user')
|
const [permissionType, setPermissionType] = useState('Route')
|
||||||
|
const [principalType, setPrincipalType] = useState('group')
|
||||||
const [userPrincipal, setUserPrincipal] = useState<UserResponse>()
|
const [userPrincipal, setUserPrincipal] = useState<UserResponse>()
|
||||||
const [groupPrincipal, setGroupPrincipal] = useState<GroupResponse>()
|
const [groupPrincipal, setGroupPrincipal] = useState<GroupResponse>()
|
||||||
const [permissionSetting, setPermissionSetting] = useState('Grant')
|
const [permissionSetting, setPermissionSetting] = useState('Grant')
|
||||||
@@ -52,19 +53,19 @@ const AddPermissionModal = ({
|
|||||||
const [groupPrincipals, setGroupPrincipals] = useState<GroupResponse[]>([])
|
const [groupPrincipals, setGroupPrincipals] = useState<GroupResponse[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoadingURIs(true)
|
setLoadingPaths(true)
|
||||||
axios
|
axios
|
||||||
.get('/SASjsApi/info/authorizedRoutes')
|
.get('/SASjsApi/info/authorizedRoutes')
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
setURIs(res.data.URIs)
|
setPaths(res.data.paths)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoadingURIs(false)
|
setLoadingPaths(false)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -93,7 +94,8 @@ const AddPermissionModal = ({
|
|||||||
|
|
||||||
const handleAddPermission = () => {
|
const handleAddPermission = () => {
|
||||||
const addPermissionPayload: any = {
|
const addPermissionPayload: any = {
|
||||||
uri,
|
path,
|
||||||
|
type: permissionType,
|
||||||
setting: permissionSetting,
|
setting: permissionSetting,
|
||||||
principalType
|
principalType
|
||||||
}
|
}
|
||||||
@@ -106,7 +108,7 @@ const AddPermissionModal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addButtonDisabled =
|
const addButtonDisabled =
|
||||||
!uri || (principalType === 'user' ? !userPrincipal : !groupPrincipal)
|
!path || (principalType === 'user' ? !userPrincipal : !groupPrincipal)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
||||||
@@ -120,22 +122,40 @@ const AddPermissionModal = ({
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={URIs}
|
options={paths}
|
||||||
disableClearable
|
disableClearable
|
||||||
value={uri}
|
value={path}
|
||||||
onChange={(event: any, newValue: string) => setUri(newValue)}
|
onChange={(event: any, newValue: string) => setPath(newValue)}
|
||||||
renderInput={(params) =>
|
renderInput={(params) =>
|
||||||
loadingURIs ? (
|
loadingPaths ? (
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
) : (
|
) : (
|
||||||
<TextField {...params} label="Principal" />
|
<TextField {...params} autoFocus label="Path" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={['user', 'group']}
|
options={['Route']}
|
||||||
|
disableClearable
|
||||||
|
value={permissionType}
|
||||||
|
onChange={(event: any, newValue: string) =>
|
||||||
|
setPermissionType(newValue)
|
||||||
|
}
|
||||||
|
renderInput={(params) =>
|
||||||
|
loadingPaths ? (
|
||||||
|
<CircularProgress />
|
||||||
|
) : (
|
||||||
|
<TextField {...params} label="Permission Type" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Autocomplete
|
||||||
|
options={['group', 'user']}
|
||||||
|
getOptionLabel={(option) => option.toUpperCase()}
|
||||||
disableClearable
|
disableClearable
|
||||||
value={principalType}
|
value={principalType}
|
||||||
onChange={(event: any, newValue: string) =>
|
onChange={(event: any, newValue: string) =>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const Settings = () => {
|
|||||||
>
|
>
|
||||||
<StyledTab label="Profile" value="profile" />
|
<StyledTab label="Profile" value="profile" />
|
||||||
{appContext.mode === ModeType.Server && (
|
{appContext.mode === ModeType.Server && (
|
||||||
<StyledTab label="Uri Access" value="permission" />
|
<StyledTab label="Permission" value="permission" />
|
||||||
)}
|
)}
|
||||||
</TabList>
|
</TabList>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const Permission = () => {
|
|||||||
const [selectedPermission, setSelectedPermission] =
|
const [selectedPermission, setSelectedPermission] =
|
||||||
useState<PermissionResponse>()
|
useState<PermissionResponse>()
|
||||||
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||||
const [uriFilter, setUriFilter] = useState<string[]>([])
|
const [pathFilter, setPathFilter] = useState<string[]>([])
|
||||||
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
||||||
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
||||||
PrincipalType[]
|
PrincipalType[]
|
||||||
@@ -111,8 +111,10 @@ const Permission = () => {
|
|||||||
setFilterModalOpen(false)
|
setFilterModalOpen(false)
|
||||||
|
|
||||||
const uriFilteredPermissions =
|
const uriFilteredPermissions =
|
||||||
uriFilter.length > 0
|
pathFilter.length > 0
|
||||||
? permissions.filter((permission) => uriFilter.includes(permission.uri))
|
? permissions.filter((permission) =>
|
||||||
|
pathFilter.includes(permission.path)
|
||||||
|
)
|
||||||
: permissions
|
: permissions
|
||||||
|
|
||||||
const principalFilteredPermissions =
|
const principalFilteredPermissions =
|
||||||
@@ -172,7 +174,7 @@ const Permission = () => {
|
|||||||
|
|
||||||
const resetFilter = () => {
|
const resetFilter = () => {
|
||||||
setFilterModalOpen(false)
|
setFilterModalOpen(false)
|
||||||
setUriFilter([])
|
setPathFilter([])
|
||||||
setPrincipalFilter([])
|
setPrincipalFilter([])
|
||||||
setSettingFilter([])
|
setSettingFilter([])
|
||||||
setFilteredPermissions([])
|
setFilteredPermissions([])
|
||||||
@@ -322,8 +324,8 @@ const Permission = () => {
|
|||||||
open={filterModalOpen}
|
open={filterModalOpen}
|
||||||
handleOpen={setFilterModalOpen}
|
handleOpen={setFilterModalOpen}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
uriFilter={uriFilter}
|
pathFilter={pathFilter}
|
||||||
setUriFilter={setUriFilter}
|
setPathFilter={setPathFilter}
|
||||||
principalFilter={principalFilter}
|
principalFilter={principalFilter}
|
||||||
setPrincipalFilter={setPrincipalFilter}
|
setPrincipalFilter={setPrincipalFilter}
|
||||||
principalTypeFilter={principalTypeFilter}
|
principalTypeFilter={principalTypeFilter}
|
||||||
@@ -374,9 +376,10 @@ const PermissionTable = ({
|
|||||||
<Table sx={{ minWidth: 650 }}>
|
<Table sx={{ minWidth: 650 }}>
|
||||||
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<BootstrapTableCell>Uri</BootstrapTableCell>
|
<BootstrapTableCell>Path</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Permission Type</BootstrapTableCell>
|
||||||
<BootstrapTableCell>Principal</BootstrapTableCell>
|
<BootstrapTableCell>Principal</BootstrapTableCell>
|
||||||
<BootstrapTableCell>Type</BootstrapTableCell>
|
<BootstrapTableCell>Principal Type</BootstrapTableCell>
|
||||||
<BootstrapTableCell>Setting</BootstrapTableCell>
|
<BootstrapTableCell>Setting</BootstrapTableCell>
|
||||||
{appContext.isAdmin && (
|
{appContext.isAdmin && (
|
||||||
<BootstrapTableCell>Action</BootstrapTableCell>
|
<BootstrapTableCell>Action</BootstrapTableCell>
|
||||||
@@ -386,7 +389,8 @@ const PermissionTable = ({
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{permissions.map((permission) => (
|
{permissions.map((permission) => (
|
||||||
<TableRow key={permission.permissionId}>
|
<TableRow key={permission.permissionId}>
|
||||||
<BootstrapTableCell>{permission.uri}</BootstrapTableCell>
|
<BootstrapTableCell>{permission.path}</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>{permission.type}</BootstrapTableCell>
|
||||||
<BootstrapTableCell>
|
<BootstrapTableCell>
|
||||||
{displayPrincipal(permission)}
|
{displayPrincipal(permission)}
|
||||||
</BootstrapTableCell>
|
</BootstrapTableCell>
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ type FilterModalProps = {
|
|||||||
open: boolean
|
open: boolean
|
||||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||||
permissions: PermissionResponse[]
|
permissions: PermissionResponse[]
|
||||||
uriFilter: string[]
|
pathFilter: string[]
|
||||||
setUriFilter: Dispatch<SetStateAction<string[]>>
|
setPathFilter: Dispatch<SetStateAction<string[]>>
|
||||||
principalFilter: string[]
|
principalFilter: string[]
|
||||||
setPrincipalFilter: Dispatch<SetStateAction<string[]>>
|
setPrincipalFilter: Dispatch<SetStateAction<string[]>>
|
||||||
principalTypeFilter: PrincipalType[]
|
principalTypeFilter: PrincipalType[]
|
||||||
@@ -43,8 +43,8 @@ const PermissionFilterModal = ({
|
|||||||
open,
|
open,
|
||||||
handleOpen,
|
handleOpen,
|
||||||
permissions,
|
permissions,
|
||||||
uriFilter,
|
pathFilter,
|
||||||
setUriFilter,
|
setPathFilter,
|
||||||
principalFilter,
|
principalFilter,
|
||||||
setPrincipalFilter,
|
setPrincipalFilter,
|
||||||
principalTypeFilter,
|
principalTypeFilter,
|
||||||
@@ -54,8 +54,8 @@ const PermissionFilterModal = ({
|
|||||||
applyFilter,
|
applyFilter,
|
||||||
resetFilter
|
resetFilter
|
||||||
}: FilterModalProps) => {
|
}: FilterModalProps) => {
|
||||||
const URIs = permissions
|
const paths = permissions
|
||||||
.map((permission) => permission.uri)
|
.map((permission) => permission.path)
|
||||||
.filter((uri, index, array) => array.indexOf(uri) === index)
|
.filter((uri, index, array) => array.indexOf(uri) === index)
|
||||||
|
|
||||||
// fetch all the principals from permissions array
|
// fetch all the principals from permissions array
|
||||||
@@ -86,11 +86,11 @@ const PermissionFilterModal = ({
|
|||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
multiple
|
multiple
|
||||||
options={URIs}
|
options={paths}
|
||||||
filterSelectedOptions
|
filterSelectedOptions
|
||||||
value={uriFilter}
|
value={pathFilter}
|
||||||
onChange={(event: any, newValue: string[]) => {
|
onChange={(event: any, newValue: string[]) => {
|
||||||
setUriFilter(newValue)
|
setPathFilter(newValue)
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => <TextField {...params} label="URIs" />}
|
renderInput={(params) => <TextField {...params} label="URIs" />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
Tab,
|
Tab,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { styled } from '@mui/material/styles'
|
import { styled } from '@mui/material/styles'
|
||||||
|
|
||||||
@@ -29,7 +30,8 @@ import {
|
|||||||
import Editor, {
|
import Editor, {
|
||||||
MonacoDiffEditor,
|
MonacoDiffEditor,
|
||||||
DiffEditorDidMount,
|
DiffEditorDidMount,
|
||||||
EditorDidMount
|
EditorDidMount,
|
||||||
|
monaco
|
||||||
} from 'react-monaco-editor'
|
} from 'react-monaco-editor'
|
||||||
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ import FilePathInputModal from '../../components/filePathInputModal'
|
|||||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||||
import Modal from '../../components/modal'
|
import Modal from '../../components/modal'
|
||||||
|
|
||||||
import usePrompt from '../../utils/usePrompt'
|
import { usePrompt, useStateWithCallback } from '../../utils/hooks'
|
||||||
|
|
||||||
const StyledTabPanel = styled(TabPanel)(() => ({
|
const StyledTabPanel = styled(TabPanel)(() => ({
|
||||||
padding: '10px'
|
padding: '10px'
|
||||||
@@ -74,7 +76,7 @@ const SASjsEditor = ({
|
|||||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||||
AlertSeverityType.Success
|
AlertSeverityType.Success
|
||||||
)
|
)
|
||||||
const [prevFileContent, setPrevFileContent] = useState('')
|
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||||
const [fileContent, setFileContent] = useState('')
|
const [fileContent, setFileContent] = useState('')
|
||||||
const [log, setLog] = useState('')
|
const [log, setLog] = useState('')
|
||||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||||
@@ -88,21 +90,41 @@ const SASjsEditor = ({
|
|||||||
|
|
||||||
const editorRef = useRef(null as any)
|
const editorRef = useRef(null as any)
|
||||||
|
|
||||||
const diffEditorRef = useRef(null as any)
|
|
||||||
|
|
||||||
const handleEditorDidMount: EditorDidMount = (editor) => {
|
const handleEditorDidMount: EditorDidMount = (editor) => {
|
||||||
editor.focus()
|
|
||||||
editorRef.current = editor
|
editorRef.current = editor
|
||||||
|
editor.focus()
|
||||||
|
editor.addAction({
|
||||||
|
// An unique identifier of the contributed action.
|
||||||
|
id: 'show-difference',
|
||||||
|
|
||||||
|
// A label of the action that will be presented to the user.
|
||||||
|
label: 'Show Differences',
|
||||||
|
|
||||||
|
// An optional array of keybindings for the action.
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],
|
||||||
|
|
||||||
|
contextMenuGroupId: 'navigation',
|
||||||
|
|
||||||
|
contextMenuOrder: 1,
|
||||||
|
|
||||||
|
// Method that will be executed when the action is triggered.
|
||||||
|
// @param editor The editor instance is passed in as a convenience
|
||||||
|
run: function (ed) {
|
||||||
|
setShowDiff(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
|
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
|
||||||
diffEditor.focus()
|
diffEditor.focus()
|
||||||
diffEditorRef.current = diffEditor
|
diffEditor.addCommand(monaco.KeyCode.Escape, function () {
|
||||||
|
setShowDiff(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
usePrompt(
|
usePrompt(
|
||||||
'Changes you made may not be saved.',
|
'Changes you made may not be saved.',
|
||||||
prevFileContent !== fileContent
|
prevFileContent !== fileContent && !!selectedFilePath
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -134,10 +156,21 @@ const SASjsEditor = ({
|
|||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false))
|
.finally(() => setIsLoading(false))
|
||||||
} else {
|
} else {
|
||||||
setFileContent('')
|
const content = localStorage.getItem('fileContent') ?? ''
|
||||||
|
setFileContent(content)
|
||||||
}
|
}
|
||||||
|
setLog('')
|
||||||
|
setWebout('')
|
||||||
|
setTab('1')
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedFilePath])
|
}, [selectedFilePath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fileContent.length && !selectedFilePath) {
|
||||||
|
localStorage.setItem('fileContent', fileContent)
|
||||||
|
}
|
||||||
|
}, [fileContent, selectedFilePath])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (runTimes.includes(selectedFileExtension))
|
if (runTimes.includes(selectedFileExtension))
|
||||||
setSelectedRunTime(selectedFileExtension)
|
setSelectedRunTime(selectedFileExtension)
|
||||||
@@ -211,6 +244,10 @@ const SASjsEditor = ({
|
|||||||
const saveFile = (filePath?: string) => {
|
const saveFile = (filePath?: string) => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
|
||||||
|
}
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||||
@@ -223,10 +260,22 @@ const SASjsEditor = ({
|
|||||||
|
|
||||||
axiosPromise
|
axiosPromise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (filePath) {
|
if (filePath && fileContent === prevFileContent) {
|
||||||
|
// when fileContent and prevFileContent is same,
|
||||||
|
// callback function in setPrevFileContent method is not called
|
||||||
|
// because behind the scene useEffect hook is being used
|
||||||
|
// for calling callback function, and it's only fired when the
|
||||||
|
// new value is not equal to old value.
|
||||||
|
// So, we'll have to explicitly update the selected file path
|
||||||
|
|
||||||
setSelectedFilePath(filePath, true)
|
setSelectedFilePath(filePath, true)
|
||||||
|
} else {
|
||||||
|
setPrevFileContent(fileContent, () => {
|
||||||
|
if (filePath) {
|
||||||
|
setSelectedFilePath(filePath, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
setPrevFileContent(fileContent)
|
|
||||||
setSnackbarMessage('File saved!')
|
setSnackbarMessage('File saved!')
|
||||||
setSnackbarSeverity(AlertSeverityType.Success)
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
setOpenSnackbar(true)
|
setOpenSnackbar(true)
|
||||||
@@ -312,9 +361,14 @@ const SASjsEditor = ({
|
|||||||
<TabList onChange={handleTabChange} centered>
|
<TabList onChange={handleTabChange} centered>
|
||||||
<StyledTab label="Code" value="1" />
|
<StyledTab label="Code" value="1" />
|
||||||
<StyledTab label="Log" value="2" />
|
<StyledTab label="Log" value="2" />
|
||||||
<Tooltip title="Displays content from the _webout fileref">
|
<StyledTab
|
||||||
<StyledTab label="Webout" value="3" />
|
label={
|
||||||
</Tooltip>
|
<Tooltip title="Displays content from the _webout fileref">
|
||||||
|
<Typography>Webout</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
value="3"
|
||||||
|
/>
|
||||||
</TabList>
|
</TabList>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -324,6 +378,8 @@ const SASjsEditor = ({
|
|||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<RunMenu
|
<RunMenu
|
||||||
|
fileContent={fileContent}
|
||||||
|
prevFileContent={prevFileContent}
|
||||||
selectedFilePath={selectedFilePath}
|
selectedFilePath={selectedFilePath}
|
||||||
selectedRunTime={selectedRunTime}
|
selectedRunTime={selectedRunTime}
|
||||||
runTimes={runTimes}
|
runTimes={runTimes}
|
||||||
@@ -423,6 +479,8 @@ export default SASjsEditor
|
|||||||
|
|
||||||
type RunMenuProps = {
|
type RunMenuProps = {
|
||||||
selectedFilePath: string
|
selectedFilePath: string
|
||||||
|
fileContent: string
|
||||||
|
prevFileContent: string
|
||||||
selectedRunTime: string
|
selectedRunTime: string
|
||||||
runTimes: string[]
|
runTimes: string[]
|
||||||
handleChangeRunTime: (event: SelectChangeEvent) => void
|
handleChangeRunTime: (event: SelectChangeEvent) => void
|
||||||
@@ -431,6 +489,8 @@ type RunMenuProps = {
|
|||||||
|
|
||||||
const RunMenu = ({
|
const RunMenu = ({
|
||||||
selectedFilePath,
|
selectedFilePath,
|
||||||
|
fileContent,
|
||||||
|
prevFileContent,
|
||||||
selectedRunTime,
|
selectedRunTime,
|
||||||
runTimes,
|
runTimes,
|
||||||
handleChangeRunTime,
|
handleChangeRunTime,
|
||||||
@@ -463,10 +523,21 @@ const RunMenu = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{selectedFilePath ? (
|
{selectedFilePath ? (
|
||||||
<Box sx={{ marginLeft: '10px' }}>
|
<Box sx={{ marginLeft: '10px' }}>
|
||||||
<Tooltip title="Launch program in new window">
|
<Tooltip
|
||||||
<IconButton onClick={launchProgram}>
|
title={
|
||||||
<RocketLaunch />
|
fileContent !== prevFileContent
|
||||||
</IconButton>
|
? 'Save file before launching program'
|
||||||
|
: 'Launch program in new window'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={fileContent !== prevFileContent}
|
||||||
|
onClick={launchProgram}
|
||||||
|
>
|
||||||
|
<RocketLaunch />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
2
web/src/utils/hooks/index.ts
Normal file
2
web/src/utils/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './usePrompt'
|
||||||
|
export * from './useStateWithCallback'
|
||||||
@@ -2,7 +2,7 @@ import { useEffect, useCallback, useContext } from 'react'
|
|||||||
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom'
|
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom'
|
||||||
import { History, Blocker, Transition } from 'history'
|
import { History, Blocker, Transition } from 'history'
|
||||||
|
|
||||||
function useBlocker(blocker: Blocker, when = true) {
|
const useBlocker = (blocker: Blocker, when = true) => {
|
||||||
const navigator = useContext(NavigationContext).navigator as History
|
const navigator = useContext(NavigationContext).navigator as History
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -24,7 +24,7 @@ function useBlocker(blocker: Blocker, when = true) {
|
|||||||
}, [navigator, blocker, when])
|
}, [navigator, blocker, when])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function usePrompt(message: string, when = true) {
|
export const usePrompt = (message: string, when = true) => {
|
||||||
const blocker = useCallback(
|
const blocker = useCallback(
|
||||||
(tx) => {
|
(tx) => {
|
||||||
if (window.confirm(message)) tx.retry()
|
if (window.confirm(message)) tx.retry()
|
||||||
27
web/src/utils/hooks/useStateWithCallback.ts
Normal file
27
web/src/utils/hooks/useStateWithCallback.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export const useStateWithCallback = <T>(
|
||||||
|
initialValue: T
|
||||||
|
): [T, (newValue: T, callback?: () => void) => void] => {
|
||||||
|
const callbackRef = useRef<any>(null)
|
||||||
|
|
||||||
|
const [value, setValue] = useState(initialValue)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof callbackRef.current === 'function') {
|
||||||
|
callbackRef.current()
|
||||||
|
|
||||||
|
callbackRef.current = null
|
||||||
|
}
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
const setValueWithCallback = (newValue: T, callback?: () => void) => {
|
||||||
|
callbackRef.current = callback
|
||||||
|
|
||||||
|
setValue(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [value, setValueWithCallback]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useStateWithCallback
|
||||||
@@ -18,14 +18,16 @@ export interface GroupDetailsResponse extends GroupResponse {
|
|||||||
|
|
||||||
export interface PermissionResponse {
|
export interface PermissionResponse {
|
||||||
permissionId: number
|
permissionId: number
|
||||||
uri: string
|
path: string
|
||||||
|
type: string
|
||||||
setting: string
|
setting: string
|
||||||
user?: UserResponse
|
user?: UserResponse
|
||||||
group?: GroupDetailsResponse
|
group?: GroupDetailsResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterPermissionPayload {
|
export interface RegisterPermissionPayload {
|
||||||
uri: string
|
path: string
|
||||||
|
type: string
|
||||||
setting: string
|
setting: string
|
||||||
principalType: string
|
principalType: string
|
||||||
principalId: number
|
principalId: number
|
||||||
|
|||||||
Reference in New Issue
Block a user