From 5d5a9d3788281d75c56f68f0dff231abc9c9c275 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 1 Aug 2022 21:33:10 +0500 Subject: [PATCH] fix: update schema of Permission --- api/public/swagger.yaml | 451 +++++++++--------- api/src/controllers/info.ts | 8 +- api/src/controllers/permission.ts | 61 ++- api/src/middlewares/authorize.ts | 26 +- api/src/model/Permission.ts | 12 +- api/src/routes/api/spec/drive.spec.ts | 44 +- api/src/routes/api/spec/permission.spec.ts | 129 +++-- api/src/routes/api/spec/stp.spec.ts | 8 +- api/src/utils/getAuthorizedRoutes.ts | 4 +- api/src/utils/validation.ts | 15 +- .../Settings/addPermissionModal.tsx | 50 +- web/src/containers/Settings/permission.tsx | 22 +- .../Settings/permissionFilterModal.tsx | 18 +- web/src/utils/types.ts | 6 +- 14 files changed, 488 insertions(+), 366 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 022e605..1e077bc 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -470,12 +470,89 @@ components: additionalProperties: false AuthorizedRoutesResponse: properties: - URIs: + paths: items: type: string type: array 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 additionalProperties: false ExecuteReturnJsonPayload: @@ -521,71 +598,6 @@ components: - clientId type: object additionalProperties: false - PermissionDetailsResponse: - properties: - permissionId: - type: number - format: double - uri: - type: string - setting: - type: string - user: - $ref: '#/components/schemas/UserResponse' - group: - $ref: '#/components/schemas/GroupDetailsResponse' - required: - - permissionId - - uri - - setting - type: object - additionalProperties: false - PermissionSetting: - enum: - - Grant - - Deny - type: string - PrincipalType: - enum: - - user - - group - type: string - RegisterPermissionPayload: - properties: - uri: - type: string - description: 'Name of affected resource' - example: /SASjsApi/code/execute - setting: - $ref: '#/components/schemas/PermissionSetting' - description: 'The indication of whether (and to what extent) access is provided' - example: Grant - principalType: - $ref: '#/components/schemas/PrincipalType' - description: 'Indicates the type of principal' - example: user - principalId: - type: number - format: double - description: 'The id of user or group to which a rule is assigned.' - example: 123 - required: - - uri - - setting - - principalType - - principalId - type: object - additionalProperties: false - UpdatePermissionPayload: - properties: - setting: - $ref: '#/components/schemas/PermissionSetting' - description: 'The indication of whether (and to what extent) access is provided' - example: Grant - required: - - setting - type: object - additionalProperties: false securitySchemes: bearerAuth: type: http @@ -1598,12 +1610,165 @@ paths: $ref: '#/components/schemas/AuthorizedRoutesResponse' examples: 'Example 1': - value: { URIs: [/AppStream, /SASjsApi/stp/execute] } - summary: 'Get authorized routes.' + value: { paths: [/AppStream, /SASjsApi/stp/execute] } + summary: 'Get the list of available routes to which permissions can be applied. Used to populate the dialog in the URI Permissions feature.' tags: - Info security: [] parameters: [] + /SASjsApi/permission: + get: + operationId: GetAllPermissions + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/PermissionDetailsResponse' + type: array + examples: + 'Example 1': + value: + [ + { + permissionId: 123, + 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: get: operationId: Session @@ -1788,154 +1953,6 @@ paths: - Web security: [] parameters: [] - /SASjsApi/permission: - get: - operationId: GetAllPermissions - responses: - '200': - description: Ok - content: - application/json: - schema: - items: - $ref: '#/components/schemas/PermissionDetailsResponse' - type: array - examples: - 'Example 1': - value: - [ - { - permissionId: 123, - uri: /SASjsApi/code/execute, - setting: Grant, - user: - { - id: 1, - username: johnSnow01, - displayName: 'John Snow', - isAdmin: false - } - }, - { - permissionId: 124, - uri: /SASjsApi/code/execute, - setting: Grant, - group: - { - groupId: 1, - name: DCGroup, - description: 'This group represents Data Controller Users', - isActive: true, - users: [] - } - } - ] - summary: 'Get list of all permissions (uri, setting and userDetail).' - tags: - - Permission - security: - - bearerAuth: [] - parameters: [] - post: - operationId: CreatePermission - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/PermissionDetailsResponse' - examples: - 'Example 1': - value: - { - permissionId: 123, - uri: /SASjsApi/code/execute, - setting: Grant, - user: - { - id: 1, - username: johnSnow01, - displayName: 'John Snow', - isAdmin: false - } - } - summary: 'Create a new permission. Admin only.' - tags: - - Permission - security: - - bearerAuth: [] - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RegisterPermissionPayload' - '/SASjsApi/permission/{permissionId}': - patch: - operationId: UpdatePermission - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/PermissionDetailsResponse' - examples: - 'Example 1': - value: - { - permissionId: 123, - uri: /SASjsApi/code/execute, - setting: Grant, - user: - { - id: 1, - username: johnSnow01, - displayName: 'John Snow', - isAdmin: false - } - } - summary: 'Update permission setting. Admin only' - tags: - - Permission - security: - - bearerAuth: [] - parameters: - - description: "The permission's identifier" - in: path - name: permissionId - required: true - schema: - format: double - type: number - example: 1234 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdatePermissionPayload' - delete: - operationId: DeletePermission - responses: - '204': - description: 'No content' - summary: 'Delete a permission. Admin only.' - tags: - - Permission - security: - - bearerAuth: [] - parameters: - - description: "The user's identifier" - in: path - name: permissionId - required: true - schema: - format: double - type: number - example: 1234 servers: - url: / tags: diff --git a/api/src/controllers/info.ts b/api/src/controllers/info.ts index 08cd1d5..26bd276 100644 --- a/api/src/controllers/info.ts +++ b/api/src/controllers/info.ts @@ -1,7 +1,7 @@ import { Route, Tags, Example, Get } from 'tsoa' import { getAuthorizedRoutes } from '../utils' export interface AuthorizedRoutesResponse { - URIs: string[] + paths: string[] } 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({ - URIs: ['/AppStream', '/SASjsApi/stp/execute'] + paths: ['/AppStream', '/SASjsApi/stp/execute'] }) @Get('/authorizedRoutes') public authorizedRoutes(): AuthorizedRoutesResponse { const response = { - URIs: getAuthorizedRoutes() + paths: getAuthorizedRoutes() } return response } diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index a73f16b..9f77cf2 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -19,12 +19,16 @@ import Group from '../model/Group' import { UserResponse } from './user' import { GroupDetailsResponse } from './group' +export enum PermissionType { + route = 'Route' +} + export enum PrincipalType { user = 'user', group = 'group' } -export enum PermissionSetting { +export enum PermissionSettingForRoute { grant = 'Grant', deny = 'Deny' } @@ -34,12 +38,17 @@ interface RegisterPermissionPayload { * Name of affected resource * @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 * @example "Grant" */ - setting: PermissionSetting + setting: PermissionSettingForRoute /** * Indicates the type of principal * @example "user" @@ -57,12 +66,13 @@ interface UpdatePermissionPayload { * The indication of whether (and to what extent) access is provided * @example "Grant" */ - setting: PermissionSetting + setting: PermissionSettingForRoute } export interface PermissionDetailsResponse { permissionId: number - uri: string + path: string + type: string setting: string user?: UserResponse group?: GroupDetailsResponse @@ -73,13 +83,17 @@ export interface PermissionDetailsResponse { @Tags('Permission') export class PermissionController { /** - * @summary Get a list of user's permissions, if user is admin all permissions are returned. + * 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([ { permissionId: 123, - uri: '/SASjsApi/code/execute', + path: '/SASjsApi/code/execute', + type: 'Route', setting: 'Grant', user: { id: 1, @@ -90,7 +104,8 @@ export class PermissionController { }, { permissionId: 124, - uri: '/SASjsApi/code/execute', + path: '/SASjsApi/code/execute', + type: 'Route', setting: 'Grant', group: { groupId: 1, @@ -114,7 +129,8 @@ export class PermissionController { */ @Example({ permissionId: 123, - uri: '/SASjsApi/code/execute', + path: '/SASjsApi/code/execute', + type: 'Route', setting: 'Grant', user: { id: 1, @@ -137,7 +153,8 @@ export class PermissionController { */ @Example({ permissionId: 123, - uri: '/SASjsApi/code/execute', + path: '/SASjsApi/code/execute', + type: 'Route', setting: 'Grant', user: { id: 1, @@ -193,13 +210,15 @@ const getAllPermissions = async ( } const createPermission = async ({ - uri, + path, + type, setting, principalType, principalId }: RegisterPermissionPayload): Promise => { const permission = new Permission({ - uri, + path, + type, setting }) @@ -224,7 +243,8 @@ const createPermission = async ({ } const alreadyExists = await Permission.findOne({ - uri, + path, + type, user: userInDB._id }) @@ -232,7 +252,8 @@ const createPermission = async ({ throw { code: 409, 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 @@ -255,14 +276,16 @@ const createPermission = async ({ } const alreadyExists = await Permission.findOne({ - uri, + path, + type, group: groupInDB._id }) if (alreadyExists) throw { code: 409, 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 @@ -292,7 +315,8 @@ const createPermission = async ({ return { permissionId: savedPermission.permissionId, - uri: savedPermission.uri, + path: savedPermission.path, + type: savedPermission.type, setting: savedPermission.setting, user, group @@ -313,7 +337,8 @@ const updatePermission = async ( .select({ _id: 0, permissionId: 1, - uri: 1, + path: 1, + type: 1, setting: 1 }) .populate({ path: 'user', select: 'id username displayName isAdmin -_id' }) diff --git a/api/src/middlewares/authorize.ts b/api/src/middlewares/authorize.ts index 20c85fa..3901b3e 100644 --- a/api/src/middlewares/authorize.ts +++ b/api/src/middlewares/authorize.ts @@ -1,8 +1,11 @@ import { RequestHandler } from 'express' import User from '../model/User' import Permission from '../model/Permission' -import { PermissionSetting } from '../controllers/permission' -import { getUri } from '../utils' +import { + PermissionSettingForRoute, + PermissionType +} from '../controllers/permission' +import { getPath } from '../utils' export const authorize: RequestHandler = async (req, res, next) => { const { user } = req @@ -17,20 +20,29 @@ export const authorize: RequestHandler = async (req, res, next) => { const dbUser = await User.findOne({ id: user.userId }) if (!dbUser) return res.sendStatus(401) - const uri = getUri(req) + const path = getPath(req) // 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.setting === PermissionSetting.grant) return next() + if (permission.setting === PermissionSettingForRoute.grant) return next() else return res.sendStatus(401) } // find permission w.r.t user's groups for (const group of dbUser.groups) { - const groupPermission = await Permission.findOne({ uri, group }) - if (groupPermission?.setting === PermissionSetting.grant) return next() + const groupPermission = await Permission.findOne({ + path, + type: PermissionType.route, + group + }) + if (groupPermission?.setting === PermissionSettingForRoute.grant) + return next() } return res.sendStatus(401) } diff --git a/api/src/model/Permission.ts b/api/src/model/Permission.ts index bb8302d..d83ca68 100644 --- a/api/src/model/Permission.ts +++ b/api/src/model/Permission.ts @@ -8,7 +8,8 @@ interface GetPermissionBy { } interface IPermissionDocument extends Document { - uri: string + path: string + type: string setting: string permissionId: number user: Schema.Types.ObjectId @@ -22,7 +23,11 @@ interface IPermissionModel extends Model { } const permissionSchema = new Schema({ - uri: { + path: { + type: String, + required: true + }, + type: { type: String, required: true }, @@ -44,7 +49,8 @@ permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise< .select({ _id: 0, permissionId: 1, - uri: 1, + path: 1, + type: 1, setting: 1 }) .populate({ path: 'user', select: 'id username displayName isAdmin -_id' }) diff --git a/api/src/routes/api/spec/drive.spec.ts b/api/src/routes/api/spec/drive.spec.ts index dfc5f8b..c4c28ce 100644 --- a/api/src/routes/api/spec/drive.spec.ts +++ b/api/src/routes/api/spec/drive.spec.ts @@ -32,7 +32,8 @@ import appPromise from '../../../app' import { UserController, PermissionController, - PermissionSetting, + PermissionType, + PermissionSettingForRoute, PrincipalType } from '../../../controllers/' import { getTreeExample } from '../../../controllers/internal' @@ -48,6 +49,12 @@ const user = { isActive: true } +const permission = { + type: PermissionType.route, + principalType: PrincipalType.user, + setting: PermissionSettingForRoute.grant +} + describe('drive', () => { let app: Express let con: Mongoose @@ -66,34 +73,29 @@ describe('drive', () => { const dbUser = await controller.createUser(user) accessToken = await generateAndSaveToken(dbUser.id) await permissionController.createPermission({ - uri: '/SASjsApi/drive/deploy', - principalType: PrincipalType.user, - principalId: dbUser.id, - setting: PermissionSetting.grant + ...permission, + path: '/SASjsApi/drive/deploy', + principalId: dbUser.id }) await permissionController.createPermission({ - uri: '/SASjsApi/drive/deploy/upload', - principalType: PrincipalType.user, - principalId: dbUser.id, - setting: PermissionSetting.grant + ...permission, + path: '/SASjsApi/drive/deploy/upload', + principalId: dbUser.id }) await permissionController.createPermission({ - uri: '/SASjsApi/drive/file', - principalType: PrincipalType.user, - principalId: dbUser.id, - setting: PermissionSetting.grant + ...permission, + path: '/SASjsApi/drive/file', + principalId: dbUser.id }) await permissionController.createPermission({ - uri: '/SASjsApi/drive/folder', - principalType: PrincipalType.user, - principalId: dbUser.id, - setting: PermissionSetting.grant + ...permission, + path: '/SASjsApi/drive/folder', + principalId: dbUser.id }) await permissionController.createPermission({ - uri: '/SASjsApi/drive/rename', - principalType: PrincipalType.user, - principalId: dbUser.id, - setting: PermissionSetting.grant + ...permission, + path: '/SASjsApi/drive/rename', + principalId: dbUser.id }) }) diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index 4393020..1d21ebe 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -9,7 +9,8 @@ import { GroupController, PermissionController, PrincipalType, - PermissionSetting + PermissionType, + PermissionSettingForRoute } from '../../../controllers/' import { UserDetailsResponse, @@ -55,10 +56,10 @@ const user = { } const permission = { - uri: '/SASjsApi/code/execute', - setting: PermissionSetting.grant, - principalType: PrincipalType.user, - principalId: 123 + path: '/SASjsApi/code/execute', + type: PermissionType.route, + setting: PermissionSettingForRoute.grant, + principalType: PrincipalType.user } const group = { @@ -106,7 +107,8 @@ describe('permission', () => { .expect(200) 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.user).toBeTruthy() }) @@ -125,7 +127,8 @@ describe('permission', () => { .expect(200) 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.group).toBeTruthy() }) @@ -140,53 +143,74 @@ describe('permission', () => { 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) - await permissionController.createPermission({ - uri: '/SASjsApi/permission', - principalType: PrincipalType.user, - principalId: dbUser.id, - setting: PermissionSetting.grant - }) - const res = await request(app) .post('/SASjsApi/permission') .auth(accessToken, { type: 'bearer' }) - .send() + .send(permission) .expect(401) expect(res.text).toEqual('Admin account required') 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) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) .send({ ...permission, - uri: undefined + path: undefined }) .expect(400) - expect(res.text).toEqual(`"uri" is required`) + expect(res.text).toEqual(`"path" is required`) 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) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) .send({ ...permission, - uri: '/some/random/api/endpoint' + path: '/some/random/api/endpoint' }) .expect(400) 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 () => { const res = await request(app) .post('/SASjsApi/permission') @@ -201,6 +225,20 @@ describe('permission', () => { 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 () => { const res = await request(app) .post('/SASjsApi/permission') @@ -215,20 +253,6 @@ describe('permission', () => { expect(res.body).toEqual({}) }) - it('should respond with Bad Request if principalId is missing', async () => { - const res = await request(app) - .post('/SASjsApi/permission') - .auth(adminAccessToken, { type: 'bearer' }) - .send({ - ...permission, - principalId: undefined - }) - .expect(400) - - expect(res.text).toEqual(`"principalId" is required`) - expect(res.body).toEqual({}) - }) - it('should respond with Bad Request if principal type is not valid', async () => { const res = await request(app) .post('/SASjsApi/permission') @@ -243,17 +267,17 @@ describe('permission', () => { 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) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) .send({ ...permission, - setting: 'invalid' + principalId: undefined }) .expect(400) - expect(res.text).toEqual('"setting" must be one of [Grant, Deny]') + expect(res.text).toEqual(`"principalId" is required`) expect(res.body).toEqual({}) }) @@ -311,7 +335,8 @@ describe('permission', () => { .auth(adminAccessToken, { type: 'bearer' }) .send({ ...permission, - principalType: 'group' + principalType: 'group', + principalId: 123 }) .expect(404) @@ -332,7 +357,7 @@ describe('permission', () => { .expect(409) 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({}) }) @@ -355,7 +380,7 @@ describe('permission', () => { const res = await request(app) .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) .auth(adminAccessToken, { type: 'bearer' }) - .send({ setting: 'Deny' }) + .send({ setting: PermissionSettingForRoute.deny }) .expect(200) expect(res.body.setting).toEqual('Deny') @@ -364,7 +389,7 @@ describe('permission', () => { it('should respond with Unauthorized if access token is not present', async () => { const res = await request(app) .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) - .send(permission) + .send() .expect(401) expect(res.text).toEqual('Unauthorized') @@ -398,12 +423,11 @@ describe('permission', () => { 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) - .post('/SASjsApi/permission') + .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) .auth(adminAccessToken, { type: 'bearer' }) .send({ - ...permission, setting: 'invalid' }) .expect(400) @@ -412,12 +436,12 @@ describe('permission', () => { 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) .patch('/SASjsApi/permission/123') .auth(adminAccessToken, { type: 'bearer' }) .send({ - setting: PermissionSetting.deny + setting: PermissionSettingForRoute.deny }) .expect(404) @@ -456,12 +480,12 @@ describe('permission', () => { beforeAll(async () => { await permissionController.createPermission({ ...permission, - uri: '/test-1', + path: '/test-1', principalId: dbUser.id }) await permissionController.createPermission({ ...permission, - uri: '/test-2', + path: '/test-2', principalId: dbUser.id }) }) @@ -483,10 +507,11 @@ describe('permission', () => { }) const accessToken = await generateAndSaveToken(nonAdminUser.id) await permissionController.createPermission({ - uri: '/test-1', + path: '/test-1', + type: PermissionType.route, principalType: PrincipalType.user, principalId: nonAdminUser.id, - setting: PermissionSetting.grant + setting: PermissionSettingForRoute.grant }) const res = await request(app) @@ -503,7 +528,7 @@ describe('permission', () => { beforeAll(async () => { await permissionController.createPermission({ ...permission, - uri: '/SASjsApi/drive/deploy', + path: '/SASjsApi/drive/deploy', principalId: dbUser.id }) }) diff --git a/api/src/routes/api/spec/stp.spec.ts b/api/src/routes/api/spec/stp.spec.ts index eb34570..10525c5 100644 --- a/api/src/routes/api/spec/stp.spec.ts +++ b/api/src/routes/api/spec/stp.spec.ts @@ -7,7 +7,8 @@ import appPromise from '../../../app' import { UserController, PermissionController, - PermissionSetting, + PermissionType, + PermissionSettingForRoute, PrincipalType } from '../../../controllers/' import { @@ -56,10 +57,11 @@ describe('stp', () => { const dbUser = await userController.createUser(user) accessToken = await generateAndSaveToken(dbUser.id) await permissionController.createPermission({ - uri: '/SASjsApi/stp/execute', + path: '/SASjsApi/stp/execute', + type: PermissionType.route, principalType: PrincipalType.user, principalId: dbUser.id, - setting: PermissionSetting.grant + setting: PermissionSettingForRoute.grant }) }) diff --git a/api/src/utils/getAuthorizedRoutes.ts b/api/src/utils/getAuthorizedRoutes.ts index a9ab123..e9dac26 100644 --- a/api/src/utils/getAuthorizedRoutes.ts +++ b/api/src/utils/getAuthorizedRoutes.ts @@ -18,7 +18,7 @@ export const getAuthorizedRoutes = () => { return [...StaticAuthorizedRoutes, ...streamingAppsRoutes] } -export const getUri = (req: Request) => { +export const getPath = (req: Request) => { const { baseUrl, path: reqPath } = req if (baseUrl === '/AppStream') { @@ -32,4 +32,4 @@ export const getUri = (req: Request) => { } export const isAuthorizingRoute = (req: Request): boolean => - getAuthorizedRoutes().includes(getUri(req)) + getAuthorizedRoutes().includes(getPath(req)) diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 09868c7..e3a3d3d 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -1,5 +1,9 @@ import Joi from 'joi' -import { PermissionSetting, PrincipalType } from '../controllers/permission' +import { + PermissionType, + PermissionSettingForRoute, + PrincipalType +} from '../controllers/permission' import { getAuthorizedRoutes } from './getAuthorizedRoutes' 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 => Joi.object({ - uri: Joi.string() + path: Joi.string() .required() .valid(...getAuthorizedRoutes()), + type: Joi.string() + .required() + .valid(...Object.values(PermissionType)), setting: Joi.string() .required() - .valid(...Object.values(PermissionSetting)), + .valid(...Object.values(PermissionSettingForRoute)), principalType: Joi.string() .required() .valid(...Object.values(PrincipalType)), @@ -105,7 +112,7 @@ export const updatePermissionValidation = (data: any): Joi.ValidationResult => Joi.object({ setting: Joi.string() .required() - .valid(...Object.values(PermissionSetting)) + .valid(...Object.values(PermissionSettingForRoute)) }).validate(data) export const deployValidation = (data: any): Joi.ValidationResult => diff --git a/web/src/containers/Settings/addPermissionModal.tsx b/web/src/containers/Settings/addPermissionModal.tsx index 2a147dc..9af5c3f 100644 --- a/web/src/containers/Settings/addPermissionModal.tsx +++ b/web/src/containers/Settings/addPermissionModal.tsx @@ -40,10 +40,11 @@ const AddPermissionModal = ({ handleOpen, addPermission }: AddPermissionModalProps) => { - const [URIs, setURIs] = useState([]) - const [loadingURIs, setLoadingURIs] = useState(false) - const [uri, setUri] = useState() - const [principalType, setPrincipalType] = useState('user') + const [paths, setPaths] = useState([]) + const [loadingPaths, setLoadingPaths] = useState(false) + const [path, setPath] = useState() + const [permissionType, setPermissionType] = useState('Route') + const [principalType, setPrincipalType] = useState('group') const [userPrincipal, setUserPrincipal] = useState() const [groupPrincipal, setGroupPrincipal] = useState() const [permissionSetting, setPermissionSetting] = useState('Grant') @@ -52,19 +53,19 @@ const AddPermissionModal = ({ const [groupPrincipals, setGroupPrincipals] = useState([]) useEffect(() => { - setLoadingURIs(true) + setLoadingPaths(true) axios .get('/SASjsApi/info/authorizedRoutes') .then((res: any) => { if (res.data) { - setURIs(res.data.URIs) + setPaths(res.data.paths) } }) .catch((err) => { console.log(err) }) .finally(() => { - setLoadingURIs(false) + setLoadingPaths(false) }) }, []) @@ -93,7 +94,8 @@ const AddPermissionModal = ({ const handleAddPermission = () => { const addPermissionPayload: any = { - uri, + path, + type: permissionType, setting: permissionSetting, principalType } @@ -106,7 +108,7 @@ const AddPermissionModal = ({ } const addButtonDisabled = - !uri || (principalType === 'user' ? !userPrincipal : !groupPrincipal) + !path || (principalType === 'user' ? !userPrincipal : !groupPrincipal) return ( handleOpen(false)} open={open}> @@ -120,22 +122,40 @@ const AddPermissionModal = ({ setUri(newValue)} + value={path} + onChange={(event: any, newValue: string) => setPath(newValue)} renderInput={(params) => - loadingURIs ? ( + loadingPaths ? ( ) : ( - + ) } /> + setPermissionType(newValue) + } + renderInput={(params) => + loadingPaths ? ( + + ) : ( + + ) + } + /> + + + option.toUpperCase()} disableClearable value={principalType} onChange={(event: any, newValue: string) => diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index 772d592..3238d69 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -68,7 +68,7 @@ const Permission = () => { const [selectedPermission, setSelectedPermission] = useState() const [filterModalOpen, setFilterModalOpen] = useState(false) - const [uriFilter, setUriFilter] = useState([]) + const [pathFilter, setPathFilter] = useState([]) const [principalFilter, setPrincipalFilter] = useState([]) const [principalTypeFilter, setPrincipalTypeFilter] = useState< PrincipalType[] @@ -111,8 +111,10 @@ const Permission = () => { setFilterModalOpen(false) const uriFilteredPermissions = - uriFilter.length > 0 - ? permissions.filter((permission) => uriFilter.includes(permission.uri)) + pathFilter.length > 0 + ? permissions.filter((permission) => + pathFilter.includes(permission.path) + ) : permissions const principalFilteredPermissions = @@ -172,7 +174,7 @@ const Permission = () => { const resetFilter = () => { setFilterModalOpen(false) - setUriFilter([]) + setPathFilter([]) setPrincipalFilter([]) setSettingFilter([]) setFilteredPermissions([]) @@ -322,8 +324,8 @@ const Permission = () => { open={filterModalOpen} handleOpen={setFilterModalOpen} permissions={permissions} - uriFilter={uriFilter} - setUriFilter={setUriFilter} + pathFilter={pathFilter} + setPathFilter={setPathFilter} principalFilter={principalFilter} setPrincipalFilter={setPrincipalFilter} principalTypeFilter={principalTypeFilter} @@ -374,9 +376,10 @@ const PermissionTable = ({ - Uri + Path + Permission Type Principal - Type + Principal Type Setting {appContext.isAdmin && ( Action @@ -386,7 +389,8 @@ const PermissionTable = ({ {permissions.map((permission) => ( - {permission.uri} + {permission.path} + {permission.type} {displayPrincipal(permission)} diff --git a/web/src/containers/Settings/permissionFilterModal.tsx b/web/src/containers/Settings/permissionFilterModal.tsx index bbe9177..44e104e 100644 --- a/web/src/containers/Settings/permissionFilterModal.tsx +++ b/web/src/containers/Settings/permissionFilterModal.tsx @@ -27,8 +27,8 @@ type FilterModalProps = { open: boolean handleOpen: Dispatch> permissions: PermissionResponse[] - uriFilter: string[] - setUriFilter: Dispatch> + pathFilter: string[] + setPathFilter: Dispatch> principalFilter: string[] setPrincipalFilter: Dispatch> principalTypeFilter: PrincipalType[] @@ -43,8 +43,8 @@ const PermissionFilterModal = ({ open, handleOpen, permissions, - uriFilter, - setUriFilter, + pathFilter, + setPathFilter, principalFilter, setPrincipalFilter, principalTypeFilter, @@ -54,8 +54,8 @@ const PermissionFilterModal = ({ applyFilter, resetFilter }: FilterModalProps) => { - const URIs = permissions - .map((permission) => permission.uri) + const paths = permissions + .map((permission) => permission.path) .filter((uri, index, array) => array.indexOf(uri) === index) // fetch all the principals from permissions array @@ -86,11 +86,11 @@ const PermissionFilterModal = ({ { - setUriFilter(newValue) + setPathFilter(newValue) }} renderInput={(params) => } /> diff --git a/web/src/utils/types.ts b/web/src/utils/types.ts index 99c6fc4..c733e23 100644 --- a/web/src/utils/types.ts +++ b/web/src/utils/types.ts @@ -18,14 +18,16 @@ export interface GroupDetailsResponse extends GroupResponse { export interface PermissionResponse { permissionId: number - uri: string + path: string + type: string setting: string user?: UserResponse group?: GroupDetailsResponse } export interface RegisterPermissionPayload { - uri: string + path: string + type: string setting: string principalType: string principalId: number