mirror of
https://github.com/sasjs/server.git
synced 2025-12-11 03:34:35 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb5be1be21 | ||
|
|
d90fa9e5dd | ||
| d99fdd1ec7 | |||
|
|
399b5edad0 | ||
|
|
1dbc12e96b | ||
| e215958b8b | |||
| 9227cd449d | |||
| c67d3ee2f1 | |||
| 6ef40b954a | |||
|
|
0d913baff1 | ||
|
|
3671736c3d | ||
| 34cd84d8a9 | |||
|
|
f7fcc7741a | ||
|
|
18052fdbf6 | ||
|
|
5966016853 | ||
|
|
87c03c5f8d | ||
| 7a162eda8f | |||
| 754704bca8 | |||
|
|
77f8d30baf | ||
|
|
78bea7c154 | ||
|
|
9c3b155c12 | ||
|
|
98e501334f | ||
|
|
bbfd53e79e | ||
| 254bc07da7 | |||
| f978814ca7 | |||
| 68515f95a6 | |||
| d3a516c36e | |||
| c3e3befc17 | |||
|
|
275de9478e | ||
|
|
1a3ef62cb2 | ||
| 79b7827b7c | |||
| 37e1aa9b61 | |||
| 7e504008b7 | |||
| 5d5a9d3788 | |||
| b7dff341f0 | |||
| 8a3054e19a |
55
CHANGELOG.md
55
CHANGELOG.md
@@ -1,3 +1,58 @@
|
|||||||
|
## [0.15.1](https://github.com/sasjs/server/compare/v0.15.0...v0.15.1) (2022-08-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** fix UI responsiveness ([d99fdd1](https://github.com/sasjs/server/commit/d99fdd1ec7991b94a0d98338d7a7a6216f46ce45))
|
||||||
|
|
||||||
|
# [0.15.0](https://github.com/sasjs/server/compare/v0.14.1...v0.15.0) (2022-08-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* after selecting file in sidebar collapse sidebar in mobile view ([e215958](https://github.com/sasjs/server/commit/e215958b8b05d7a8ce9d82395e0640b5b37fb40d))
|
||||||
|
* improve mobile view for studio page ([c67d3ee](https://github.com/sasjs/server/commit/c67d3ee2f102155e2e9781e13d5d33c1ab227cb4))
|
||||||
|
* improve responsiveness for mobile view ([6ef40b9](https://github.com/sasjs/server/commit/6ef40b954a87ebb0a2621119064f38d58ea85148))
|
||||||
|
* improve user experience for adding permissions ([7a162ed](https://github.com/sasjs/server/commit/7a162eda8fc60383ff647d93e6611799e2e6af7a))
|
||||||
|
* show logout button only when user is logged in ([9227cd4](https://github.com/sasjs/server/commit/9227cd449dc46fd960a488eb281804a9b9ffc284))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add multiple permission for same combination of type and principal at once ([754704b](https://github.com/sasjs/server/commit/754704bca89ecbdbcc3bd4ef04b94124c4f24167))
|
||||||
|
|
||||||
|
## [0.14.1](https://github.com/sasjs/server/compare/v0.14.0...v0.14.1) (2022-08-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **apps:** App Stream logo fix ([87c03c5](https://github.com/sasjs/server/commit/87c03c5f8dbdfc151d4ff3722ecbcd3f7e409aea))
|
||||||
|
* **cookie:** XSRF cookie is removed and passed token in head section ([77f8d30](https://github.com/sasjs/server/commit/77f8d30baf9b1077279c29f1c3e5ca02a5436bc0))
|
||||||
|
* **env:** check added for not providing WHITELIST ([5966016](https://github.com/sasjs/server/commit/5966016853369146b27ac5781808cb51d65c887f))
|
||||||
|
* **web:** show login on logged-out state ([f7fcc77](https://github.com/sasjs/server/commit/f7fcc7741aa2af93a4a2b1e651003704c9bbff0c))
|
||||||
|
|
||||||
|
# [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)
|
## [0.13.2](https://github.com/sasjs/server/compare/v0.13.1...v0.13.2) (2022-08-01)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { ErrorRequestHandler } from 'express'
|
import express, { ErrorRequestHandler } from 'express'
|
||||||
import csrf from 'csurf'
|
import csrf, { CookieOptions } from 'csurf'
|
||||||
import cookieParser from 'cookie-parser'
|
import cookieParser from 'cookie-parser'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
@@ -32,9 +32,10 @@ const app = express()
|
|||||||
|
|
||||||
const { PROTOCOL } = process.env
|
const { PROTOCOL } = process.env
|
||||||
|
|
||||||
export const cookieOptions = {
|
export const cookieOptions: CookieOptions = {
|
||||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
|
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
|
||||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,11 @@ describe('web', () => {
|
|||||||
|
|
||||||
describe('home', () => {
|
describe('home', () => {
|
||||||
it('should respond with CSRF Token', async () => {
|
it('should respond with CSRF Token', async () => {
|
||||||
await request(app)
|
const res = await request(app).get('/').expect(200)
|
||||||
.get('/')
|
|
||||||
.expect(
|
expect(res.text).toMatch(
|
||||||
'set-cookie',
|
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/
|
||||||
/_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\//
|
)
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -154,10 +153,10 @@ describe('web', () => {
|
|||||||
|
|
||||||
const getCSRF = async (app: Express) => {
|
const getCSRF = async (app: Express) => {
|
||||||
// make request to get CSRF
|
// make request to get CSRF
|
||||||
const { header } = await request(app).get('/')
|
const { header, text } = await request(app).get('/')
|
||||||
const cookies = header['set-cookie'].join()
|
const cookies = header['set-cookie'].join()
|
||||||
|
|
||||||
const csrfToken = extractCSRF(cookies)
|
const csrfToken = extractCSRF(text)
|
||||||
return { csrfToken, cookies }
|
return { csrfToken, cookies }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +176,7 @@ const performLogin = async (
|
|||||||
return { cookies: newCookies }
|
return { cookies: newCookies }
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractCSRF = (cookies: string) =>
|
const extractCSRF = (text: string) =>
|
||||||
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
|
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
|
||||||
cookies
|
text
|
||||||
)![2]
|
)![1]
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const style = `<style>
|
|||||||
}
|
}
|
||||||
.app-container .app img{
|
.app-container .app img{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: calc(100% - 30px);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ webRouter.get('/', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
response = await controller.home()
|
response = await controller.home()
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
response = 'Web Build is not present'
|
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||||
} finally {
|
} finally {
|
||||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||||
|
const injectedContent = response?.replace(
|
||||||
|
'</head>',
|
||||||
|
`${codeToInject}</head>`
|
||||||
|
)
|
||||||
|
|
||||||
return res.send(response)
|
return res.send(injectedContent)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
|||||||
@@ -125,8 +125,27 @@ const verifyCORS = (): string[] => {
|
|||||||
|
|
||||||
if (CORS) {
|
if (CORS) {
|
||||||
const corsTypes = Object.values(CorsType)
|
const corsTypes = Object.values(CorsType)
|
||||||
|
|
||||||
if (!corsTypes.includes(CORS as CorsType))
|
if (!corsTypes.includes(CORS as CorsType))
|
||||||
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
|
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
|
||||||
|
|
||||||
|
if (CORS === CorsType.ENABLED) {
|
||||||
|
const { WHITELIST } = process.env
|
||||||
|
|
||||||
|
const urls = WHITELIST?.trim()
|
||||||
|
.split(' ')
|
||||||
|
.filter((url) => !!url)
|
||||||
|
if (urls?.length) {
|
||||||
|
urls.forEach((url) => {
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://'))
|
||||||
|
errors.push(
|
||||||
|
`- CORS '${CORS}'\n - provided WHITELIST ${url} is not valid`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
errors.push(`- CORS '${CORS}'\n - provide at least one WHITELIST URL`)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
process.env.CORS =
|
process.env.CORS =
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function App() {
|
|||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Header />
|
<Header />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Login />} />
|
<Route path="*" element={<Login />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ import React, { useState, useEffect, useContext } from 'react'
|
|||||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
AppBar,
|
AppBar,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Button,
|
Button,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem
|
MenuItem,
|
||||||
|
IconButton,
|
||||||
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
import { OpenInNew, Settings, Menu as MenuIcon } from '@mui/icons-material'
|
||||||
import SettingsIcon from '@mui/icons-material/Settings'
|
|
||||||
|
|
||||||
import Username from './username'
|
import Username from './username'
|
||||||
import { AppContext } from '../context/appContext'
|
import { AppContext } from '../context/appContext'
|
||||||
@@ -30,31 +32,38 @@ const Header = (props: any) => {
|
|||||||
const [tabValue, setTabValue] = useState(
|
const [tabValue, setTabValue] = useState(
|
||||||
validTabs.includes(pathname) ? pathname : '/'
|
validTabs.includes(pathname) ? pathname : '/'
|
||||||
)
|
)
|
||||||
const [anchorEl, setAnchorEl] = useState<
|
|
||||||
(EventTarget & HTMLButtonElement) | null
|
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(null)
|
||||||
>(null)
|
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorElNav(event.currentTarget)
|
||||||
|
}
|
||||||
|
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorElUser(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseNavMenu = () => {
|
||||||
|
setAnchorElNav(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseUserMenu = () => {
|
||||||
|
setAnchorElUser(null)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTabValue(validTabs.includes(pathname) ? pathname : '/')
|
setTabValue(validTabs.includes(pathname) ? pathname : '/')
|
||||||
}, [pathname])
|
}, [pathname])
|
||||||
|
|
||||||
const handleMenu = (
|
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
||||||
) => {
|
|
||||||
setAnchorEl(event.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
||||||
setTabValue(value)
|
setTabValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
if (appContext.logout) {
|
if (appContext.logout) {
|
||||||
handleClose()
|
handleCloseUserMenu()
|
||||||
appContext.logout()
|
appContext.logout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,43 +73,129 @@ const Header = (props: any) => {
|
|||||||
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
>
|
>
|
||||||
<Toolbar variant="dense">
|
<Toolbar variant="dense">
|
||||||
<img
|
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
|
||||||
src="logo.png"
|
<img
|
||||||
alt="logo"
|
src="logo.png"
|
||||||
style={{
|
alt="logo"
|
||||||
width: '35px',
|
style={{
|
||||||
cursor: 'pointer',
|
width: '35px',
|
||||||
marginRight: '25px'
|
height: '35px',
|
||||||
}}
|
marginTop: '9px',
|
||||||
onClick={() => {
|
cursor: 'pointer',
|
||||||
setTabValue('/')
|
marginRight: '25px'
|
||||||
navigate('/')
|
}}
|
||||||
}}
|
onClick={() => {
|
||||||
/>
|
setTabValue('/')
|
||||||
<Tabs
|
navigate('/')
|
||||||
indicatorColor="secondary"
|
}}
|
||||||
value={tabValue}
|
|
||||||
onChange={handleTabChange}
|
|
||||||
>
|
|
||||||
<Tab label="Home" value="/" to="/" component={Link} />
|
|
||||||
<Tab
|
|
||||||
label="Studio"
|
|
||||||
value="/SASjsStudio"
|
|
||||||
to="/SASjsStudio"
|
|
||||||
component={Link}
|
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
<Tabs
|
||||||
<Button
|
indicatorColor="secondary"
|
||||||
href={`${baseUrl}/AppStream`}
|
value={tabValue}
|
||||||
target="_blank"
|
onChange={handleTabChange}
|
||||||
rel="noreferrer"
|
>
|
||||||
variant="contained"
|
<Tab label="Home" value="/" to="/" component={Link} />
|
||||||
color="primary"
|
<Tab
|
||||||
size="large"
|
label="Studio"
|
||||||
endIcon={<OpenInNewIcon />}
|
value="/SASjsStudio"
|
||||||
>
|
to="/SASjsStudio"
|
||||||
Apps
|
component={Link}
|
||||||
</Button>
|
/>
|
||||||
|
</Tabs>
|
||||||
|
<Button
|
||||||
|
href={`${baseUrl}/AppStream`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
endIcon={<OpenInNew />}
|
||||||
|
>
|
||||||
|
Apps
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}>
|
||||||
|
<IconButton size="large" onClick={handleOpenNavMenu} color="inherit">
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
id="menu-appbar"
|
||||||
|
anchorEl={anchorElNav}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
open={!!anchorElNav}
|
||||||
|
onClose={handleCloseNavMenu}
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'block', md: 'none' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
onClick={handleCloseNavMenu}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/SASjsStudio"
|
||||||
|
onClick={handleCloseNavMenu}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Studio
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
href={`${baseUrl}/AppStream`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
onClick={handleCloseNavMenu}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
endIcon={<OpenInNew />}
|
||||||
|
>
|
||||||
|
Apps
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: { xs: 'flex', md: 'none' } }}>
|
||||||
|
<img
|
||||||
|
src="logo.png"
|
||||||
|
alt="logo"
|
||||||
|
style={{
|
||||||
|
width: '35px',
|
||||||
|
height: '35px',
|
||||||
|
marginTop: '2px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
marginRight: '25px'
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setTabValue('/')
|
||||||
|
navigate('/')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -110,11 +205,11 @@ const Header = (props: any) => {
|
|||||||
>
|
>
|
||||||
<Username
|
<Username
|
||||||
username={appContext.displayName || appContext.username}
|
username={appContext.displayName || appContext.username}
|
||||||
onClickHandler={handleMenu}
|
onClickHandler={handleOpenUserMenu}
|
||||||
/>
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
id="menu-appbar"
|
id="menu-appbar"
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorElUser}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: 'bottom',
|
vertical: 'bottom',
|
||||||
horizontal: 'center'
|
horizontal: 'center'
|
||||||
@@ -124,17 +219,30 @@ const Header = (props: any) => {
|
|||||||
vertical: 'top',
|
vertical: 'top',
|
||||||
horizontal: 'center'
|
horizontal: 'center'
|
||||||
}}
|
}}
|
||||||
open={!!anchorEl}
|
open={!!anchorElUser}
|
||||||
onClose={handleClose}
|
onClose={handleCloseUserMenu}
|
||||||
>
|
>
|
||||||
|
{appContext.loggedIn && (
|
||||||
|
<MenuItem
|
||||||
|
sx={{ justifyContent: 'center', display: { md: 'none' } }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
sx={{ border: '1px solid black', padding: '5px' }}
|
||||||
|
>
|
||||||
|
{appContext.displayName || appContext.username}
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/SASjsSettings"
|
to="/SASjsSettings"
|
||||||
onClick={handleClose}
|
onClick={handleCloseUserMenu}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<SettingsIcon />}
|
startIcon={<Settings />}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
@@ -147,7 +255,7 @@ const Header = (props: any) => {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
size="large"
|
size="large"
|
||||||
color="primary"
|
color="primary"
|
||||||
endIcon={<OpenInNewIcon />}
|
endIcon={<OpenInNew />}
|
||||||
>
|
>
|
||||||
Docs
|
Docs
|
||||||
</Button>
|
</Button>
|
||||||
@@ -160,16 +268,21 @@ const Header = (props: any) => {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
endIcon={<OpenInNewIcon />}
|
endIcon={<OpenInNew />}
|
||||||
>
|
>
|
||||||
API
|
API
|
||||||
</Button>
|
</Button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
{appContext.loggedIn && (
|
||||||
<Button variant="contained" color="primary">
|
<MenuItem
|
||||||
Logout
|
onClick={handleLogout}
|
||||||
</Button>
|
sx={{ justifyContent: 'center' }}
|
||||||
</MenuItem>
|
>
|
||||||
|
<Button variant="contained" color="primary">
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|||||||
@@ -20,7 +20,14 @@ const Username = (props: any) => {
|
|||||||
) : (
|
) : (
|
||||||
<AccountCircle></AccountCircle>
|
<AccountCircle></AccountCircle>
|
||||||
)}
|
)}
|
||||||
<Typography variant="h6" sx={{ color: 'white', padding: '0 8px' }}>
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
padding: '0 8px',
|
||||||
|
display: { xs: 'none', md: 'flex' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
{props.username}
|
{props.username}
|
||||||
</Typography>
|
</Typography>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|||||||
type AddPermissionModalProps = {
|
type AddPermissionModalProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||||
addPermission: (addPermissionPayload: RegisterPermissionPayload) => void
|
addPermission: (
|
||||||
|
permissions: RegisterPermissionPayload[],
|
||||||
|
permissionType: string,
|
||||||
|
principalType: string,
|
||||||
|
principal: string,
|
||||||
|
permissionSetting: string
|
||||||
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddPermissionModal = ({
|
const AddPermissionModal = ({
|
||||||
@@ -40,10 +46,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 [selectedPaths, setSelectedPaths] = 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,29 +59,29 @@ 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)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoadingPrincipals(true)
|
setLoadingPrincipals(true)
|
||||||
axios
|
axios
|
||||||
.get(`/SASjsApi/${principalType}`)
|
.get(`/SASjsApi/${principalType.toLowerCase()}`)
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
if (principalType === 'user') {
|
if (principalType.toLowerCase() === 'user') {
|
||||||
const users: UserResponse[] = res.data
|
const users: UserResponse[] = res.data
|
||||||
const nonAdminUsers = users.filter((user) => !user.isAdmin)
|
const nonAdminUsers = users.filter((user) => !user.isAdmin)
|
||||||
setUserPrincipals(nonAdminUsers)
|
setUserPrincipals(nonAdminUsers)
|
||||||
@@ -92,21 +99,40 @@ const AddPermissionModal = ({
|
|||||||
}, [principalType])
|
}, [principalType])
|
||||||
|
|
||||||
const handleAddPermission = () => {
|
const handleAddPermission = () => {
|
||||||
const addPermissionPayload: any = {
|
const permissions: RegisterPermissionPayload[] = []
|
||||||
uri,
|
|
||||||
setting: permissionSetting,
|
selectedPaths.forEach((path) => {
|
||||||
principalType
|
const addPermissionPayload: any = {
|
||||||
}
|
path,
|
||||||
if (principalType === 'user' && userPrincipal) {
|
type: permissionType,
|
||||||
addPermissionPayload.principalId = userPrincipal.id
|
setting: permissionSetting,
|
||||||
} else if (principalType === 'group' && groupPrincipal) {
|
principalType: principalType.toLowerCase(),
|
||||||
addPermissionPayload.principalId = groupPrincipal.groupId
|
principalId:
|
||||||
}
|
principalType.toLowerCase() === 'user'
|
||||||
addPermission(addPermissionPayload)
|
? userPrincipal?.id
|
||||||
|
: groupPrincipal?.groupId
|
||||||
|
}
|
||||||
|
|
||||||
|
permissions.push(addPermissionPayload)
|
||||||
|
})
|
||||||
|
|
||||||
|
const principal =
|
||||||
|
principalType.toLowerCase() === 'user'
|
||||||
|
? userPrincipal?.username
|
||||||
|
: groupPrincipal?.name
|
||||||
|
|
||||||
|
addPermission(
|
||||||
|
permissions,
|
||||||
|
permissionType,
|
||||||
|
principalType,
|
||||||
|
principal!,
|
||||||
|
permissionSetting
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const addButtonDisabled =
|
const addButtonDisabled =
|
||||||
!uri || (principalType === 'user' ? !userPrincipal : !groupPrincipal)
|
!selectedPaths.length ||
|
||||||
|
(principalType.toLowerCase() === 'user' ? !userPrincipal : !groupPrincipal)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
||||||
@@ -120,22 +146,37 @@ const AddPermissionModal = ({
|
|||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={URIs}
|
multiple
|
||||||
disableClearable
|
disableClearable
|
||||||
value={uri}
|
options={paths}
|
||||||
onChange={(event: any, newValue: string) => setUri(newValue)}
|
filterSelectedOptions
|
||||||
|
value={selectedPaths}
|
||||||
|
onChange={(event: any, newValue: string[]) => {
|
||||||
|
setSelectedPaths(newValue)
|
||||||
|
}}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Paths" />}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Autocomplete
|
||||||
|
options={['Route']}
|
||||||
|
disableClearable
|
||||||
|
value={permissionType}
|
||||||
|
onChange={(event: any, newValue: string) =>
|
||||||
|
setPermissionType(newValue)
|
||||||
|
}
|
||||||
renderInput={(params) =>
|
renderInput={(params) =>
|
||||||
loadingURIs ? (
|
loadingPaths ? (
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
) : (
|
) : (
|
||||||
<TextField {...params} label="Principal" />
|
<TextField {...params} label="Permission Type" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={['user', 'group']}
|
options={['Group', 'User']}
|
||||||
disableClearable
|
disableClearable
|
||||||
value={principalType}
|
value={principalType}
|
||||||
onChange={(event: any, newValue: string) =>
|
onChange={(event: any, newValue: string) =>
|
||||||
@@ -147,7 +188,7 @@ const AddPermissionModal = ({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
{principalType === 'user' ? (
|
{principalType.toLowerCase() === 'user' ? (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={userPrincipals}
|
options={userPrincipals}
|
||||||
getOptionLabel={(option) => option.displayName}
|
getOptionLabel={(option) => option.displayName}
|
||||||
|
|||||||
120
web/src/containers/Settings/addPermissionResponseModal.tsx
Normal file
120
web/src/containers/Settings/addPermissionResponseModal.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { Typography, DialogContent } from '@mui/material'
|
||||||
|
|
||||||
|
import { BootstrapDialog } from '../../components/modal'
|
||||||
|
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
||||||
|
import { PermissionResponse } from '../../utils/types'
|
||||||
|
|
||||||
|
export interface PermissionResponsePayload {
|
||||||
|
permissionType: string
|
||||||
|
principalType: string
|
||||||
|
principal: string
|
||||||
|
permissionSetting: string
|
||||||
|
existingPermissions: PermissionResponse[]
|
||||||
|
newAddedPermissions: PermissionResponse[]
|
||||||
|
updatedPermissions: PermissionResponse[]
|
||||||
|
errorPaths: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
payload: PermissionResponsePayload
|
||||||
|
}
|
||||||
|
|
||||||
|
const PermissionResponseModal = ({ open, setOpen, payload }: Props) => {
|
||||||
|
const newAddedPermissionsLength = payload.newAddedPermissions.length
|
||||||
|
const updatedPermissionsLength = payload.updatedPermissions.length
|
||||||
|
const existingPermissionsLength = payload.existingPermissions.length
|
||||||
|
const appliedPermissionsLength =
|
||||||
|
newAddedPermissionsLength + updatedPermissionsLength
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
||||||
|
<BootstrapDialogTitle
|
||||||
|
id="permission-response-modal"
|
||||||
|
handleOpen={setOpen}
|
||||||
|
>
|
||||||
|
Permission Response
|
||||||
|
</BootstrapDialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Typography sx={{ fontWeight: 'bold', marginBottom: '15px' }}>
|
||||||
|
{`${appliedPermissionsLength} "${payload.permissionSetting}", "${
|
||||||
|
payload.permissionType
|
||||||
|
}", "${payload.principalType}", "${payload.principal}" ${
|
||||||
|
appliedPermissionsLength > 1 ? 'Rules' : 'Rule'
|
||||||
|
}`}{' '}
|
||||||
|
Applied:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{newAddedPermissionsLength > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography>
|
||||||
|
{`${newAddedPermissionsLength} ${
|
||||||
|
newAddedPermissionsLength > 1 ? 'Rules' : 'Rule'
|
||||||
|
}`}{' '}
|
||||||
|
Added:
|
||||||
|
</Typography>
|
||||||
|
<ul>
|
||||||
|
{payload.newAddedPermissions.map((permission, index) => (
|
||||||
|
<li key={index}>{permission.path}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{updatedPermissionsLength > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography>
|
||||||
|
{` ${updatedPermissionsLength} ${
|
||||||
|
updatedPermissionsLength > 1 ? 'Rules' : 'Rule'
|
||||||
|
}`}{' '}
|
||||||
|
Updated:
|
||||||
|
</Typography>
|
||||||
|
<ul>
|
||||||
|
{payload.updatedPermissions.map((permission, index) => (
|
||||||
|
<li key={index}>{permission.path}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{existingPermissionsLength > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography>
|
||||||
|
{`${existingPermissionsLength} ${
|
||||||
|
existingPermissionsLength > 1 ? 'Rules' : 'Rule'
|
||||||
|
}`}{' '}
|
||||||
|
Unchanged:
|
||||||
|
</Typography>
|
||||||
|
<ul>
|
||||||
|
{payload.existingPermissions.map((permission, index) => (
|
||||||
|
<li key={index}>{permission.path}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{payload.errorPaths.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography style={{ color: 'red', marginTop: '10px' }}>
|
||||||
|
Errors occurred for following paths:
|
||||||
|
</Typography>
|
||||||
|
<ul>
|
||||||
|
{payload.errorPaths.map((path, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<Typography>{path}</Typography>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</BootstrapDialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PermissionResponseModal
|
||||||
@@ -31,11 +31,20 @@ const Settings = () => {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', md: 'row' },
|
||||||
marginTop: '65px'
|
marginTop: '65px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabContext value={value}>
|
<TabContext value={value}>
|
||||||
<Box component={Paper} sx={{ margin: '0 5px', height: '92vh' }}>
|
<Box
|
||||||
|
component={Paper}
|
||||||
|
sx={{
|
||||||
|
margin: '0 5px',
|
||||||
|
height: { md: '92vh' },
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TabList
|
<TabList
|
||||||
TabIndicatorProps={{
|
TabIndicatorProps={{
|
||||||
style: {
|
style: {
|
||||||
@@ -47,7 +56,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="Permissions" value="permission" />
|
||||||
)}
|
)}
|
||||||
</TabList>
|
</TabList>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import { styled } from '@mui/material/styles'
|
|||||||
import Modal from '../../components/modal'
|
import Modal from '../../components/modal'
|
||||||
import PermissionFilterModal from './permissionFilterModal'
|
import PermissionFilterModal from './permissionFilterModal'
|
||||||
import AddPermissionModal from './addPermissionModal'
|
import AddPermissionModal from './addPermissionModal'
|
||||||
|
import PermissionResponseModal, {
|
||||||
|
PermissionResponsePayload
|
||||||
|
} from './addPermissionResponseModal'
|
||||||
import UpdatePermissionModal from './updatePermissionModal'
|
import UpdatePermissionModal from './updatePermissionModal'
|
||||||
import DeleteConfirmationModal from '../../components/deleteConfirmationModal'
|
import DeleteConfirmationModal from '../../components/deleteConfirmationModal'
|
||||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||||
@@ -36,12 +39,23 @@ import {
|
|||||||
PermissionResponse,
|
PermissionResponse,
|
||||||
RegisterPermissionPayload
|
RegisterPermissionPayload
|
||||||
} from '../../utils/types'
|
} from '../../utils/types'
|
||||||
|
import {
|
||||||
|
findExistingPermission,
|
||||||
|
findUpdatingPermission
|
||||||
|
} from '../../utils/helper'
|
||||||
|
|
||||||
import { AppContext } from '../../context/appContext'
|
import { AppContext } from '../../context/appContext'
|
||||||
|
|
||||||
const BootstrapTableCell = styled(TableCell)({
|
const BootstrapTableCell = styled(TableCell)({
|
||||||
textAlign: 'left'
|
textAlign: 'left'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const BootstrapGridItem = styled(Grid)({
|
||||||
|
'&.MuiGrid-item': {
|
||||||
|
maxWidth: '100%'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export enum PrincipalType {
|
export enum PrincipalType {
|
||||||
User = 'User',
|
User = 'User',
|
||||||
Group = 'Group'
|
Group = 'Group'
|
||||||
@@ -59,6 +73,20 @@ const Permission = () => {
|
|||||||
AlertSeverityType.Success
|
AlertSeverityType.Success
|
||||||
)
|
)
|
||||||
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
||||||
|
const [openPermissionResponseModal, setOpenPermissionResponseModal] =
|
||||||
|
useState(false)
|
||||||
|
const [permissionResponsePayload, setPermissionResponsePayload] =
|
||||||
|
useState<PermissionResponsePayload>({
|
||||||
|
permissionType: '',
|
||||||
|
principalType: '',
|
||||||
|
principal: '',
|
||||||
|
permissionSetting: '',
|
||||||
|
existingPermissions: [],
|
||||||
|
newAddedPermissions: [],
|
||||||
|
updatedPermissions: [],
|
||||||
|
errorPaths: []
|
||||||
|
})
|
||||||
|
|
||||||
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
||||||
useState(false)
|
useState(false)
|
||||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||||
@@ -68,7 +96,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 +139,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,36 +202,84 @@ const Permission = () => {
|
|||||||
|
|
||||||
const resetFilter = () => {
|
const resetFilter = () => {
|
||||||
setFilterModalOpen(false)
|
setFilterModalOpen(false)
|
||||||
setUriFilter([])
|
setPathFilter([])
|
||||||
setPrincipalFilter([])
|
setPrincipalFilter([])
|
||||||
setSettingFilter([])
|
setSettingFilter([])
|
||||||
setFilteredPermissions([])
|
setFilteredPermissions([])
|
||||||
setFilterApplied(false)
|
setFilterApplied(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const addPermission = (addPermissionPayload: RegisterPermissionPayload) => {
|
const addPermission = async (
|
||||||
|
permissionsToAdd: RegisterPermissionPayload[],
|
||||||
|
permissionType: string,
|
||||||
|
principalType: string,
|
||||||
|
principal: string,
|
||||||
|
permissionSetting: string
|
||||||
|
) => {
|
||||||
setAddPermissionModalOpen(false)
|
setAddPermissionModalOpen(false)
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
axios
|
|
||||||
.post('/SASjsApi/permission', addPermissionPayload)
|
const newAddedPermissions: PermissionResponse[] = []
|
||||||
.then((res: any) => {
|
const updatedPermissions: PermissionResponse[] = []
|
||||||
fetchPermissions()
|
const errorPaths: string[] = []
|
||||||
setSnackbarMessage('Permission added!')
|
|
||||||
setSnackbarSeverity(AlertSeverityType.Success)
|
const existingPermissions: PermissionResponse[] = []
|
||||||
setOpenSnackbar(true)
|
const updatingPermissions: PermissionResponse[] = []
|
||||||
})
|
const newPermissions: RegisterPermissionPayload[] = []
|
||||||
.catch((err) => {
|
|
||||||
setModalTitle('Abort')
|
permissionsToAdd.forEach((permission) => {
|
||||||
setModalPayload(
|
const existingPermission = findExistingPermission(permissions, permission)
|
||||||
typeof err.response.data === 'object'
|
if (existingPermission) {
|
||||||
? JSON.stringify(err.response.data)
|
existingPermissions.push(existingPermission)
|
||||||
: err.response.data
|
return
|
||||||
)
|
}
|
||||||
setOpenModal(true)
|
|
||||||
})
|
const updatingPermission = findUpdatingPermission(permissions, permission)
|
||||||
.finally(() => {
|
if (updatingPermission) {
|
||||||
setIsLoading(false)
|
updatingPermissions.push(updatingPermission)
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPermissions.push(permission)
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const permission of newPermissions) {
|
||||||
|
await axios
|
||||||
|
.post('/SASjsApi/permission', permission)
|
||||||
|
.then((res) => {
|
||||||
|
newAddedPermissions.push(res.data)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorPaths.push(permission.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const permission of updatingPermissions) {
|
||||||
|
await axios
|
||||||
|
.patch(`/SASjsApi/permission/${permission.permissionId}`, {
|
||||||
|
setting: permission.setting === 'Grant' ? 'Deny' : 'Grant'
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
updatedPermissions.push(res.data)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorPaths.push(permission.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPermissions()
|
||||||
|
setIsLoading(false)
|
||||||
|
setPermissionResponsePayload({
|
||||||
|
permissionType,
|
||||||
|
principalType,
|
||||||
|
principal,
|
||||||
|
permissionSetting,
|
||||||
|
existingPermissions,
|
||||||
|
updatedPermissions,
|
||||||
|
newAddedPermissions,
|
||||||
|
errorPaths
|
||||||
|
})
|
||||||
|
setOpenPermissionResponseModal(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdatePermissionClick = (permission: PermissionResponse) => {
|
const handleUpdatePermissionClick = (permission: PermissionResponse) => {
|
||||||
@@ -278,11 +356,11 @@ const Permission = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Box className="permissions-page">
|
<Box className="permissions-page">
|
||||||
<Grid container direction="column" spacing={1}>
|
<Grid container direction="column" spacing={1}>
|
||||||
<Grid item xs={12}>
|
<BootstrapGridItem item xs={12}>
|
||||||
<Paper elevation={3} sx={{ display: 'flex' }}>
|
<Paper elevation={3} sx={{ display: 'flex' }}>
|
||||||
<Tooltip title="Filter Permissions">
|
<Tooltip title="Filter Permissions">
|
||||||
<IconButton>
|
<IconButton onClick={() => setFilterModalOpen(true)}>
|
||||||
<FilterListIcon onClick={() => setFilterModalOpen(true)} />
|
<FilterListIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{appContext.isAdmin && (
|
{appContext.isAdmin && (
|
||||||
@@ -297,14 +375,14 @@ const Permission = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</BootstrapGridItem>
|
||||||
<Grid item xs={12}>
|
<BootstrapGridItem item xs={12}>
|
||||||
<PermissionTable
|
<PermissionTable
|
||||||
permissions={filterApplied ? filteredPermissions : permissions}
|
permissions={filterApplied ? filteredPermissions : permissions}
|
||||||
handleUpdatePermissionClick={handleUpdatePermissionClick}
|
handleUpdatePermissionClick={handleUpdatePermissionClick}
|
||||||
handleDeletePermissionClick={handleDeletePermissionClick}
|
handleDeletePermissionClick={handleDeletePermissionClick}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</BootstrapGridItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
<BootstrapSnackbar
|
<BootstrapSnackbar
|
||||||
open={openSnackbar}
|
open={openSnackbar}
|
||||||
@@ -322,8 +400,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}
|
||||||
@@ -338,6 +416,11 @@ const Permission = () => {
|
|||||||
handleOpen={setAddPermissionModalOpen}
|
handleOpen={setAddPermissionModalOpen}
|
||||||
addPermission={addPermission}
|
addPermission={addPermission}
|
||||||
/>
|
/>
|
||||||
|
<PermissionResponseModal
|
||||||
|
open={openPermissionResponseModal}
|
||||||
|
setOpen={setOpenPermissionResponseModal}
|
||||||
|
payload={permissionResponsePayload}
|
||||||
|
/>
|
||||||
<UpdatePermissionModal
|
<UpdatePermissionModal
|
||||||
open={updatePermissionModalOpen}
|
open={updatePermissionModalOpen}
|
||||||
handleOpen={setUpdatePermissionModalOpen}
|
handleOpen={setUpdatePermissionModalOpen}
|
||||||
@@ -374,9 +457,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 +470,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>
|
||||||
@@ -474,8 +559,8 @@ const DisplayGroup = ({ group }: DisplayGroupProps) => {
|
|||||||
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
||||||
Group Members
|
Group Members
|
||||||
</Typography>
|
</Typography>
|
||||||
{group.users.map((user) => (
|
{group.users.map((user, index) => (
|
||||||
<Typography sx={{ p: 1 }} component="li">
|
<Typography key={index} sx={{ p: 1 }} component="li">
|
||||||
{user.username}
|
{user.username}
|
||||||
</Typography>
|
</Typography>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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,13 +86,13 @@ 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="Paths" />}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import React, { useEffect, useRef, useState, useContext } from 'react'
|
import React, {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useContext
|
||||||
|
} from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -58,13 +65,17 @@ const StyledTab = styled(Tab)(() => ({
|
|||||||
type SASjsEditorProps = {
|
type SASjsEditorProps = {
|
||||||
selectedFilePath: string
|
selectedFilePath: string
|
||||||
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
|
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
|
||||||
|
tab: string
|
||||||
|
setTab: Dispatch<SetStateAction<string>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
|
|
||||||
const SASjsEditor = ({
|
const SASjsEditor = ({
|
||||||
selectedFilePath,
|
selectedFilePath,
|
||||||
setSelectedFilePath
|
setSelectedFilePath,
|
||||||
|
tab,
|
||||||
|
setTab
|
||||||
}: SASjsEditorProps) => {
|
}: SASjsEditorProps) => {
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
@@ -81,7 +92,6 @@ const SASjsEditor = ({
|
|||||||
const [log, setLog] = useState('')
|
const [log, setLog] = useState('')
|
||||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||||
const [webout, setWebout] = useState('')
|
const [webout, setWebout] = useState('')
|
||||||
const [tab, setTab] = useState('1')
|
|
||||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||||
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
||||||
@@ -161,7 +171,7 @@ const SASjsEditor = ({
|
|||||||
}
|
}
|
||||||
setLog('')
|
setLog('')
|
||||||
setWebout('')
|
setWebout('')
|
||||||
setTab('1')
|
setTab('code')
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedFilePath])
|
}, [selectedFilePath])
|
||||||
|
|
||||||
@@ -200,10 +210,11 @@ const SASjsEditor = ({
|
|||||||
setLog(parsedLog)
|
setLog(parsedLog)
|
||||||
|
|
||||||
setWebout(`${res.data?._webout}`)
|
setWebout(`${res.data?._webout}`)
|
||||||
setTab('2')
|
setTab('log')
|
||||||
|
|
||||||
// Scroll to bottom of log
|
// Scroll to bottom of log
|
||||||
window.scrollTo(0, document.body.scrollHeight)
|
const logElement = document.getElementById('log')
|
||||||
|
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setModalTitle('Abort')
|
setModalTitle('Abort')
|
||||||
@@ -353,29 +364,24 @@ const SASjsEditor = ({
|
|||||||
sx={{
|
sx={{
|
||||||
borderBottom: 1,
|
borderBottom: 1,
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
position: 'fixed',
|
background: 'white'
|
||||||
background: 'white',
|
|
||||||
width: '85%'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TabList onChange={handleTabChange} centered>
|
<TabList onChange={handleTabChange} centered>
|
||||||
<StyledTab label="Code" value="1" />
|
<StyledTab label="Code" value="code" />
|
||||||
<StyledTab label="Log" value="2" />
|
<StyledTab label="Log" value="log" />
|
||||||
<StyledTab
|
<StyledTab
|
||||||
label={
|
label={
|
||||||
<Tooltip title="Displays content from the _webout fileref">
|
<Tooltip title="Displays content from the _webout fileref">
|
||||||
<Typography>Webout</Typography>
|
<Typography>Webout</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
value="3"
|
value="webout"
|
||||||
/>
|
/>
|
||||||
</TabList>
|
</TabList>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<StyledTabPanel
|
<StyledTabPanel sx={{ paddingBottom: 0 }} value="code">
|
||||||
sx={{ paddingBottom: 0, marginTop: '45px' }}
|
|
||||||
value="1"
|
|
||||||
>
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<RunMenu
|
<RunMenu
|
||||||
fileContent={fileContent}
|
fileContent={fileContent}
|
||||||
@@ -441,14 +447,16 @@ const SASjsEditor = ({
|
|||||||
</p>
|
</p>
|
||||||
</Paper>
|
</Paper>
|
||||||
</StyledTabPanel>
|
</StyledTabPanel>
|
||||||
<StyledTabPanel value="2">
|
<StyledTabPanel value="log">
|
||||||
<div style={{ marginTop: '50px' }}>
|
<div>
|
||||||
<h2>SAS Log</h2>
|
<h2>Log</h2>
|
||||||
<pre>{log}</pre>
|
<pre id="log" style={{ overflow: 'auto', height: '75vh' }}>
|
||||||
|
{log}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</StyledTabPanel>
|
</StyledTabPanel>
|
||||||
<StyledTabPanel value="3">
|
<StyledTabPanel value="webout">
|
||||||
<div style={{ marginTop: '50px' }}>
|
<div>
|
||||||
<pre>{webout}</pre>
|
<pre>{webout}</pre>
|
||||||
</div>
|
</div>
|
||||||
</StyledTabPanel>
|
</StyledTabPanel>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const Studio = () => {
|
|||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const [selectedFilePath, setSelectedFilePath] = useState('')
|
const [selectedFilePath, setSelectedFilePath] = useState('')
|
||||||
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
||||||
|
const [tab, setTab] = useState('code')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedFilePath(searchParams.get('filePath') ?? '')
|
setSelectedFilePath(searchParams.get('filePath') ?? '')
|
||||||
@@ -83,16 +84,20 @@ const Studio = () => {
|
|||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<SideBar
|
{tab === 'code' && (
|
||||||
selectedFilePath={selectedFilePath}
|
<SideBar
|
||||||
directoryData={directoryData}
|
selectedFilePath={selectedFilePath}
|
||||||
handleSelect={handleSelect}
|
directoryData={directoryData}
|
||||||
removeFileFromTree={removeFileFromTree}
|
handleSelect={handleSelect}
|
||||||
refreshSideBar={fetchDirectoryData}
|
removeFileFromTree={removeFileFromTree}
|
||||||
/>
|
refreshSideBar={fetchDirectoryData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SASjsEditor
|
<SASjsEditor
|
||||||
selectedFilePath={selectedFilePath}
|
selectedFilePath={selectedFilePath}
|
||||||
setSelectedFilePath={handleSelect}
|
setSelectedFilePath={handleSelect}
|
||||||
|
tab={tab}
|
||||||
|
setTab={setTab}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import React, { useState, useMemo } from 'react'
|
import React, { useState, useMemo } from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Backdrop, Box, CircularProgress, Drawer, Toolbar } from '@mui/material'
|
import {
|
||||||
|
Backdrop,
|
||||||
|
Box,
|
||||||
|
Paper,
|
||||||
|
CircularProgress,
|
||||||
|
Drawer,
|
||||||
|
Toolbar,
|
||||||
|
IconButton
|
||||||
|
} from '@mui/material'
|
||||||
|
import { FolderOpen } from '@mui/icons-material'
|
||||||
|
|
||||||
import TreeView from '../../components/tree'
|
import TreeView from '../../components/tree'
|
||||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||||
@@ -33,6 +42,17 @@ const SideBar = ({
|
|||||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||||
AlertSeverityType.Success
|
AlertSeverityType.Success
|
||||||
)
|
)
|
||||||
|
const [mobileOpen, setMobileOpen] = React.useState(false)
|
||||||
|
|
||||||
|
const handleDrawerToggle = () => {
|
||||||
|
setMobileOpen(!mobileOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileSelect = (filePath: string) => {
|
||||||
|
setMobileOpen(false)
|
||||||
|
handleSelect(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
const defaultExpanded = useMemo(() => {
|
const defaultExpanded = useMemo(() => {
|
||||||
const splittedPath = selectedFilePath.split('/')
|
const splittedPath = selectedFilePath.split('/')
|
||||||
const arr = ['']
|
const arr = ['']
|
||||||
@@ -147,15 +167,8 @@ const SideBar = ({
|
|||||||
.finally(() => setIsLoading(false))
|
.finally(() => setIsLoading(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const drawer = (
|
||||||
<Drawer
|
<div>
|
||||||
variant="permanent"
|
|
||||||
sx={{
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Backdrop
|
<Backdrop
|
||||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
open={isLoading}
|
open={isLoading}
|
||||||
@@ -168,7 +181,7 @@ const SideBar = ({
|
|||||||
<TreeView
|
<TreeView
|
||||||
node={directoryData}
|
node={directoryData}
|
||||||
selectedFilePath={selectedFilePath}
|
selectedFilePath={selectedFilePath}
|
||||||
handleSelect={handleSelect}
|
handleSelect={handleFileSelect}
|
||||||
deleteNode={deleteNode}
|
deleteNode={deleteNode}
|
||||||
addFile={addFile}
|
addFile={addFile}
|
||||||
addFolder={addFolder}
|
addFolder={addFolder}
|
||||||
@@ -189,7 +202,65 @@ const SideBar = ({
|
|||||||
title={modalTitle}
|
title={modalTitle}
|
||||||
payload={modalPayload}
|
payload={modalPayload}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
component={Paper}
|
||||||
|
sx={{
|
||||||
|
margin: '5px',
|
||||||
|
height: '97vh',
|
||||||
|
paddingTop: '45px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
color="inherit"
|
||||||
|
size="large"
|
||||||
|
aria-label="open drawer"
|
||||||
|
edge="start"
|
||||||
|
onClick={handleDrawerToggle}
|
||||||
|
sx={{ left: '5px', display: { md: 'none' } }}
|
||||||
|
>
|
||||||
|
<FolderOpen />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Drawer
|
||||||
|
variant="temporary"
|
||||||
|
open={mobileOpen}
|
||||||
|
onClose={handleDrawerToggle}
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true // Better open performance on mobile.
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'block', md: 'none' },
|
||||||
|
flexShrink: 0,
|
||||||
|
[`& .MuiDrawer-paper`]: {
|
||||||
|
width: 240,
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{drawer}
|
||||||
|
</Drawer>
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'none', md: 'block' },
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
[`& .MuiDrawer-paper`]: {
|
||||||
|
width: drawerWidth,
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{drawer}
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,18 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoggedIn(false)
|
setLoggedIn(false)
|
||||||
axios.get('/') // get CSRF TOKEN
|
// get CSRF TOKEN and set cookie
|
||||||
|
axios
|
||||||
|
.get('/')
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((data: string) => {
|
||||||
|
const result =
|
||||||
|
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
|
||||||
|
data
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
if (result) document.cookie = result
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
axios
|
axios
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
margin-top: 50px;
|
margin: 50px 10px 0 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
59
web/src/utils/helper.ts
Normal file
59
web/src/utils/helper.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { PermissionResponse, RegisterPermissionPayload } from './types'
|
||||||
|
|
||||||
|
export const findExistingPermission = (
|
||||||
|
existingPermissions: PermissionResponse[],
|
||||||
|
newPermission: RegisterPermissionPayload
|
||||||
|
) => {
|
||||||
|
for (const permission of existingPermissions) {
|
||||||
|
if (
|
||||||
|
permission.user?.id === newPermission.principalId &&
|
||||||
|
hasSameCombination(permission, newPermission)
|
||||||
|
)
|
||||||
|
return permission
|
||||||
|
|
||||||
|
if (
|
||||||
|
permission.group?.groupId === newPermission.principalId &&
|
||||||
|
hasSameCombination(permission, newPermission)
|
||||||
|
)
|
||||||
|
return permission
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findUpdatingPermission = (
|
||||||
|
existingPermissions: PermissionResponse[],
|
||||||
|
newPermission: RegisterPermissionPayload
|
||||||
|
) => {
|
||||||
|
for (const permission of existingPermissions) {
|
||||||
|
if (
|
||||||
|
permission.user?.id === newPermission.principalId &&
|
||||||
|
hasDifferentSetting(permission, newPermission)
|
||||||
|
)
|
||||||
|
return permission
|
||||||
|
|
||||||
|
if (
|
||||||
|
permission.group?.groupId === newPermission.principalId &&
|
||||||
|
hasDifferentSetting(permission, newPermission)
|
||||||
|
)
|
||||||
|
return permission
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSameCombination = (
|
||||||
|
existingPermission: PermissionResponse,
|
||||||
|
newPermission: RegisterPermissionPayload
|
||||||
|
) =>
|
||||||
|
existingPermission.path === newPermission.path &&
|
||||||
|
existingPermission.type === newPermission.type &&
|
||||||
|
existingPermission.setting === newPermission.setting
|
||||||
|
|
||||||
|
const hasDifferentSetting = (
|
||||||
|
existingPermission: PermissionResponse,
|
||||||
|
newPermission: RegisterPermissionPayload
|
||||||
|
) =>
|
||||||
|
existingPermission.path === newPermission.path &&
|
||||||
|
existingPermission.type === newPermission.type &&
|
||||||
|
existingPermission.setting !== newPermission.setting
|
||||||
@@ -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