From 6bea1f76668ddb070ad95b3e02c31238af67c346 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 28 Apr 2022 21:18:23 +0500 Subject: [PATCH 01/54] feat: add permission model --- api/src/model/Permission.ts | 170 ++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 api/src/model/Permission.ts diff --git a/api/src/model/Permission.ts b/api/src/model/Permission.ts new file mode 100644 index 0000000..cd21ef1 --- /dev/null +++ b/api/src/model/Permission.ts @@ -0,0 +1,170 @@ +import { string } from 'joi' +import mongoose, { Schema, model, Document, Model } from 'mongoose' +const AutoIncrement = require('mongoose-sequence')(mongoose) + +export interface PermissionPayload { + /** + * Name of affected resource + * @example "/SASjsApi/code/execute" + */ + uri: string +} + +interface IPermissionDocument extends PermissionPayload, Document { + permissionId: number + users: [{ id: Schema.Types.ObjectId; setting: string }] + groups: [{ id: Schema.Types.ObjectId; setting: string }] + clients: [{ id: Schema.Types.ObjectId; setting: string }] +} + +interface IPermission extends IPermissionDocument { + addPermission( + objectId: Schema.Types.ObjectId, + objectType: string, + setting: string + ): Promise + updatePermission( + objectId: Schema.Types.ObjectId, + objectType: string, + setting: string + ): Promise + removePermission( + objectId: Schema.Types.ObjectId, + objectType: string + ): Promise +} + +interface IPermissionModel extends Model {} + +const permissionSchema = new Schema({ + uri: { + type: String, + required: true + }, + users: [ + { + id: { type: Schema.Types.ObjectId, ref: 'User' }, + setting: { type: String } + } + ], + groups: [ + { + id: { type: Schema.Types.ObjectId, ref: 'Group' }, + setting: { type: String } + } + ], + clients: [ + { + id: { type: Schema.Types.ObjectId, ref: 'Client' }, + setting: { type: String } + } + ] +}) + +permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' }) + +// Instance Methods +permissionSchema.method( + 'addPermission', + async function ( + objectId: Schema.Types.ObjectId, + objectType: string, + setting: string + ) { + switch (objectType) { + case 'User': + const user = this.users.find((obj) => obj.id === objectId) + if (!user) { + this.users.push({ id: objectId, setting }) + } + this.markModified('users') + break + case 'Group': + const group = this.groups.find((obj) => obj.id === objectId) + if (!group) { + this.groups.push({ id: objectId, setting }) + } + this.markModified('groups') + break + case 'Client': + const client = this.clients.find((obj) => obj.id === objectId) + if (!client) { + this.clients.push({ id: objectId, setting }) + } + this.markModified('clients') + break + } + return this.save() + } +) + +permissionSchema.method( + 'updatePermission', + async function ( + objectId: Schema.Types.ObjectId, + objectType: string, + setting: string + ) { + switch (objectType) { + case 'User': + const user = this.users.find( + (obj) => obj.id === objectId && obj.setting !== setting + ) + if (user) user.setting = setting + this.markModified('users') + break + case 'Group': + const group = this.groups.find( + (obj) => obj.id === objectId && obj.setting !== setting + ) + if (group) group.setting = setting + this.markModified('groups') + break + case 'Client': + const client = this.clients.find( + (obj) => obj.id === objectId && obj.setting !== setting + ) + if (client) client.setting = setting + this.markModified('clients') + break + } + return this.save() + } +) + +permissionSchema.method( + 'removePermission', + async function (objectId: Schema.Types.ObjectId, objectType: string) { + switch (objectType) { + case 'User': + const userIndex = this.users.findIndex((obj) => obj.id === objectId) + if (userIndex !== -1) { + this.users.splice(userIndex, 1) + } + this.markModified('users') + break + case 'Group': + const groupIndex = this.groups.findIndex((obj) => obj.id === objectId) + if (groupIndex !== -1) { + this.groups.splice(groupIndex, 1) + } + this.markModified('groups') + break + case 'Client': + const clientIndex = this.clients.findIndex((obj) => obj.id === objectId) + if (clientIndex !== -1) { + this.clients.splice(clientIndex, 1) + } + this.markModified('clients') + break + } + return this.save() + } +) + +export const Permission: IPermissionModel = model< + IPermission, + IPermissionModel +>('Permission', permissionSchema) + +export default Permission From 39fc908de1945f2aaea18d14e6bce703f6bf0c06 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 29 Apr 2022 15:26:26 +0500 Subject: [PATCH 02/54] fix: update permission model --- api/src/model/Permission.ts | 158 +++--------------------------------- 1 file changed, 13 insertions(+), 145 deletions(-) diff --git a/api/src/model/Permission.ts b/api/src/model/Permission.ts index cd21ef1..6343e4f 100644 --- a/api/src/model/Permission.ts +++ b/api/src/model/Permission.ts @@ -1,38 +1,16 @@ -import { string } from 'joi' import mongoose, { Schema, model, Document, Model } from 'mongoose' const AutoIncrement = require('mongoose-sequence')(mongoose) -export interface PermissionPayload { - /** - * Name of affected resource - * @example "/SASjsApi/code/execute" - */ +interface IPermissionDocument extends Document { uri: string -} - -interface IPermissionDocument extends PermissionPayload, Document { + setting: string permissionId: number - users: [{ id: Schema.Types.ObjectId; setting: string }] - groups: [{ id: Schema.Types.ObjectId; setting: string }] - clients: [{ id: Schema.Types.ObjectId; setting: string }] + user: Schema.Types.ObjectId + group: Schema.Types.ObjectId + client: Schema.Types.ObjectId } -interface IPermission extends IPermissionDocument { - addPermission( - objectId: Schema.Types.ObjectId, - objectType: string, - setting: string - ): Promise - updatePermission( - objectId: Schema.Types.ObjectId, - objectType: string, - setting: string - ): Promise - removePermission( - objectId: Schema.Types.ObjectId, - objectType: string - ): Promise -} +interface IPermission extends IPermissionDocument {} interface IPermissionModel extends Model {} @@ -41,127 +19,17 @@ const permissionSchema = new Schema({ type: String, required: true }, - users: [ - { - id: { type: Schema.Types.ObjectId, ref: 'User' }, - setting: { type: String } - } - ], - groups: [ - { - id: { type: Schema.Types.ObjectId, ref: 'Group' }, - setting: { type: String } - } - ], - clients: [ - { - id: { type: Schema.Types.ObjectId, ref: 'Client' }, - setting: { type: String } - } - ] + setting: { + type: String, + required: true + }, + user: { type: Schema.Types.ObjectId, ref: 'User' }, + group: { type: Schema.Types.ObjectId, ref: 'Group' }, + client: { type: Schema.Types.ObjectId, ref: 'Client' } }) permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' }) -// Instance Methods -permissionSchema.method( - 'addPermission', - async function ( - objectId: Schema.Types.ObjectId, - objectType: string, - setting: string - ) { - switch (objectType) { - case 'User': - const user = this.users.find((obj) => obj.id === objectId) - if (!user) { - this.users.push({ id: objectId, setting }) - } - this.markModified('users') - break - case 'Group': - const group = this.groups.find((obj) => obj.id === objectId) - if (!group) { - this.groups.push({ id: objectId, setting }) - } - this.markModified('groups') - break - case 'Client': - const client = this.clients.find((obj) => obj.id === objectId) - if (!client) { - this.clients.push({ id: objectId, setting }) - } - this.markModified('clients') - break - } - return this.save() - } -) - -permissionSchema.method( - 'updatePermission', - async function ( - objectId: Schema.Types.ObjectId, - objectType: string, - setting: string - ) { - switch (objectType) { - case 'User': - const user = this.users.find( - (obj) => obj.id === objectId && obj.setting !== setting - ) - if (user) user.setting = setting - this.markModified('users') - break - case 'Group': - const group = this.groups.find( - (obj) => obj.id === objectId && obj.setting !== setting - ) - if (group) group.setting = setting - this.markModified('groups') - break - case 'Client': - const client = this.clients.find( - (obj) => obj.id === objectId && obj.setting !== setting - ) - if (client) client.setting = setting - this.markModified('clients') - break - } - return this.save() - } -) - -permissionSchema.method( - 'removePermission', - async function (objectId: Schema.Types.ObjectId, objectType: string) { - switch (objectType) { - case 'User': - const userIndex = this.users.findIndex((obj) => obj.id === objectId) - if (userIndex !== -1) { - this.users.splice(userIndex, 1) - } - this.markModified('users') - break - case 'Group': - const groupIndex = this.groups.findIndex((obj) => obj.id === objectId) - if (groupIndex !== -1) { - this.groups.splice(groupIndex, 1) - } - this.markModified('groups') - break - case 'Client': - const clientIndex = this.clients.findIndex((obj) => obj.id === objectId) - if (clientIndex !== -1) { - this.clients.splice(clientIndex, 1) - } - this.markModified('clients') - break - } - return this.save() - } -) - export const Permission: IPermissionModel = model< IPermission, IPermissionModel From 38a7db8514de0acd94d74ba96bc1efb732add30c Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 29 Apr 2022 15:27:34 +0500 Subject: [PATCH 03/54] fix: export GroupResponse interface --- api/src/controllers/group.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/controllers/group.ts b/api/src/controllers/group.ts index 44adef2..033c877 100644 --- a/api/src/controllers/group.ts +++ b/api/src/controllers/group.ts @@ -14,7 +14,7 @@ import Group, { GroupPayload } from '../model/Group' import User from '../model/User' import { UserResponse } from './user' -interface GroupResponse { +export interface GroupResponse { groupId: number name: string description: string From e5200c1000903185dfad9ee49c99583e473c4388 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 29 Apr 2022 15:28:29 +0500 Subject: [PATCH 04/54] feat: add validation for registering permission --- api/src/utils/validation.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index a18cef9..cbdd23d 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -74,6 +74,14 @@ export const registerClientValidation = (data: any): Joi.ValidationResult => clientSecret: Joi.string().required() }).validate(data) +export const registerPermissionValidation = (data: any): Joi.ValidationResult => + Joi.object({ + uri: Joi.string().required(), + setting: Joi.string().required(), + principalType: Joi.string().required(), + principalId: Joi.any().required() + }).validate(data) + export const deployValidation = (data: any): Joi.ValidationResult => Joi.object({ appLoc: Joi.string().pattern(/^\//).required().min(2), From 1103ffe07b88496967cb03683b08f058ca3bbb9f Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 29 Apr 2022 15:30:41 +0500 Subject: [PATCH 05/54] feat: defined register permission and get all permissions api endpoints --- api/src/controllers/index.ts | 1 + api/src/controllers/permission.ts | 176 ++++++++++++++++++++++++++++++ api/src/routes/api/index.ts | 2 + api/src/routes/api/permission.ts | 35 ++++++ 4 files changed, 214 insertions(+) create mode 100644 api/src/controllers/permission.ts create mode 100644 api/src/routes/api/permission.ts diff --git a/api/src/controllers/index.ts b/api/src/controllers/index.ts index 05e8b84..9ff7768 100644 --- a/api/src/controllers/index.ts +++ b/api/src/controllers/index.ts @@ -7,3 +7,4 @@ export * from './session' export * from './stp' export * from './user' export * from './info' +export * from './permission' diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts new file mode 100644 index 0000000..8ea77d7 --- /dev/null +++ b/api/src/controllers/permission.ts @@ -0,0 +1,176 @@ +import { + Security, + Route, + Tags, + Path, + Example, + Get, + Post, + Delete, + Body +} from 'tsoa' + +import Permission from '../model/Permission' +import User from '../model/User' +import Group from '../model/Group' +import Client from '../model/Client' +import { UserResponse } from './user' +import { GroupResponse } from './group' + +interface PermissionPayload { + /** + * Name of affected resource + * @example "/SASjsApi/code/execute" + */ + uri: string + /** + * The indication of whether (and to what extent) access is provided + * @example "Grant" + */ + setting: string + /** + * Indicates the type of principal + * @example "user" + */ + principalType: string + /** + * The id of user(number), group(name), or client(clientId) to which a rule is assigned. + * @example 123 + */ + principalId: any +} + +interface PermissionDetailsResponse { + permissionId: number + uri: string + setting: string + user?: UserResponse + group?: GroupResponse + clientId?: string +} + +@Security('bearerAuth') +@Route('SASjsApi/permission') +@Tags('Permission') +export class PermissionController { + /** + * @summary Get list of all permissions (uri, setting and userDetail). + * + */ + @Example([ + { + permissionId: 123, + uri: '/SASjsApi/code/execute', + setting: 'Grant', + user: { id: 1, username: 'johnSnow01', displayName: 'John Snow' } + }, + { + permissionId: 124, + uri: '/SASjsApi/code/execute', + setting: 'Grant', + group: { + groupId: 1, + name: 'DCGroup', + description: 'This group represents Data Controller Users' + } + }, + { + permissionId: 125, + uri: '/SASjsApi/code/execute', + setting: 'Deny', + clientId: 'clientId1' + } + ]) + @Get('/') + public async getAllPermissions(): Promise { + return getAllPermissions() + } + + /** + * @summary Create a new permission. Admin only. + * + */ + @Example({ + permissionId: 123, + uri: '/SASjsApi/code/execute', + setting: 'Grant', + user: { id: 1, username: 'johnSnow01', displayName: 'John Snow' } + }) + @Post('/') + public async createPermission( + @Body() body: PermissionPayload + ): Promise { + return createPermission(body) + } +} + +const getAllPermissions = async (): Promise => + (await Permission.find({}) + .select({ + _id: 0, + permissionId: 1, + uri: 1, + setting: 1 + }) + .populate({ path: 'user', select: 'id username displayName -_id' }) + .populate({ + path: 'group', + select: 'groupId name description -_id' + }) + .populate({ + path: 'client', + select: 'clientId -_id' + })) as unknown as PermissionDetailsResponse[] + +const createPermission = async ({ + uri, + setting, + principalType, + principalId +}: PermissionPayload): Promise => { + const permission = new Permission({ + uri, + setting + }) + + let user, group, client + + switch (principalType) { + case 'user': + user = await User.findOne({ id: principalId }) + if (!user) throw new Error('User not found.') + permission.user = user._id + break + case 'group': + group = await Group.findOne({ groupId: principalId }) + if (!group) throw new Error('Group not found.') + permission.group = group._id + break + case 'client': + client = await Client.findOne({ clientId: principalId }) + if (!client) throw new Error('Client not found.') + permission.client = client._id + break + default: + throw new Error('Invalid principal type.') + } + + const savedPermission = await permission.save() + + return { + permissionId: savedPermission.permissionId, + uri: savedPermission.uri, + setting: savedPermission.setting, + user: !!user + ? { id: user.id, username: user.username, displayName: user.displayName } + : undefined, + group: !!group + ? { + groupId: group.groupId, + name: group.name, + description: group.description + } + : undefined, + clientId: !!client ? client.clientId : undefined + } +} diff --git a/api/src/routes/api/index.ts b/api/src/routes/api/index.ts index 7b249a9..9627d7e 100644 --- a/api/src/routes/api/index.ts +++ b/api/src/routes/api/index.ts @@ -18,6 +18,7 @@ import groupRouter from './group' import clientRouter from './client' import authRouter from './auth' import sessionRouter from './session' +import permissionRouter from './permission' const router = express.Router() @@ -36,6 +37,7 @@ router.use('/group', desktopRestrict, groupRouter) router.use('/stp', authenticateAccessToken, stpRouter) router.use('/code', authenticateAccessToken, codeRouter) router.use('/user', desktopRestrict, userRouter) +router.use('/permission', desktopRestrict, permissionRouter) router.use( '/', swaggerUi.serve, diff --git a/api/src/routes/api/permission.ts b/api/src/routes/api/permission.ts new file mode 100644 index 0000000..fc83462 --- /dev/null +++ b/api/src/routes/api/permission.ts @@ -0,0 +1,35 @@ +import express from 'express' +import { PermissionController } from '../../controllers/' +import { authenticateAccessToken, verifyAdmin } from '../../middlewares' +import { registerPermissionValidation } from '../../utils' + +const permissionRouter = express.Router() +const controller = new PermissionController() + +permissionRouter.get('/', authenticateAccessToken, async (req, res) => { + try { + const response = await controller.getAllPermissions() + res.send(response) + } catch (err: any) { + res.status(403).send(err.toString()) + } +}) + +permissionRouter.post( + '/', + authenticateAccessToken, + verifyAdmin, + async (req, res) => { + const { error, value: body } = registerPermissionValidation(req.body) + if (error) return res.status(400).send(error.details[0].message) + + try { + const response = await controller.createPermission(body) + res.send(response) + } catch (err: any) { + res.status(403).send(err.toString()) + } + } +) + +export default permissionRouter From 797c2bcc39005a05a995be15a150d584fecae259 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 29 Apr 2022 15:31:24 +0500 Subject: [PATCH 06/54] feat: update swagger docs --- api/public/swagger.yaml | 95 +++++++++++++++++++++++++++++++++++++++++ api/tsoa.json | 4 ++ 2 files changed, 99 insertions(+) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 0cee9b9..4fec97d 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -452,6 +452,51 @@ components: - protocol 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/GroupResponse' + clientId: + type: string + required: + - permissionId + - uri + - setting + type: object + additionalProperties: false + PermissionPayload: + properties: + uri: + type: string + description: 'Name of affected resource' + example: /SASjsApi/code/execute + setting: + type: string + description: 'The indication of whether (and to what extent) access is provided' + example: Grant + principalType: + type: string + description: 'Indicates the type of principal' + example: user + principalId: + description: 'The id of user(number), group(name), or client(clientId) to which a rule is assigned.' + example: 123 + required: + - uri + - setting + - principalType + - principalId + type: object + additionalProperties: false securitySchemes: bearerAuth: type: http @@ -1333,6 +1378,53 @@ paths: - 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, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow'}}, {permissionId: 124, uri: /SASjsApi/code/execute, setting: Grant, group: {groupId: 1, name: DCGroup, description: 'This group represents Data Controller Users'}}, {permissionId: 125, uri: /SASjsApi/code/execute, setting: Deny, clientId: clientId1}] + 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'}} + summary: 'Create a new permission. Admin only.' + tags: + - Permission + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionPayload' servers: - url: / @@ -1346,6 +1438,9 @@ tags: - name: User description: 'Operations about users' + - + name: Permission + description: 'Operations about permissions' - name: Client description: 'Operations about clients' diff --git a/api/tsoa.json b/api/tsoa.json index f353150..a2ceae0 100644 --- a/api/tsoa.json +++ b/api/tsoa.json @@ -23,6 +23,10 @@ "name": "User", "description": "Operations about users" }, + { + "name": "Permission", + "description": "Operations about permissions" + }, { "name": "Client", "description": "Operations about clients" From 540f54fb77b364822da7889dbe75c02242f48a59 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Sat, 30 Apr 2022 01:02:47 +0500 Subject: [PATCH 07/54] feat: add api endpoint for updating permission setting --- api/public/swagger.yaml | 48 ++++++++++++++++++++++- api/src/controllers/permission.ts | 65 +++++++++++++++++++++++++++++-- api/src/routes/api/permission.ts | 23 ++++++++++- api/src/utils/validation.ts | 5 +++ 4 files changed, 135 insertions(+), 6 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 4fec97d..fd9aa21 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -473,7 +473,7 @@ components: - setting type: object additionalProperties: false - PermissionPayload: + RegisterPermissionPayload: properties: uri: type: string @@ -497,6 +497,16 @@ components: - principalId type: object additionalProperties: false + UpdatePermissionPayload: + properties: + setting: + type: string + description: 'The indication of whether (and to what extent) access is provided' + example: Grant + required: + - setting + type: object + additionalProperties: false securitySchemes: bearerAuth: type: http @@ -1424,7 +1434,41 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PermissionPayload' + $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'}} + summary: 'Update permission setting.' + tags: + - Permission + security: + - + bearerAuth: [] + parameters: + - + description: 'The permission''s identifier' + in: path + name: permissionId + required: true + schema: + format: double + type: number + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdatePermissionPayload' servers: - url: / diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index 8ea77d7..f1e8bf3 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -6,6 +6,7 @@ import { Example, Get, Post, + Patch, Delete, Body } from 'tsoa' @@ -17,7 +18,7 @@ import Client from '../model/Client' import { UserResponse } from './user' import { GroupResponse } from './group' -interface PermissionPayload { +interface RegisterPermissionPayload { /** * Name of affected resource * @example "/SASjsApi/code/execute" @@ -40,6 +41,14 @@ interface PermissionPayload { principalId: any } +interface UpdatePermissionPayload { + /** + * The indication of whether (and to what extent) access is provided + * @example "Grant" + */ + setting: string +} + interface PermissionDetailsResponse { permissionId: number uri: string @@ -98,10 +107,29 @@ export class PermissionController { }) @Post('/') public async createPermission( - @Body() body: PermissionPayload + @Body() body: RegisterPermissionPayload ): Promise { return createPermission(body) } + + /** + * @summary Update permission setting. + * @param permissionId The permission's identifier + * @example userId "1234" + */ + @Example({ + permissionId: 123, + uri: '/SASjsApi/code/execute', + setting: 'Grant', + user: { id: 1, username: 'johnSnow01', displayName: 'John Snow' } + }) + @Patch('{permissionId}') + public async updatePermission( + @Path() permissionId: number, + @Body() body: UpdatePermissionPayload + ): Promise { + return updatePermission(permissionId, body) + } } const getAllPermissions = async (): Promise => @@ -127,7 +155,7 @@ const createPermission = async ({ setting, principalType, principalId -}: PermissionPayload): Promise => { +}: RegisterPermissionPayload): Promise => { const permission = new Permission({ uri, setting @@ -174,3 +202,34 @@ const createPermission = async ({ clientId: !!client ? client.clientId : undefined } } + +const updatePermission = async ( + id: number, + data: UpdatePermissionPayload +): Promise => { + const { setting } = data + + const updatedPermission = (await Permission.findOneAndUpdate( + { permissionId: id }, + { setting }, + { new: true } + ) + .select({ + _id: 0, + permissionId: 1, + uri: 1, + setting: 1 + }) + .populate({ path: 'user', select: 'id username displayName -_id' }) + .populate({ + path: 'group', + select: 'groupId name description -_id' + }) + .populate({ + path: 'client', + select: 'clientId -_id' + })) as unknown as PermissionDetailsResponse + if (!updatedPermission) throw new Error('Unable to update permission') + + return updatedPermission +} diff --git a/api/src/routes/api/permission.ts b/api/src/routes/api/permission.ts index fc83462..224c329 100644 --- a/api/src/routes/api/permission.ts +++ b/api/src/routes/api/permission.ts @@ -1,7 +1,10 @@ import express from 'express' import { PermissionController } from '../../controllers/' import { authenticateAccessToken, verifyAdmin } from '../../middlewares' -import { registerPermissionValidation } from '../../utils' +import { + registerPermissionValidation, + updatePermissionValidation +} from '../../utils' const permissionRouter = express.Router() const controller = new PermissionController() @@ -32,4 +35,22 @@ permissionRouter.post( } ) +permissionRouter.patch( + '/:permissionId', + authenticateAccessToken, + verifyAdmin, + async (req: any, res) => { + const { permissionId } = req.params + + const { error, value: body } = updatePermissionValidation(req.body) + if (error) return res.status(400).send(error.details[0].message) + + try { + const response = await controller.updatePermission(permissionId, body) + res.send(response) + } catch (err: any) { + res.status(403).send(err.toString()) + } + } +) export default permissionRouter diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index cbdd23d..a9d35c2 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -82,6 +82,11 @@ export const registerPermissionValidation = (data: any): Joi.ValidationResult => principalId: Joi.any().required() }).validate(data) +export const updatePermissionValidation = (data: any): Joi.ValidationResult => + Joi.object({ + setting: Joi.string().required() + }).validate(data) + export const deployValidation = (data: any): Joi.ValidationResult => Joi.object({ appLoc: Joi.string().pattern(/^\//).required().min(2), From 01713440a4fa661b76368785c0ca731f096ac70a Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Sat, 30 Apr 2022 01:16:52 +0500 Subject: [PATCH 08/54] feat: add api endpoint for deleting permission --- api/public/swagger.yaml | 24 +++++++++++++++++++++++- api/src/controllers/permission.ts | 20 ++++++++++++++++++-- api/src/routes/api/permission.ts | 16 ++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index fd9aa21..0ba5e59 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -1448,7 +1448,7 @@ paths: examples: 'Example 1': value: {permissionId: 123, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow'}} - summary: 'Update permission setting.' + summary: 'Update permission setting. Admin only' tags: - Permission security: @@ -1463,12 +1463,34 @@ paths: 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: / diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index f1e8bf3..d6f1f71 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -113,9 +113,9 @@ export class PermissionController { } /** - * @summary Update permission setting. + * @summary Update permission setting. Admin only * @param permissionId The permission's identifier - * @example userId "1234" + * @example permissionId 1234 */ @Example({ permissionId: 123, @@ -130,6 +130,16 @@ export class PermissionController { ): Promise { return updatePermission(permissionId, body) } + + /** + * @summary Delete a permission. Admin only. + * @param permissionId The user's identifier + * @example permissionId 1234 + */ + @Delete('{permissionId}') + public async deletePermission(@Path() permissionId: number) { + return deletePermission(permissionId) + } } const getAllPermissions = async (): Promise => @@ -233,3 +243,9 @@ const updatePermission = async ( return updatedPermission } + +const deletePermission = async (id: number) => { + const permission = await Permission.findOne({ id }) + if (!permission) throw new Error('Permission is not found.') + await Permission.deleteOne({ id }) +} diff --git a/api/src/routes/api/permission.ts b/api/src/routes/api/permission.ts index 224c329..8abc0a1 100644 --- a/api/src/routes/api/permission.ts +++ b/api/src/routes/api/permission.ts @@ -53,4 +53,20 @@ permissionRouter.patch( } } ) + +permissionRouter.delete( + '/:permissionId', + authenticateAccessToken, + verifyAdmin, + async (req: any, res) => { + const { permissionId } = req.params + + try { + await controller.deletePermission(permissionId) + res.status(200).send('Permission Deleted!') + } catch (err: any) { + res.status(403).send(err.toString()) + } + } +) export default permissionRouter From 89b32e70fff4af27c79e03613ce877fb32314fe3 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Sat, 30 Apr 2022 03:49:26 +0500 Subject: [PATCH 09/54] refactor: code in permission controller --- api/src/controllers/permission.ts | 53 +++++++++++++++++++------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index d6f1f71..2eb6e1d 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -171,23 +171,42 @@ const createPermission = async ({ setting }) - let user, group, client + let user: UserResponse | undefined + let group: GroupResponse | undefined + let clientId: string | undefined switch (principalType) { case 'user': - user = await User.findOne({ id: principalId }) - if (!user) throw new Error('User not found.') - permission.user = user._id + const userInDB = await User.findOne({ id: principalId }) + if (!userInDB) throw new Error('User not found.') + + permission.user = userInDB._id + + user = { + id: userInDB.id, + username: userInDB.username, + displayName: userInDB.displayName + } break case 'group': - group = await Group.findOne({ groupId: principalId }) - if (!group) throw new Error('Group not found.') - permission.group = group._id + const groupInDB = await Group.findOne({ groupId: principalId }) + if (!groupInDB) throw new Error('Group not found.') + + permission.group = groupInDB._id + + group = { + groupId: groupInDB.groupId, + name: groupInDB.name, + description: groupInDB.description + } break case 'client': - client = await Client.findOne({ clientId: principalId }) - if (!client) throw new Error('Client not found.') - permission.client = client._id + const clientInDB = await Client.findOne({ clientId: principalId }) + if (!clientInDB) throw new Error('Client not found.') + + permission.client = clientInDB._id + + clientId = clientInDB.clientId break default: throw new Error('Invalid principal type.') @@ -199,17 +218,9 @@ const createPermission = async ({ permissionId: savedPermission.permissionId, uri: savedPermission.uri, setting: savedPermission.setting, - user: !!user - ? { id: user.id, username: user.username, displayName: user.displayName } - : undefined, - group: !!group - ? { - groupId: group.groupId, - name: group.name, - description: group.description - } - : undefined, - clientId: !!client ? client.clientId : undefined + user, + group, + clientId } } From 9136c95013472271ce677345540bd2ca9e2d4d9e Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 9 May 2022 13:08:15 +0500 Subject: [PATCH 10/54] chore: write specs for create permission api endpoint --- api/src/routes/api/spec/permission.spec.ts | 274 +++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 api/src/routes/api/spec/permission.spec.ts diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts new file mode 100644 index 0000000..19c2f8c --- /dev/null +++ b/api/src/routes/api/spec/permission.spec.ts @@ -0,0 +1,274 @@ +import { Express } from 'express' +import mongoose, { Mongoose } from 'mongoose' +import { MongoMemoryServer } from 'mongodb-memory-server' +import request from 'supertest' +import appPromise from '../../../app' +import { + UserController, + GroupController, + ClientController +} from '../../../controllers/' +import { generateAccessToken, saveTokensInDB } from '../../../utils' + +const clientId = 'someclientID' +const adminUser = { + displayName: 'Test Admin', + username: 'testAdminUsername', + password: '12345678', + isAdmin: true, + isActive: true +} +const user = { + displayName: 'Test User', + username: 'testUsername', + password: '87654321', + isAdmin: false, + isActive: true +} + +const permission = { + uri: '/SASjsApi/code/execute', + setting: 'Grant', + principalType: 'user', + principalId: 123 +} + +const group = { + name: 'DCGroup1', + description: 'DC group for testing purposes.' +} + +const userController = new UserController() +const groupController = new GroupController() +const clientController = new ClientController() + +describe('permission', () => { + let app: Express + let con: Mongoose + let mongoServer: MongoMemoryServer + let adminAccessToken: string + + beforeAll(async () => { + app = await appPromise + + mongoServer = await MongoMemoryServer.create() + con = await mongoose.connect(mongoServer.getUri()) + + adminAccessToken = await generateSaveTokenAndCreateUser() + }) + + afterAll(async () => { + await con.connection.dropDatabase() + await con.connection.close() + await mongoServer.stop() + }) + + describe('create', () => { + afterEach(async () => { + await deleteAllPermissions() + }) + + it('should respond with new permission when principalType is user', async () => { + const dbUser = await userController.createUser(user) + permission.principalId = dbUser.id + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send(permission) + .expect(200) + + expect(res.body.permissionId).toBeTruthy() + expect(res.body.uri).toEqual(permission.uri) + expect(res.body.setting).toEqual(permission.setting) + expect(res.body.user).toBeTruthy() + }) + + it('should respond with new permission when principalType is group', async () => { + const dbGroup = await groupController.createGroup(group) + + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + principalType: 'group', + principalId: dbGroup.groupId + }) + .expect(200) + + expect(res.body.permissionId).toBeTruthy() + expect(res.body.uri).toEqual(permission.uri) + expect(res.body.setting).toEqual(permission.setting) + expect(res.body.group).toBeTruthy() + }) + + it('should respond with new permission when principalType is client', async () => { + const dbclient = await clientController.createClient({ + clientId: '123456789', + clientSecret: '123456789' + }) + + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + principalType: 'client', + principalId: dbclient.clientId + }) + .expect(200) + + expect(res.body.permissionId).toBeTruthy() + expect(res.body.uri).toEqual(permission.uri) + expect(res.body.setting).toEqual(permission.setting) + expect(res.body.clientId).toEqual(dbclient.clientId) + }) + + it('should respond with Unauthorized if access token is not present', async () => { + const res = await request(app) + .post('/SASjsApi/permission') + .send(permission) + .expect(401) + + expect(res.text).toEqual('Unauthorized') + expect(res.body).toEqual({}) + }) + + it('should respond with Unauthorized if access token is not of an admin account', async () => { + const accessToken = await generateSaveTokenAndCreateUser({ + ...user, + username: 'create' + user.username + }) + + const res = await request(app) + .post('/SASjsApi/permission') + .auth(accessToken, { type: 'bearer' }) + .send() + .expect(401) + + expect(res.text).toEqual('Admin account required') + expect(res.body).toEqual({}) + }) + + it('should respond with Bad Request if uri is missing', async () => { + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + uri: undefined + }) + .expect(400) + + expect(res.text).toEqual(`"uri" 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') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + setting: undefined + }) + .expect(400) + + expect(res.text).toEqual(`"setting" is required`) + expect(res.body).toEqual({}) + }) + + it('should respond with Bad Request if principalType is missing', async () => { + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + principalType: undefined + }) + .expect(400) + + expect(res.text).toEqual(`"principalType" is required`) + 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 forbidden Request (403) if user is not found', async () => { + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + principalId: 123 + }) + .expect(403) + + expect(res.text).toEqual('Error: User not found.') + expect(res.body).toEqual({}) + }) + + it('should respond with forbidden Request (403) if group is not found', async () => { + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + principalType: 'group' + }) + .expect(403) + + expect(res.text).toEqual('Error: Group not found.') + expect(res.body).toEqual({}) + }) + + it('should respond with forbidden Request (403) if client is not found', async () => { + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + principalType: 'client' + }) + .expect(403) + + expect(res.text).toEqual('Error: Client not found.') + expect(res.body).toEqual({}) + }) + }) +}) + +const generateSaveTokenAndCreateUser = async ( + someUser?: any +): Promise => { + const dbUser = await userController.createUser(someUser ?? adminUser) + + return generateAndSaveToken(dbUser.id) +} + +const generateAndSaveToken = async (userId: number) => { + const adminAccessToken = generateAccessToken({ + clientId, + userId + }) + await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken') + return adminAccessToken +} + +const deleteAllPermissions = async () => { + const { collections } = mongoose.connection + const collection = collections['permissions'] + await collection.deleteMany({}) +} From 1aec3abd2804a9585a551f129fe6f90f44922225 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 10 May 2022 06:11:24 +0500 Subject: [PATCH 11/54] chore: add specs for update permission api endpoint --- api/src/controllers/permission.ts | 2 +- api/src/controllers/user.ts | 2 +- api/src/routes/api/spec/permission.spec.ts | 77 +++++++++++++++++++++- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index 2eb6e1d..7431cd8 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -49,7 +49,7 @@ interface UpdatePermissionPayload { setting: string } -interface PermissionDetailsResponse { +export interface PermissionDetailsResponse { permissionId: number uri: string setting: string diff --git a/api/src/controllers/user.ts b/api/src/controllers/user.ts index 9aa66b9..1324128 100644 --- a/api/src/controllers/user.ts +++ b/api/src/controllers/user.ts @@ -21,7 +21,7 @@ export interface UserResponse { displayName: string } -interface UserDetailsResponse { +export interface UserDetailsResponse { id: number displayName: string username: string diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index 19c2f8c..03ece1f 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -6,8 +6,13 @@ import appPromise from '../../../app' import { UserController, GroupController, - ClientController + ClientController, + PermissionController } from '../../../controllers/' +import { + UserDetailsResponse, + PermissionDetailsResponse +} from '../../../controllers' import { generateAccessToken, saveTokensInDB } from '../../../utils' const clientId = 'someclientID' @@ -41,6 +46,7 @@ const group = { const userController = new UserController() const groupController = new GroupController() const clientController = new ClientController() +const permissionController = new PermissionController() describe('permission', () => { let app: Express @@ -70,11 +76,10 @@ describe('permission', () => { it('should respond with new permission when principalType is user', async () => { const dbUser = await userController.createUser(user) - permission.principalId = dbUser.id const res = await request(app) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) - .send(permission) + .send({ ...permission, principalId: dbUser.id }) .expect(200) expect(res.body.permissionId).toBeTruthy() @@ -248,6 +253,72 @@ describe('permission', () => { expect(res.body).toEqual({}) }) }) + + describe('update', () => { + let dbUser: UserDetailsResponse | undefined + let dbPermission: PermissionDetailsResponse | undefined + beforeAll(async () => { + dbUser = await userController.createUser({ + ...user, + username: 'updated username' + }) + dbPermission = await permissionController.createPermission({ + ...permission, + principalId: dbUser.id + }) + }) + + afterEach(async () => { + await deleteAllPermissions() + }) + + it('should respond with updated permission', async () => { + const res = await request(app) + .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) + .auth(adminAccessToken, { type: 'bearer' }) + .send({ setting: 'Deny' }) + .expect(200) + + expect(res.body.setting).toEqual('Deny') + }) + + it('should respond with Unauthorized if access token is not present', async () => { + const res = await request(app) + .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) + .send(permission) + .expect(401) + + expect(res.text).toEqual('Unauthorized') + expect(res.body).toEqual({}) + }) + + it('should respond with Unauthorized if access token is not of an admin account', async () => { + const accessToken = await generateSaveTokenAndCreateUser({ + ...user, + username: 'update' + user.username + }) + + const res = await request(app) + .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) + .auth(accessToken, { type: 'bearer' }) + .send() + .expect(401) + + expect(res.text).toEqual('Admin account required') + expect(res.body).toEqual({}) + }) + + it('should respond with Bad Request if setting is missing', async () => { + const res = await request(app) + .patch(`/SASjsApi/permission/${dbPermission?.permissionId}`) + .auth(adminAccessToken, { type: 'bearer' }) + .send() + .expect(400) + + expect(res.text).toEqual(`"setting" is required`) + expect(res.body).toEqual({}) + }) + }) }) const generateSaveTokenAndCreateUser = async ( From fce05d695984208e079b56145f5831b3fff09e76 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 10 May 2022 06:18:19 +0500 Subject: [PATCH 12/54] chore: add spec for invalid principal type --- api/src/controllers/permission.ts | 4 +++- api/src/routes/api/spec/permission.spec.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index 7431cd8..ce04f52 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -209,7 +209,9 @@ const createPermission = async ({ clientId = clientInDB.clientId break default: - throw new Error('Invalid principal type.') + throw new Error( + 'Invalid principal type. Valid types are user, group and client.' + ) } const savedPermission = await permission.save() diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index 03ece1f..77e4095 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -252,6 +252,22 @@ describe('permission', () => { expect(res.text).toEqual('Error: Client not found.') expect(res.body).toEqual({}) }) + + it('should respond with forbidden Request (403) if principal type is not valid', async () => { + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + principalType: 'invalid' + }) + .expect(403) + + expect(res.text).toEqual( + 'Error: Invalid principal type. Valid types are user, group and client.' + ) + expect(res.body).toEqual({}) + }) }) describe('update', () => { From 72a3197a06d79fa57d0991fe36fd5cbf4e23bf6e Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 10 May 2022 06:25:52 +0500 Subject: [PATCH 13/54] chore: add spec for update permission when permission with provided id not exists --- api/src/routes/api/spec/permission.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index 77e4095..6a90462 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -334,6 +334,19 @@ describe('permission', () => { expect(res.text).toEqual(`"setting" is required`) expect(res.body).toEqual({}) }) + + it('should respond with forbidden Request (403) if permission with provided id does not exists', async () => { + const res = await request(app) + .patch('/SASjsApi/permission/123') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + setting: 'deny' + }) + .expect(403) + + expect(res.text).toEqual('Error: Unable to update permission') + expect(res.body).toEqual({}) + }) }) }) From 98b8a751484feb8a18ce438126a4513e2598a033 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 10 May 2022 06:40:34 +0500 Subject: [PATCH 14/54] chore: add specs for delete permission api endpoint --- api/src/routes/api/spec/permission.spec.ts | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index 6a90462..fca6a81 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -348,6 +348,37 @@ describe('permission', () => { expect(res.body).toEqual({}) }) }) + + describe('delete', () => { + it('should delete permission', async () => { + const dbUser = await userController.createUser({ + ...user, + username: 'deleted username' + }) + + const dbPermission = await permissionController.createPermission({ + ...permission, + principalId: dbUser.id + }) + const res = await request(app) + .delete(`/SASjsApi/permission/${dbPermission?.permissionId}`) + .auth(adminAccessToken, { type: 'bearer' }) + .send() + .expect(200) + + expect(res.text).toEqual('Permission Deleted!') + }) + + it('should respond with forbidden Request (403) if permission with provided id does not exists', async () => { + const res = await request(app) + .delete('/SASjsApi/permission/123') + .auth(adminAccessToken, { type: 'bearer' }) + .send() + .expect(403) + + expect(res.text).toEqual('Error: Permission is not found.') + }) + }) }) const generateSaveTokenAndCreateUser = async ( From 7be77cc38aa632d4abf05386c21df0cbaa7988af Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 10 May 2022 07:05:59 +0500 Subject: [PATCH 15/54] chore: remvoe code redundancy and add specs for get permissions api endpoint --- api/src/routes/api/spec/permission.spec.ts | 58 ++++++++++++++++------ 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index fca6a81..df2c662 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -53,6 +53,7 @@ describe('permission', () => { let con: Mongoose let mongoServer: MongoMemoryServer let adminAccessToken: string + let dbUser: UserDetailsResponse | undefined beforeAll(async () => { app = await appPromise @@ -61,6 +62,7 @@ describe('permission', () => { con = await mongoose.connect(mongoServer.getUri()) adminAccessToken = await generateSaveTokenAndCreateUser() + dbUser = await userController.createUser(user) }) afterAll(async () => { @@ -75,11 +77,10 @@ describe('permission', () => { }) it('should respond with new permission when principalType is user', async () => { - const dbUser = await userController.createUser(user) const res = await request(app) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) - .send({ ...permission, principalId: dbUser.id }) + .send({ ...permission, principalId: dbUser?.id }) .expect(200) expect(res.body.permissionId).toBeTruthy() @@ -271,16 +272,11 @@ describe('permission', () => { }) describe('update', () => { - let dbUser: UserDetailsResponse | undefined let dbPermission: PermissionDetailsResponse | undefined beforeAll(async () => { - dbUser = await userController.createUser({ - ...user, - username: 'updated username' - }) dbPermission = await permissionController.createPermission({ ...permission, - principalId: dbUser.id + principalId: dbUser?.id }) }) @@ -351,14 +347,9 @@ describe('permission', () => { describe('delete', () => { it('should delete permission', async () => { - const dbUser = await userController.createUser({ - ...user, - username: 'deleted username' - }) - const dbPermission = await permissionController.createPermission({ ...permission, - principalId: dbUser.id + principalId: dbUser?.id }) const res = await request(app) .delete(`/SASjsApi/permission/${dbPermission?.permissionId}`) @@ -379,6 +370,45 @@ describe('permission', () => { expect(res.text).toEqual('Error: Permission is not found.') }) }) + + describe('get', () => { + beforeAll(async () => { + await permissionController.createPermission({ + ...permission, + uri: '/test-1', + principalId: dbUser?.id + }) + await permissionController.createPermission({ + ...permission, + uri: '/test-2', + principalId: dbUser?.id + }) + }) + + it('should give a list of all permissions when user is admin', async () => { + const res = await request(app) + .get('/SASjsApi/permission/') + .auth(adminAccessToken, { type: 'bearer' }) + .send() + .expect(200) + + expect(res.body).toHaveLength(2) + }) + + it('should give a list of all permissions when user is not admin', async () => { + const accessToken = await generateSaveTokenAndCreateUser({ + ...user, + username: 'get' + user.username + }) + const res = await request(app) + .get('/SASjsApi/permission/') + .auth(accessToken, { type: 'bearer' }) + .send() + .expect(200) + + expect(res.body).toHaveLength(2) + }) + }) }) const generateSaveTokenAndCreateUser = async ( From 0781ddd64e3b5e5ca39647bb4e4e1a9332a0f4f8 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 16 May 2022 19:56:56 +0500 Subject: [PATCH 16/54] fix: remove clientId from principal types --- api/public/swagger.yaml | 4 +-- api/src/controllers/permission.ts | 32 ++---------------- api/src/model/Permission.ts | 4 +-- api/src/routes/api/spec/permission.spec.ts | 38 +--------------------- 4 files changed, 5 insertions(+), 73 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 0ba5e59..8449e3d 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -465,8 +465,6 @@ components: $ref: '#/components/schemas/UserResponse' group: $ref: '#/components/schemas/GroupResponse' - clientId: - type: string required: - permissionId - uri @@ -1402,7 +1400,7 @@ paths: type: array examples: 'Example 1': - value: [{permissionId: 123, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow'}}, {permissionId: 124, uri: /SASjsApi/code/execute, setting: Grant, group: {groupId: 1, name: DCGroup, description: 'This group represents Data Controller Users'}}, {permissionId: 125, uri: /SASjsApi/code/execute, setting: Deny, clientId: clientId1}] + value: [{permissionId: 123, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow'}}, {permissionId: 124, uri: /SASjsApi/code/execute, setting: Grant, group: {groupId: 1, name: DCGroup, description: 'This group represents Data Controller Users'}}] summary: 'Get list of all permissions (uri, setting and userDetail).' tags: - Permission diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index ce04f52..d968692 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -14,7 +14,6 @@ import { import Permission from '../model/Permission' import User from '../model/User' import Group from '../model/Group' -import Client from '../model/Client' import { UserResponse } from './user' import { GroupResponse } from './group' @@ -55,7 +54,6 @@ export interface PermissionDetailsResponse { setting: string user?: UserResponse group?: GroupResponse - clientId?: string } @Security('bearerAuth') @@ -82,12 +80,6 @@ export class PermissionController { name: 'DCGroup', description: 'This group represents Data Controller Users' } - }, - { - permissionId: 125, - uri: '/SASjsApi/code/execute', - setting: 'Deny', - clientId: 'clientId1' } ]) @Get('/') @@ -154,10 +146,6 @@ const getAllPermissions = async (): Promise => .populate({ path: 'group', select: 'groupId name description -_id' - }) - .populate({ - path: 'client', - select: 'clientId -_id' })) as unknown as PermissionDetailsResponse[] const createPermission = async ({ @@ -173,7 +161,6 @@ const createPermission = async ({ let user: UserResponse | undefined let group: GroupResponse | undefined - let clientId: string | undefined switch (principalType) { case 'user': @@ -200,18 +187,8 @@ const createPermission = async ({ description: groupInDB.description } break - case 'client': - const clientInDB = await Client.findOne({ clientId: principalId }) - if (!clientInDB) throw new Error('Client not found.') - - permission.client = clientInDB._id - - clientId = clientInDB.clientId - break default: - throw new Error( - 'Invalid principal type. Valid types are user, group and client.' - ) + throw new Error('Invalid principal type. Valid types are user or group.') } const savedPermission = await permission.save() @@ -221,8 +198,7 @@ const createPermission = async ({ uri: savedPermission.uri, setting: savedPermission.setting, user, - group, - clientId + group } } @@ -247,10 +223,6 @@ const updatePermission = async ( .populate({ path: 'group', select: 'groupId name description -_id' - }) - .populate({ - path: 'client', - select: 'clientId -_id' })) as unknown as PermissionDetailsResponse if (!updatedPermission) throw new Error('Unable to update permission') diff --git a/api/src/model/Permission.ts b/api/src/model/Permission.ts index 6343e4f..8d9454e 100644 --- a/api/src/model/Permission.ts +++ b/api/src/model/Permission.ts @@ -7,7 +7,6 @@ interface IPermissionDocument extends Document { permissionId: number user: Schema.Types.ObjectId group: Schema.Types.ObjectId - client: Schema.Types.ObjectId } interface IPermission extends IPermissionDocument {} @@ -24,8 +23,7 @@ const permissionSchema = new Schema({ required: true }, user: { type: Schema.Types.ObjectId, ref: 'User' }, - group: { type: Schema.Types.ObjectId, ref: 'Group' }, - client: { type: Schema.Types.ObjectId, ref: 'Client' } + group: { type: Schema.Types.ObjectId, ref: 'Group' } }) permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' }) diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index df2c662..7255a4b 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -108,28 +108,6 @@ describe('permission', () => { expect(res.body.group).toBeTruthy() }) - it('should respond with new permission when principalType is client', async () => { - const dbclient = await clientController.createClient({ - clientId: '123456789', - clientSecret: '123456789' - }) - - const res = await request(app) - .post('/SASjsApi/permission') - .auth(adminAccessToken, { type: 'bearer' }) - .send({ - ...permission, - principalType: 'client', - principalId: dbclient.clientId - }) - .expect(200) - - expect(res.body.permissionId).toBeTruthy() - expect(res.body.uri).toEqual(permission.uri) - expect(res.body.setting).toEqual(permission.setting) - expect(res.body.clientId).toEqual(dbclient.clientId) - }) - it('should respond with Unauthorized if access token is not present', async () => { const res = await request(app) .post('/SASjsApi/permission') @@ -240,20 +218,6 @@ describe('permission', () => { expect(res.body).toEqual({}) }) - it('should respond with forbidden Request (403) if client is not found', async () => { - const res = await request(app) - .post('/SASjsApi/permission') - .auth(adminAccessToken, { type: 'bearer' }) - .send({ - ...permission, - principalType: 'client' - }) - .expect(403) - - expect(res.text).toEqual('Error: Client not found.') - expect(res.body).toEqual({}) - }) - it('should respond with forbidden Request (403) if principal type is not valid', async () => { const res = await request(app) .post('/SASjsApi/permission') @@ -265,7 +229,7 @@ describe('permission', () => { .expect(403) expect(res.text).toEqual( - 'Error: Invalid principal type. Valid types are user, group and client.' + 'Error: Invalid principal type. Valid types are user or group.' ) expect(res.body).toEqual({}) }) From 56523254525a66e756196e90b39a2b8cdadc1518 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 16 May 2022 23:53:30 +0500 Subject: [PATCH 17/54] feat: add basic UI for settings and permissions --- web/src/App.tsx | 4 + web/src/components/dialogTitle.tsx | 35 +++ web/src/components/header.tsx | 18 +- web/src/containers/Settings/index.tsx | 57 ++++ web/src/containers/Settings/permission.tsx | 318 +++++++++++++++++++++ web/src/index.css | 7 + 6 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 web/src/components/dialogTitle.tsx create mode 100644 web/src/containers/Settings/index.tsx create mode 100644 web/src/containers/Settings/permission.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index 1f13e31..f905708 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -8,6 +8,7 @@ import Header from './components/header' import Home from './components/home' import Drive from './containers/Drive' import Studio from './containers/Studio' +import Settings from './containers/Settings' import { AppContext } from './context/appContext' @@ -46,6 +47,9 @@ function App() { + + + diff --git a/web/src/components/dialogTitle.tsx b/web/src/components/dialogTitle.tsx new file mode 100644 index 0000000..8c78033 --- /dev/null +++ b/web/src/components/dialogTitle.tsx @@ -0,0 +1,35 @@ +import React, { Dispatch, SetStateAction } from 'react' + +import DialogTitle from '@mui/material/DialogTitle' +import IconButton from '@mui/material/IconButton' +import CloseIcon from '@mui/icons-material/Close' + +export interface DialogTitleProps { + id: string + children?: React.ReactNode + onClose: Dispatch> +} + +export const BootstrapDialogTitle = (props: DialogTitleProps) => { + const { children, onClose, ...other } = props + + return ( + + {children} + {onClose ? ( + onClose(false)} + sx={{ + position: 'absolute', + right: 8, + top: 8, + color: (theme) => theme.palette.grey[500] + }} + > + + + ) : null} + + ) +} diff --git a/web/src/components/header.tsx b/web/src/components/header.tsx index c499ae3..82b01ed 100644 --- a/web/src/components/header.tsx +++ b/web/src/components/header.tsx @@ -1,4 +1,4 @@ -import React, { useState, useContext } from 'react' +import React, { useState, useEffect, useContext } from 'react' import { Link, useHistory, useLocation } from 'react-router-dom' import { @@ -11,6 +11,7 @@ import { MenuItem } from '@mui/material' import OpenInNewIcon from '@mui/icons-material/OpenInNew' +import SettingsIcon from '@mui/icons-material/Settings' import Username from './username' import { AppContext } from '../context/appContext' @@ -29,6 +30,10 @@ const Header = (props: any) => { (EventTarget & HTMLButtonElement) | null >(null) + useEffect(() => { + setTabValue(pathname) + }, [pathname]) + const handleMenu = ( event: React.MouseEvent ) => { @@ -132,6 +137,17 @@ const Header = (props: any) => { open={!!anchorEl} onClose={handleClose} > + + + + + + + ) +} diff --git a/web/src/index.css b/web/src/index.css index 6f5fcc7..34d605c 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -18,3 +18,10 @@ code { flex-direction: column; align-items: center; } + +.permissions-page { + display: flex; + flex-direction: column; + padding: '5px 10px'; + margin-top: '10px'; +} From d000f7508f6d7384afffafee4179151fca802ca8 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 17 May 2022 15:42:29 +0500 Subject: [PATCH 18/54] fix: move permission filter modal to separate file and icons for different actions --- web/src/containers/Settings/permission.tsx | 155 ++++-------------- .../Settings/permissionFilterModal.tsx | 124 ++++++++++++++ 2 files changed, 156 insertions(+), 123 deletions(-) create mode 100644 web/src/containers/Settings/permissionFilterModal.tsx diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index d697151..42e2854 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -1,8 +1,7 @@ -import React, { useState, useEffect, Dispatch, SetStateAction } from 'react' +import React, { useState, useEffect } from 'react' import axios from 'axios' import { Box, - Button, Table, TableBody, TableCell, @@ -13,18 +12,17 @@ import { Grid, CircularProgress, IconButton, - Dialog, - DialogContent, - DialogActions, - TextField + Tooltip } from '@mui/material' -import Autocomplete from '@mui/material/Autocomplete' import FilterListIcon from '@mui/icons-material/FilterList' +import AddIcon from '@mui/icons-material/Add' +import EditIcon from '@mui/icons-material/Edit' +import DeleteForeverIcon from '@mui/icons-material/DeleteForever' import { styled } from '@mui/material/styles' -import { BootstrapDialogTitle } from '../../components/dialogTitle' +import PermissionFilterModal from './permissionFilterModal' interface UserResponse { id: number @@ -38,7 +36,7 @@ interface GroupResponse { description: string } -interface PermissionResponse { +export interface PermissionResponse { permissionId: number uri: string setting: string @@ -50,15 +48,6 @@ const BootstrapTableCell = styled(TableCell)({ textAlign: 'left' }) -const BootstrapDialog = styled(Dialog)(({ theme }) => ({ - '& .MuiDialogContent-root': { - padding: theme.spacing(2) - }, - '& .MuiDialogActions-root': { - padding: theme.spacing(1) - } -})) - const Permission = () => { const [isLoading, setIsLoading] = useState(false) const [filterModalOpen, setFilterModalOpen] = useState(false) @@ -147,10 +136,17 @@ const Permission = () => { - - - setFilterModalOpen(true)} /> - + + + + setFilterModalOpen(true)} /> + + + + + + + @@ -159,7 +155,7 @@ const Permission = () => { /> - { Uri Principal Setting + Action @@ -201,6 +198,18 @@ const PermissionTable = ({ permissions }: PermissionTableProps) => { {displayPrincipal(permission)} {permission.setting} + + + + + + + + + + + + ))} @@ -216,103 +225,3 @@ const displayPrincipal = (permission: PermissionResponse) => { return permission.group?.name } } - -type FilterModalProps = { - open: boolean - handleClose: Dispatch> - permissions: PermissionResponse[] - uriFilter: string[] - setUriFilter: Dispatch> - principalFilter: string[] - setPrincipalFilter: Dispatch> - settingFilter: string[] - setSettingFilter: Dispatch> - applyFilter: () => void - resetFilter: () => void -} - -const FilterModal = ({ - open, - handleClose, - permissions, - uriFilter, - setUriFilter, - principalFilter, - setPrincipalFilter, - settingFilter, - setSettingFilter, - applyFilter, - resetFilter -}: FilterModalProps) => { - const URIs = permissions.map((permission) => permission.uri) - const principals = permissions - .map((permission) => { - if (permission.user) return permission.user.displayName - if (permission.group) return permission.group.name - return '' - }) - .filter((principal) => principal !== '') - - return ( - - - Permission Filter - - - - - { - setUriFilter(newValue) - }} - renderInput={(params) => } - /> - - - { - setPrincipalFilter(newValue) - }} - renderInput={(params) => ( - - )} - /> - - - { - setSettingFilter(newValue) - }} - renderInput={(params) => ( - - )} - /> - - - - - - - - - ) -} diff --git a/web/src/containers/Settings/permissionFilterModal.tsx b/web/src/containers/Settings/permissionFilterModal.tsx new file mode 100644 index 0000000..f7d67e1 --- /dev/null +++ b/web/src/containers/Settings/permissionFilterModal.tsx @@ -0,0 +1,124 @@ +import React, { Dispatch, SetStateAction } from 'react' +import { + Button, + Grid, + Dialog, + DialogContent, + DialogActions, + TextField +} from '@mui/material' +import { styled } from '@mui/material/styles' +import Autocomplete from '@mui/material/Autocomplete' + +import { PermissionResponse } from './permission' +import { BootstrapDialogTitle } from '../../components/dialogTitle' +const BootstrapDialog = styled(Dialog)(({ theme }) => ({ + '& .MuiDialogContent-root': { + padding: theme.spacing(2) + }, + '& .MuiDialogActions-root': { + padding: theme.spacing(1) + } +})) + +type FilterModalProps = { + open: boolean + handleClose: Dispatch> + permissions: PermissionResponse[] + uriFilter: string[] + setUriFilter: Dispatch> + principalFilter: string[] + setPrincipalFilter: Dispatch> + settingFilter: string[] + setSettingFilter: Dispatch> + applyFilter: () => void + resetFilter: () => void +} + +const PermissionFilterModal = ({ + open, + handleClose, + permissions, + uriFilter, + setUriFilter, + principalFilter, + setPrincipalFilter, + settingFilter, + setSettingFilter, + applyFilter, + resetFilter +}: FilterModalProps) => { + const URIs = permissions.map((permission) => permission.uri) + const principals = permissions + .map((permission) => { + if (permission.user) return permission.user.displayName + if (permission.group) return permission.group.name + return '' + }) + .filter((principal) => principal !== '') + + return ( + + + Permission Filter + + + + + { + setUriFilter(newValue) + }} + renderInput={(params) => } + /> + + + { + setPrincipalFilter(newValue) + }} + renderInput={(params) => ( + + )} + /> + + + { + setSettingFilter(newValue) + }} + renderInput={(params) => ( + + )} + /> + + + + + + + + + ) +} + +export default PermissionFilterModal From 4fcc191ce9edc7e4dcd8821fb8019f4eea5db4ea Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 18 May 2022 00:03:11 +0500 Subject: [PATCH 19/54] fix: principalId type changed to number from any --- api/src/controllers/permission.ts | 4 ++-- api/src/utils/validation.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index d968692..c986d81 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -34,10 +34,10 @@ interface RegisterPermissionPayload { */ principalType: string /** - * The id of user(number), group(name), or client(clientId) to which a rule is assigned. + * The id of user or group to which a rule is assigned. * @example 123 */ - principalId: any + principalId: number } interface UpdatePermissionPayload { diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index a9d35c2..84a0138 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -79,7 +79,7 @@ export const registerPermissionValidation = (data: any): Joi.ValidationResult => uri: Joi.string().required(), setting: Joi.string().required(), principalType: Joi.string().required(), - principalId: Joi.any().required() + principalId: Joi.number().required() }).validate(data) export const updatePermissionValidation = (data: any): Joi.ValidationResult => From dfbd1557115afbe89da9eaf946561ef4d1f8866f Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 18 May 2022 00:04:37 +0500 Subject: [PATCH 20/54] chore: move common interfaces to utils folder --- web/src/utils/types.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 web/src/utils/types.ts diff --git a/web/src/utils/types.ts b/web/src/utils/types.ts new file mode 100644 index 0000000..9dd74d2 --- /dev/null +++ b/web/src/utils/types.ts @@ -0,0 +1,26 @@ +export interface UserResponse { + id: number + username: string + displayName: string +} + +export interface GroupResponse { + groupId: number + name: string + description: string +} + +export interface PermissionResponse { + permissionId: number + uri: string + setting: string + user?: UserResponse + group?: GroupResponse +} + +export interface RegisterPermissionPayload { + uri: string + setting: string + principalType: string + principalId: number +} From 1413b1850838ecc988ab289da4541bde36a9a346 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 18 May 2022 00:05:28 +0500 Subject: [PATCH 21/54] feat: created modal for adding permission --- web/src/components/dialogTitle.tsx | 8 +- .../Settings/addPermissionModal.tsx | 217 ++++++++++++++++++ web/src/containers/Settings/permission.tsx | 42 ++-- .../Settings/permissionFilterModal.tsx | 10 +- 4 files changed, 245 insertions(+), 32 deletions(-) create mode 100644 web/src/containers/Settings/addPermissionModal.tsx diff --git a/web/src/components/dialogTitle.tsx b/web/src/components/dialogTitle.tsx index 8c78033..5084809 100644 --- a/web/src/components/dialogTitle.tsx +++ b/web/src/components/dialogTitle.tsx @@ -7,19 +7,19 @@ import CloseIcon from '@mui/icons-material/Close' export interface DialogTitleProps { id: string children?: React.ReactNode - onClose: Dispatch> + handleOpen: Dispatch> } export const BootstrapDialogTitle = (props: DialogTitleProps) => { - const { children, onClose, ...other } = props + const { children, handleOpen, ...other } = props return ( {children} - {onClose ? ( + {handleOpen ? ( onClose(false)} + onClick={() => handleOpen(false)} sx={{ position: 'absolute', right: 8, diff --git a/web/src/containers/Settings/addPermissionModal.tsx b/web/src/containers/Settings/addPermissionModal.tsx new file mode 100644 index 0000000..0d413b8 --- /dev/null +++ b/web/src/containers/Settings/addPermissionModal.tsx @@ -0,0 +1,217 @@ +import React, { + useState, + useEffect, + useMemo, + Dispatch, + SetStateAction +} from 'react' +import axios from 'axios' +import { + Button, + Grid, + Dialog, + DialogContent, + DialogActions, + TextField, + CircularProgress +} from '@mui/material' +import { styled } from '@mui/material/styles' +import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete' + +import { BootstrapDialogTitle } from '../../components/dialogTitle' + +import { + PermissionResponse, + UserResponse, + GroupResponse, + RegisterPermissionPayload +} from '../../utils/types' + +const BootstrapDialog = styled(Dialog)(({ theme }) => ({ + '& .MuiDialogContent-root': { + padding: theme.spacing(2) + }, + '& .MuiDialogActions-root': { + padding: theme.spacing(1) + } +})) + +type AddPermissionModalProps = { + open: boolean + handleOpen: Dispatch> + permissions: PermissionResponse[] + addPermission: (addPermissionPayload: RegisterPermissionPayload) => void +} + +const filter = createFilterOptions() + +const AddPermissionModal = ({ + open, + handleOpen, + permissions, + addPermission +}: AddPermissionModalProps) => { + const [uri, setUri] = useState() + const [principalType, setPrincipalType] = useState('user') + const [userPrincipal, setUserPrincipal] = useState() + const [groupPrincipal, setGroupPrincipal] = useState() + const [permissionSetting, setPermissionSetting] = useState('Grant') + const [loadingPrincipals, setLoadingPrincipals] = useState(false) + const [userPrincipals, setUserPrincipals] = useState([]) + const [groupPrincipals, setGroupPrincipals] = useState([]) + + useEffect(() => { + setLoadingPrincipals(true) + axios + .get(`/SASjsApi/${principalType}`) + .then((res: any) => { + if (res.data) { + if (principalType === 'user') setUserPrincipals(res.data) + else setGroupPrincipals(res.data) + } + }) + .catch((err) => { + console.log(err) + }) + .finally(() => { + setLoadingPrincipals(false) + }) + }, [principalType]) + + const handleAddPermission = () => { + const addPermissionPayload: any = { + uri, + setting: permissionSetting, + principalType + } + if (principalType === 'user' && userPrincipal) { + addPermissionPayload.principalId = userPrincipal.id + } else if (principalType === 'group' && groupPrincipal) { + addPermissionPayload.principalId = groupPrincipal.groupId + } + addPermission(addPermissionPayload) + } + + const URIs = useMemo(() => { + return permissions.map((permission) => permission.uri) + }, [permissions]) + + const addButtonDisabled = + !uri || (principalType === 'user' ? !userPrincipal : !groupPrincipal) + + return ( + handleOpen(false)} open={open}> + + Add Permission + + + + + setUri(newValue)} + filterOptions={(options, params) => { + const filtered = filter(options, params) + + const { inputValue } = params + + const isExisting = options.some( + (option) => inputValue === option + ) + if (inputValue !== '' && !isExisting) { + filtered.push(inputValue) + } + return filtered + }} + selectOnFocus + clearOnBlur + handleHomeEndKeys + options={URIs} + renderOption={(props, option) =>
  • {option}
  • } + freeSolo + renderInput={(params) => } + /> +
    + + + setPrincipalType(newValue) + } + renderInput={(params) => ( + + )} + /> + + + {principalType === 'user' ? ( + option.displayName} + disableClearable + value={userPrincipal} + onChange={(event: any, newValue: UserResponse) => + setUserPrincipal(newValue) + } + renderInput={(params) => + loadingPrincipals ? ( + + ) : ( + + ) + } + /> + ) : ( + option.name} + disableClearable + value={groupPrincipal} + onChange={(event: any, newValue: GroupResponse) => + setGroupPrincipal(newValue) + } + renderInput={(params) => + loadingPrincipals ? ( + + ) : ( + + ) + } + /> + )} + + + + setPermissionSetting(newValue) + } + renderInput={(params) => ( + + )} + /> + +
    +
    + + + +
    + ) +} + +export default AddPermissionModal diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index 42e2854..ae4c17f 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -23,26 +23,9 @@ import DeleteForeverIcon from '@mui/icons-material/DeleteForever' import { styled } from '@mui/material/styles' import PermissionFilterModal from './permissionFilterModal' +import AddPermissionModal from './addPermissionModal' -interface UserResponse { - id: number - username: string - displayName: string -} - -interface GroupResponse { - groupId: number - name: string - description: string -} - -export interface PermissionResponse { - permissionId: number - uri: string - setting: string - user?: UserResponse - group?: GroupResponse -} +import { PermissionResponse } from '../../utils/types' const BootstrapTableCell = styled(TableCell)({ textAlign: 'left' @@ -50,6 +33,7 @@ const BootstrapTableCell = styled(TableCell)({ const Permission = () => { const [isLoading, setIsLoading] = useState(false) + const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false) const [filterModalOpen, setFilterModalOpen] = useState(false) const [uriFilter, setUriFilter] = useState([]) const [principalFilter, setPrincipalFilter] = useState([]) @@ -128,6 +112,8 @@ const Permission = () => { setFilterApplied(false) } + const addPermission = () => {} + return isLoading ? ( { setFilterModalOpen(true)} />
    - - + + setAddPermissionModalOpen(true)}> @@ -157,7 +147,7 @@ const Permission = () => { { applyFilter={applyFilter} resetFilter={resetFilter} /> +
    ) } @@ -192,7 +188,7 @@ const PermissionTable = ({ permissions }: PermissionTableProps) => { {permissions.map((permission) => ( - + {permission.uri} {displayPrincipal(permission)} diff --git a/web/src/containers/Settings/permissionFilterModal.tsx b/web/src/containers/Settings/permissionFilterModal.tsx index f7d67e1..7f7faab 100644 --- a/web/src/containers/Settings/permissionFilterModal.tsx +++ b/web/src/containers/Settings/permissionFilterModal.tsx @@ -10,7 +10,7 @@ import { import { styled } from '@mui/material/styles' import Autocomplete from '@mui/material/Autocomplete' -import { PermissionResponse } from './permission' +import { PermissionResponse } from '../../utils/types' import { BootstrapDialogTitle } from '../../components/dialogTitle' const BootstrapDialog = styled(Dialog)(({ theme }) => ({ '& .MuiDialogContent-root': { @@ -23,7 +23,7 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({ type FilterModalProps = { open: boolean - handleClose: Dispatch> + handleOpen: Dispatch> permissions: PermissionResponse[] uriFilter: string[] setUriFilter: Dispatch> @@ -37,7 +37,7 @@ type FilterModalProps = { const PermissionFilterModal = ({ open, - handleClose, + handleOpen, permissions, uriFilter, setUriFilter, @@ -58,10 +58,10 @@ const PermissionFilterModal = ({ .filter((principal) => principal !== '') return ( - + handleOpen(false)} open={open}> Permission Filter From e8c21a43b215f5fced0463b70747cda1191a4e01 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 18 May 2022 00:20:49 +0500 Subject: [PATCH 22/54] feat: add UI for updating permission --- web/src/containers/Settings/permission.tsx | 31 ++++++- .../Settings/updatePermissionModal.tsx | 80 +++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 web/src/containers/Settings/updatePermissionModal.tsx diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index ae4c17f..a668779 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -24,6 +24,7 @@ import { styled } from '@mui/material/styles' import PermissionFilterModal from './permissionFilterModal' import AddPermissionModal from './addPermissionModal' +import UpdatePermissionModal from './updatePermissionModal' import { PermissionResponse } from '../../utils/types' @@ -34,6 +35,10 @@ const BootstrapTableCell = styled(TableCell)({ const Permission = () => { const [isLoading, setIsLoading] = useState(false) const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false) + const [updatePermissionModalOpen, setUpdatePermissionModalOpen] = + useState(false) + const [selectedPermissionForUpdate, setSelectedPermissionForUpdate] = + useState() const [filterModalOpen, setFilterModalOpen] = useState(false) const [uriFilter, setUriFilter] = useState([]) const [principalFilter, setPrincipalFilter] = useState([]) @@ -114,6 +119,13 @@ const Permission = () => { const addPermission = () => {} + const handleUpdatePermissionClick = (permission: PermissionResponse) => { + setSelectedPermissionForUpdate(permission) + setUpdatePermissionModalOpen(true) + } + + const updatePermission = () => {} + return isLoading ? ( { @@ -164,6 +177,14 @@ const Permission = () => { permissions={permissions} addPermission={addPermission} /> + {selectedPermissionForUpdate && ( + + )} ) } @@ -172,9 +193,13 @@ export default Permission type PermissionTableProps = { permissions: PermissionResponse[] + handleUpdatePermissionClick: (permission: PermissionResponse) => void } -const PermissionTable = ({ permissions }: PermissionTableProps) => { +const PermissionTable = ({ + permissions, + handleUpdatePermissionClick +}: PermissionTableProps) => { return ( @@ -196,7 +221,9 @@ const PermissionTable = ({ permissions }: PermissionTableProps) => { {permission.setting} - + handleUpdatePermissionClick(permission)} + > diff --git a/web/src/containers/Settings/updatePermissionModal.tsx b/web/src/containers/Settings/updatePermissionModal.tsx new file mode 100644 index 0000000..e134f4f --- /dev/null +++ b/web/src/containers/Settings/updatePermissionModal.tsx @@ -0,0 +1,80 @@ +import React, { useState, Dispatch, SetStateAction } from 'react' +import { + Button, + Grid, + Dialog, + DialogContent, + DialogActions, + TextField +} from '@mui/material' +import { styled } from '@mui/material/styles' +import Autocomplete from '@mui/material/Autocomplete' + +import { BootstrapDialogTitle } from '../../components/dialogTitle' + +import { PermissionResponse } from '../../utils/types' + +const BootstrapDialog = styled(Dialog)(({ theme }) => ({ + '& .MuiDialogContent-root': { + padding: theme.spacing(2) + }, + '& .MuiDialogActions-root': { + padding: theme.spacing(1) + } +})) + +type UpdatePermissionModalProps = { + open: boolean + handleOpen: Dispatch> + permission: PermissionResponse + updatePermission: (setting: string) => void +} + +const UpdatePermissionModal = ({ + open, + handleOpen, + permission, + updatePermission +}: UpdatePermissionModalProps) => { + const [permissionSetting, setPermissionSetting] = useState('Grant') + + return ( + handleOpen(false)} open={open}> + + Update Permission + + + + + + setPermissionSetting(newValue) + } + renderInput={(params) => ( + + )} + /> + + + + + + + + ) +} + +export default UpdatePermissionModal From fa63dc071b92b4a5236c671442f0c01e33b53100 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 18 May 2022 00:29:42 +0500 Subject: [PATCH 23/54] chore: update specs and swagger.yaml --- api/public/swagger.yaml | 4 +++- api/src/routes/api/spec/permission.spec.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 8449e3d..8ef6453 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -486,7 +486,9 @@ components: description: 'Indicates the type of principal' example: user principalId: - description: 'The id of user(number), group(name), or client(clientId) to which a rule is assigned.' + type: number + format: double + description: 'The id of user or group to which a rule is assigned.' example: 123 required: - uri diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index 7255a4b..b740540 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -53,7 +53,7 @@ describe('permission', () => { let con: Mongoose let mongoServer: MongoMemoryServer let adminAccessToken: string - let dbUser: UserDetailsResponse | undefined + let dbUser: UserDetailsResponse beforeAll(async () => { app = await appPromise @@ -80,7 +80,7 @@ describe('permission', () => { const res = await request(app) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) - .send({ ...permission, principalId: dbUser?.id }) + .send({ ...permission, principalId: dbUser.id }) .expect(200) expect(res.body.permissionId).toBeTruthy() @@ -240,7 +240,7 @@ describe('permission', () => { beforeAll(async () => { dbPermission = await permissionController.createPermission({ ...permission, - principalId: dbUser?.id + principalId: dbUser.id }) }) @@ -313,7 +313,7 @@ describe('permission', () => { it('should delete permission', async () => { const dbPermission = await permissionController.createPermission({ ...permission, - principalId: dbUser?.id + principalId: dbUser.id }) const res = await request(app) .delete(`/SASjsApi/permission/${dbPermission?.permissionId}`) @@ -340,12 +340,12 @@ describe('permission', () => { await permissionController.createPermission({ ...permission, uri: '/test-1', - principalId: dbUser?.id + principalId: dbUser.id }) await permissionController.createPermission({ ...permission, uri: '/test-2', - principalId: dbUser?.id + principalId: dbUser.id }) }) From bdf63df1d915892486005ec904807749786b1c0c Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 23 Jun 2022 22:50:00 +0500 Subject: [PATCH 24/54] fix: add isAdmin attribute to return response of get session and login requests --- api/src/controllers/session.ts | 14 ++++++++++---- api/src/controllers/web.ts | 3 ++- api/src/routes/api/spec/web.spec.ts | 3 ++- web/src/components/login.tsx | 1 + web/src/context/appContext.tsx | 8 ++++++++ 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/api/src/controllers/session.ts b/api/src/controllers/session.ts index 0a3562a..bf53758 100644 --- a/api/src/controllers/session.ts +++ b/api/src/controllers/session.ts @@ -2,6 +2,10 @@ import express from 'express' import { Request, Security, Route, Tags, Example, Get } from 'tsoa' import { UserResponse } from './user' +interface SessionResponse extends UserResponse { + isAdmin: boolean +} + @Security('bearerAuth') @Route('SASjsApi/session') @Tags('Session') @@ -10,15 +14,16 @@ export class SessionController { * @summary Get session info (username). * */ - @Example({ + @Example({ id: 123, username: 'johnusername', - displayName: 'John' + displayName: 'John', + isAdmin: false }) @Get('/') public async session( @Request() request: express.Request - ): Promise { + ): Promise { return session(request) } } @@ -26,5 +31,6 @@ export class SessionController { const session = (req: express.Request) => ({ id: req.user!.userId, username: req.user!.username, - displayName: req.user!.displayName + displayName: req.user!.displayName, + isAdmin: req.user!.isAdmin }) diff --git a/api/src/controllers/web.ts b/api/src/controllers/web.ts index 7cef99d..ada7550 100644 --- a/api/src/controllers/web.ts +++ b/api/src/controllers/web.ts @@ -99,7 +99,8 @@ const login = async ( user: { id: user.id, username: user.username, - displayName: user.displayName + displayName: user.displayName, + isAdmin: user.isAdmin } } } diff --git a/api/src/routes/api/spec/web.spec.ts b/api/src/routes/api/spec/web.spec.ts index 4f8ec30..45b07f1 100644 --- a/api/src/routes/api/spec/web.spec.ts +++ b/api/src/routes/api/spec/web.spec.ts @@ -79,7 +79,8 @@ describe('web', () => { expect(res.body.user).toEqual({ id: expect.any(Number), username: user.username, - displayName: user.displayName + displayName: user.displayName, + isAdmin: user.isAdmin }) }) }) diff --git a/web/src/components/login.tsx b/web/src/components/login.tsx index a60baa7..706a714 100644 --- a/web/src/components/login.tsx +++ b/web/src/components/login.tsx @@ -30,6 +30,7 @@ const Login = () => { appContext.setUserId?.(user.id) appContext.setUsername?.(user.username) appContext.setDisplayName?.(user.displayName) + appContext.setIsAdmin?.(user.isAdmin) appContext.setLoggedIn?.(loggedIn) } } diff --git a/web/src/context/appContext.tsx b/web/src/context/appContext.tsx index a91d8e3..4802a57 100644 --- a/web/src/context/appContext.tsx +++ b/web/src/context/appContext.tsx @@ -29,6 +29,8 @@ interface AppContextProps { setUsername: Dispatch> | null displayName: string setDisplayName: Dispatch> | null + isAdmin: boolean + setIsAdmin: Dispatch> | null mode: ModeType runTimes: RunTimeType[] logout: (() => void) | null @@ -44,6 +46,8 @@ export const AppContext = createContext({ setUsername: null, displayName: '', setDisplayName: null, + isAdmin: false, + setIsAdmin: null, mode: ModeType.Server, runTimes: [], logout: null @@ -56,6 +60,7 @@ const AppContextProvider = (props: { children: ReactNode }) => { const [userId, setUserId] = useState(0) const [username, setUsername] = useState('') const [displayName, setDisplayName] = useState('') + const [isAdmin, setIsAdmin] = useState(false) const [mode, setMode] = useState(ModeType.Server) const [runTimes, setRunTimes] = useState([]) @@ -70,6 +75,7 @@ const AppContextProvider = (props: { children: ReactNode }) => { setUserId(data.id) setUsername(data.username) setDisplayName(data.displayName) + setIsAdmin(data.isAdmin) setLoggedIn(true) }) .catch(() => { @@ -107,6 +113,8 @@ const AppContextProvider = (props: { children: ReactNode }) => { setUsername, displayName, setDisplayName, + isAdmin, + setIsAdmin, mode, runTimes, logout From f863b81a7d40a1296a061ec93946f204382af2c3 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 23 Jun 2022 23:14:54 +0500 Subject: [PATCH 25/54] fix: show permission component only in server mode --- web/src/containers/Settings/index.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/web/src/containers/Settings/index.tsx b/web/src/containers/Settings/index.tsx index a1a0b70..a93d400 100644 --- a/web/src/containers/Settings/index.tsx +++ b/web/src/containers/Settings/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React, { useState, useContext } from 'react' import { Box, Paper, Tab, styled } from '@mui/material' import TabContext from '@mui/lab/TabContext' @@ -8,6 +8,8 @@ import TabPanel from '@mui/lab/TabPanel' import Permission from './permission' import Profile from './profile' +import { AppContext, ModeType } from '../../context/appContext' + const StyledTab = styled(Tab)({ background: 'black', margin: '0 5px 5px 0' @@ -18,7 +20,8 @@ const StyledTabpanel = styled(TabPanel)({ }) const Settings = () => { - const [value, setValue] = React.useState('profile') + const appContext = useContext(AppContext) + const [value, setValue] = useState('profile') const handleChange = (event: React.SyntheticEvent, newValue: string) => { setValue(newValue) @@ -43,7 +46,9 @@ const Settings = () => { onChange={handleChange} > - + {appContext.mode === ModeType.Server && ( + + )} From be8635ccc5eb34c3f0a5951c8a0421292ef69c97 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 23 Jun 2022 23:35:06 +0500 Subject: [PATCH 26/54] fix(web): only admin should be able to add, update or delete permission --- web/src/containers/Settings/permission.tsx | 60 +++++++++++++--------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index a668779..10f6fd5 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useContext } from 'react' import axios from 'axios' import { Box, @@ -27,12 +27,14 @@ import AddPermissionModal from './addPermissionModal' import UpdatePermissionModal from './updatePermissionModal' import { PermissionResponse } from '../../utils/types' +import { AppContext } from '../../context/appContext' const BootstrapTableCell = styled(TableCell)({ textAlign: 'left' }) const Permission = () => { + const appContext = useContext(AppContext) const [isLoading, setIsLoading] = useState(false) const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false) const [updatePermissionModalOpen, setUpdatePermissionModalOpen] = @@ -140,15 +142,17 @@ const Permission = () => { setFilterModalOpen(true)} /> - - setAddPermissionModalOpen(true)}> - - - + {appContext.isAdmin && ( + + setAddPermissionModalOpen(true)}> + + + + )} @@ -200,6 +204,8 @@ const PermissionTable = ({ permissions, handleUpdatePermissionClick }: PermissionTableProps) => { + const appContext = useContext(AppContext) + return (
    @@ -208,7 +214,9 @@ const PermissionTable = ({ UriPrincipalSetting - Action + {appContext.isAdmin && ( + Action + )} @@ -219,20 +227,22 @@ const PermissionTable = ({ {displayPrincipal(permission)} {permission.setting} - - - handleUpdatePermissionClick(permission)} - > - - - - - - - - - + {appContext.isAdmin && ( + + + handleUpdatePermissionClick(permission)} + > + + + + + + + + + + )} ))} From 5b319f9ad1f941b306db6b9473a2128b2e42bf76 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 23 Jun 2022 23:58:40 +0500 Subject: [PATCH 27/54] fix: remove duplicates principals from permission filter modal --- .../Settings/permissionFilterModal.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/web/src/containers/Settings/permissionFilterModal.tsx b/web/src/containers/Settings/permissionFilterModal.tsx index 7f7faab..962c3da 100644 --- a/web/src/containers/Settings/permissionFilterModal.tsx +++ b/web/src/containers/Settings/permissionFilterModal.tsx @@ -49,13 +49,21 @@ const PermissionFilterModal = ({ resetFilter }: FilterModalProps) => { const URIs = permissions.map((permission) => permission.uri) - const principals = permissions - .map((permission) => { - if (permission.user) return permission.user.displayName - if (permission.group) return permission.group.name - return '' - }) - .filter((principal) => principal !== '') + + // fetch all the principals from permissions array + let principals = permissions.map((permission) => { + if (permission.user) return permission.user.displayName + if (permission.group) return permission.group.name + return '' + }) + + // removes empty strings + principals = principals.filter((principal) => principal !== '') + + // removes the duplicates + principals = principals.filter( + (value, index, self) => self.indexOf(value) === index + ) return ( handleOpen(false)} open={open}> From 97ecfdc95563c72dbdecaebcb504e5194250a763 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 24 Jun 2022 14:48:57 +0500 Subject: [PATCH 28/54] feat: add, remove and update permissions from web component --- api/public/swagger.yaml | 24 ++- web/src/components/modal.tsx | 43 ++++++ .../Settings/addPermissionModal.tsx | 4 +- .../Settings/deletePermissionModal.tsx | 44 ++++++ web/src/containers/Settings/permission.tsx | 138 +++++++++++++++--- .../Settings/permissionFilterModal.tsx | 6 +- .../Settings/updatePermissionModal.tsx | 4 +- 7 files changed, 233 insertions(+), 30 deletions(-) create mode 100644 web/src/components/modal.tsx create mode 100644 web/src/containers/Settings/deletePermissionModal.tsx diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 5182869..9c78804 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -535,6 +535,24 @@ components: - setting type: object additionalProperties: false + SessionResponse: + properties: + id: + type: number + format: double + username: + type: string + displayName: + type: string + isAdmin: + type: boolean + required: + - id + - username + - displayName + - isAdmin + type: object + additionalProperties: false ExecuteReturnJsonPayload: properties: _program: @@ -638,7 +656,7 @@ paths: application/json: schema: properties: - user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object} + user: {properties: {isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [isAdmin, displayName, username, id], type: object} loggedIn: {type: boolean} required: - user @@ -1589,10 +1607,10 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UserResponse' + $ref: '#/components/schemas/SessionResponse' examples: 'Example 1': - value: {id: 123, username: johnusername, displayName: John} + value: {id: 123, username: johnusername, displayName: John, isAdmin: false} summary: 'Get session info (username).' tags: - Session diff --git a/web/src/components/modal.tsx b/web/src/components/modal.tsx new file mode 100644 index 0000000..223bd9a --- /dev/null +++ b/web/src/components/modal.tsx @@ -0,0 +1,43 @@ +import React from 'react' + +import { Typography, Dialog, DialogContent } from '@mui/material' +import { styled } from '@mui/material/styles' + +import { BootstrapDialogTitle } from './dialogTitle' + +const BootstrapDialog = styled(Dialog)(({ theme }) => ({ + '& .MuiDialogContent-root': { + padding: theme.spacing(2) + }, + '& .MuiDialogActions-root': { + padding: theme.spacing(1) + } +})) + +export interface ModalProps { + open: boolean + setOpen: React.Dispatch> + title: string + payload: string +} + +const Modal = (props: ModalProps) => { + const { open, setOpen, title, payload } = props + + return ( +
    + setOpen(false)} open={open}> + + {title} + + + + {payload} + + + +
    + ) +} + +export default Modal diff --git a/web/src/containers/Settings/addPermissionModal.tsx b/web/src/containers/Settings/addPermissionModal.tsx index 0d413b8..ac113b8 100644 --- a/web/src/containers/Settings/addPermissionModal.tsx +++ b/web/src/containers/Settings/addPermissionModal.tsx @@ -93,7 +93,9 @@ const AddPermissionModal = ({ } const URIs = useMemo(() => { - return permissions.map((permission) => permission.uri) + return permissions + .map((permission) => permission.uri) + .filter((uri, index, array) => array.indexOf(uri) === index) }, [permissions]) const addButtonDisabled = diff --git a/web/src/containers/Settings/deletePermissionModal.tsx b/web/src/containers/Settings/deletePermissionModal.tsx new file mode 100644 index 0000000..59527bd --- /dev/null +++ b/web/src/containers/Settings/deletePermissionModal.tsx @@ -0,0 +1,44 @@ +import React from 'react' + +import { + Button, + Dialog, + DialogContent, + DialogActions, + Typography +} from '@mui/material' +import { styled } from '@mui/material/styles' + +const BootstrapDialog = styled(Dialog)(({ theme }) => ({ + '& .MuiDialogContent-root': { + padding: theme.spacing(2) + }, + '& .MuiDialogActions-root': { + padding: theme.spacing(1) + } +})) + +type DeleteModalProps = { + open: boolean + setOpen: React.Dispatch> + deletePermission: () => void +} + +const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => { + return ( + setOpen(false)} open={open}> + + + Are you sure to delete this permission? + + + + + + + ) +} + +export default DeleteModal diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index 10f6fd5..36712d4 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useContext } from 'react' +import React, { useState, useEffect, useContext, useCallback } from 'react' import axios from 'axios' import { Box, @@ -22,11 +22,16 @@ import DeleteForeverIcon from '@mui/icons-material/DeleteForever' import { styled } from '@mui/material/styles' +import Modal from '../../components/modal' import PermissionFilterModal from './permissionFilterModal' import AddPermissionModal from './addPermissionModal' import UpdatePermissionModal from './updatePermissionModal' +import DeleteModal from './deletePermissionModal' -import { PermissionResponse } from '../../utils/types' +import { + PermissionResponse, + RegisterPermissionPayload +} from '../../utils/types' import { AppContext } from '../../context/appContext' const BootstrapTableCell = styled(TableCell)({ @@ -36,10 +41,14 @@ const BootstrapTableCell = styled(TableCell)({ const Permission = () => { const appContext = useContext(AppContext) const [isLoading, setIsLoading] = useState(false) + const [openModal, setOpenModal] = useState(false) + const [modalTitle, setModalTitle] = useState('') + const [modalPayload, setModalPayload] = useState('') const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false) const [updatePermissionModalOpen, setUpdatePermissionModalOpen] = useState(false) - const [selectedPermissionForUpdate, setSelectedPermissionForUpdate] = + const [deleteModalOpen, setDeleteModalOpen] = useState(false) + const [selectedPermission, setSelectedPermission] = useState() const [filterModalOpen, setFilterModalOpen] = useState(false) const [uriFilter, setUriFilter] = useState([]) @@ -51,8 +60,7 @@ const Permission = () => { >([]) const [filterApplied, setFilterApplied] = useState(false) - useEffect(() => { - setIsLoading(true) + const fetchPermissions = useCallback(() => { axios .get(`/SASjsApi/permission`) .then((res: any) => { @@ -61,13 +69,16 @@ const Permission = () => { } }) .catch((err) => { - console.log(err) - }) - .finally(() => { - setIsLoading(false) + setModalTitle('Abort') + setModalPayload(typeof err === 'object' ? err.toSting() : err) + setOpenModal(true) }) }, []) + useEffect(() => { + fetchPermissions() + }, [fetchPermissions]) + /** * first find the permissions w.r.t each filter type * take intersection of resultant arrays @@ -119,14 +130,82 @@ const Permission = () => { setFilterApplied(false) } - const addPermission = () => {} + const addPermission = (addPermissionPayload: RegisterPermissionPayload) => { + setAddPermissionModalOpen(false) + setIsLoading(true) + axios + .post('/SASjsApi/permission', addPermissionPayload) + .then((res: any) => { + fetchPermissions() + setModalTitle('Success') + setModalPayload('Permission added Successfully.') + setOpenModal(true) + }) + .catch((err) => { + setModalTitle('Abort') + setModalPayload(typeof err === 'object' ? err.toSting() : err) + setOpenModal(true) + }) + .finally(() => { + setIsLoading(false) + }) + } const handleUpdatePermissionClick = (permission: PermissionResponse) => { - setSelectedPermissionForUpdate(permission) + setSelectedPermission(permission) setUpdatePermissionModalOpen(true) } - const updatePermission = () => {} + const updatePermission = (setting: string) => { + setUpdatePermissionModalOpen(false) + setIsLoading(true) + axios + .patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, { + setting + }) + .then((res: any) => { + fetchPermissions() + setModalTitle('Success') + setModalPayload('Permission updated Successfully.') + setOpenModal(true) + }) + .catch((err) => { + setModalTitle('Abort') + setModalPayload(typeof err === 'object' ? err.toSting() : err) + setOpenModal(true) + }) + .finally(() => { + setIsLoading(false) + setSelectedPermission(undefined) + }) + } + + const handleDeletePermissionClick = (permission: PermissionResponse) => { + setSelectedPermission(permission) + setDeleteModalOpen(true) + } + + const deletePermission = () => { + setDeleteModalOpen(false) + setIsLoading(true) + axios + .delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`) + .then((res: any) => { + fetchPermissions() + setModalTitle('Success') + setModalPayload('Permission deleted Successfully.') + setOpenModal(true) + }) + .catch((err) => { + setModalTitle('Abort') + setModalPayload(typeof err === 'object' ? err.toSting() : err) + setOpenModal(true) + }) + .finally(() => { + setIsLoading(false) + setSelectedPermission(undefined) + }) + } return isLoading ? ( { + { permissions={permissions} addPermission={addPermission} /> - {selectedPermissionForUpdate && ( - - )} + + ) } @@ -198,11 +287,13 @@ export default Permission type PermissionTableProps = { permissions: PermissionResponse[] handleUpdatePermissionClick: (permission: PermissionResponse) => void + handleDeletePermissionClick: (permission: PermissionResponse) => void } const PermissionTable = ({ permissions, - handleUpdatePermissionClick + handleUpdatePermissionClick, + handleDeletePermissionClick }: PermissionTableProps) => { const appContext = useContext(AppContext) @@ -237,7 +328,10 @@ const PermissionTable = ({ - + handleDeletePermissionClick(permission)} + > diff --git a/web/src/containers/Settings/permissionFilterModal.tsx b/web/src/containers/Settings/permissionFilterModal.tsx index 962c3da..a487276 100644 --- a/web/src/containers/Settings/permissionFilterModal.tsx +++ b/web/src/containers/Settings/permissionFilterModal.tsx @@ -48,7 +48,9 @@ const PermissionFilterModal = ({ applyFilter, resetFilter }: FilterModalProps) => { - const URIs = permissions.map((permission) => permission.uri) + const URIs = permissions + .map((permission) => permission.uri) + .filter((uri, index, array) => array.indexOf(uri) === index) // fetch all the principals from permissions array let principals = permissions.map((permission) => { @@ -62,7 +64,7 @@ const PermissionFilterModal = ({ // removes the duplicates principals = principals.filter( - (value, index, self) => self.indexOf(value) === index + (principal, index, array) => array.indexOf(principal) === index ) return ( diff --git a/web/src/containers/Settings/updatePermissionModal.tsx b/web/src/containers/Settings/updatePermissionModal.tsx index e134f4f..796017d 100644 --- a/web/src/containers/Settings/updatePermissionModal.tsx +++ b/web/src/containers/Settings/updatePermissionModal.tsx @@ -26,7 +26,7 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({ type UpdatePermissionModalProps = { open: boolean handleOpen: Dispatch> - permission: PermissionResponse + permission: PermissionResponse | undefined updatePermission: (setting: string) => void } @@ -68,7 +68,7 @@ const UpdatePermissionModal = ({ From 67fe298fd588cda7f415a8393c3c4f2d7139fbb6 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 24 Jun 2022 14:55:05 +0500 Subject: [PATCH 29/54] chore: lint fixes --- api/src/utils/getRunTimeAndFilePath.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/utils/getRunTimeAndFilePath.ts b/api/src/utils/getRunTimeAndFilePath.ts index b83cdee..259b0a9 100644 --- a/api/src/utils/getRunTimeAndFilePath.ts +++ b/api/src/utils/getRunTimeAndFilePath.ts @@ -5,7 +5,7 @@ import { RunTimeType } from '.' export const getRunTimeAndFilePath = async (programPath: string) => { const ext = path.extname(programPath) - // If programPath (_program) is provided with a ".sas" or ".js" extension + // If programPath (_program) is provided with a ".sas" or ".js" extension // we should use that extension to determine the appropriate runTime if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) { const runTime = ext.slice(1) From 54d4bf835db177362eba1b1ec94579cc3b796e37 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 24 Jun 2022 15:50:09 +0500 Subject: [PATCH 30/54] chore: show principal type in permissions list --- web/src/containers/Settings/permission.tsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index 36712d4..3f060a0 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -38,6 +38,11 @@ const BootstrapTableCell = styled(TableCell)({ textAlign: 'left' }) +enum PrincipalType { + User = 'User', + Group = 'Group' +} + const Permission = () => { const appContext = useContext(AppContext) const [isLoading, setIsLoading] = useState(false) @@ -304,6 +309,7 @@ const PermissionTable = ({ Uri Principal + Type Setting {appContext.isAdmin && ( Action @@ -317,6 +323,9 @@ const PermissionTable = ({ {displayPrincipal(permission)} + + {displayPrincipalType(permission)} + {permission.setting} {appContext.isAdmin && ( @@ -346,9 +355,11 @@ const PermissionTable = ({ } const displayPrincipal = (permission: PermissionResponse) => { - if (permission.user) { - return permission.user?.displayName - } else if (permission.group) { - return permission.group?.name - } + if (permission.user) return permission.user?.displayName + if (permission.group) return permission.group?.name +} + +const displayPrincipalType = (permission: PermissionResponse) => { + if (permission.user) return PrincipalType.User + if (permission.group) return PrincipalType.Group } From 9cb9e2dd33bc9c1a6534f9832b061b9243d5ae86 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 24 Jun 2022 16:28:41 +0500 Subject: [PATCH 31/54] chore: add filter based on principal type --- web/src/containers/Settings/permission.tsx | 35 ++++++++++++++++--- .../Settings/permissionFilterModal.tsx | 22 +++++++++++- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index 3f060a0..30deb6b 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -38,7 +38,7 @@ const BootstrapTableCell = styled(TableCell)({ textAlign: 'left' }) -enum PrincipalType { +export enum PrincipalType { User = 'User', Group = 'Group' } @@ -58,6 +58,9 @@ const Permission = () => { const [filterModalOpen, setFilterModalOpen] = useState(false) const [uriFilter, setUriFilter] = useState([]) const [principalFilter, setPrincipalFilter] = useState([]) + const [principalTypeFilter, setPrincipalTypeFilter] = useState< + PrincipalType[] + >([]) const [settingFilter, setSettingFilter] = useState([]) const [permissions, setPermissions] = useState([]) const [filteredPermissions, setFilteredPermissions] = useState< @@ -93,17 +96,33 @@ const Permission = () => { uriFilter.length > 0 ? permissions.filter((permission) => uriFilter.includes(permission.uri)) : permissions + const principalFilteredPermissions = principalFilter.length > 0 ? permissions.filter((permission) => { if (permission.user) { - return principalFilter.includes(permission.user.displayName) - } else if (permission.group) { + return principalFilter.includes(permission.user.username) + } + if (permission.group) { return principalFilter.includes(permission.group.name) } return false }) : permissions + + const principalTypeFilteredPermissions = + principalTypeFilter.length > 0 + ? permissions.filter((permission) => { + if (permission.user) { + return principalTypeFilter.includes(PrincipalType.User) + } + if (permission.group) { + return principalTypeFilter.includes(PrincipalType.Group) + } + return false + }) + : permissions + const settingFilteredPermissions = settingFilter.length > 0 ? permissions.filter((permission) => @@ -117,6 +136,12 @@ const Permission = () => { ) ) + filteredArray = filteredArray.filter((permission) => + principalTypeFilteredPermissions.some( + (item) => item.permissionId === permission.permissionId + ) + ) + filteredArray = filteredArray.filter((permission) => settingFilteredPermissions.some( (item) => item.permissionId === permission.permissionId @@ -261,6 +286,8 @@ const Permission = () => { setUriFilter={setUriFilter} principalFilter={principalFilter} setPrincipalFilter={setPrincipalFilter} + principalTypeFilter={principalTypeFilter} + setPrincipalTypeFilter={setPrincipalTypeFilter} settingFilter={settingFilter} setSettingFilter={setSettingFilter} applyFilter={applyFilter} @@ -355,7 +382,7 @@ const PermissionTable = ({ } const displayPrincipal = (permission: PermissionResponse) => { - if (permission.user) return permission.user?.displayName + if (permission.user) return permission.user?.username if (permission.group) return permission.group?.name } diff --git a/web/src/containers/Settings/permissionFilterModal.tsx b/web/src/containers/Settings/permissionFilterModal.tsx index a487276..bbe9177 100644 --- a/web/src/containers/Settings/permissionFilterModal.tsx +++ b/web/src/containers/Settings/permissionFilterModal.tsx @@ -12,6 +12,8 @@ import Autocomplete from '@mui/material/Autocomplete' import { PermissionResponse } from '../../utils/types' import { BootstrapDialogTitle } from '../../components/dialogTitle' +import { PrincipalType } from './permission' + const BootstrapDialog = styled(Dialog)(({ theme }) => ({ '& .MuiDialogContent-root': { padding: theme.spacing(2) @@ -29,6 +31,8 @@ type FilterModalProps = { setUriFilter: Dispatch> principalFilter: string[] setPrincipalFilter: Dispatch> + principalTypeFilter: PrincipalType[] + setPrincipalTypeFilter: Dispatch> settingFilter: string[] setSettingFilter: Dispatch> applyFilter: () => void @@ -43,6 +47,8 @@ const PermissionFilterModal = ({ setUriFilter, principalFilter, setPrincipalFilter, + principalTypeFilter, + setPrincipalTypeFilter, settingFilter, setSettingFilter, applyFilter, @@ -54,7 +60,7 @@ const PermissionFilterModal = ({ // fetch all the principals from permissions array let principals = permissions.map((permission) => { - if (permission.user) return permission.user.displayName + if (permission.user) return permission.user.username if (permission.group) return permission.group.name return '' }) @@ -103,6 +109,20 @@ const PermissionFilterModal = ({ )} /> + + { + setPrincipalTypeFilter(newValue) + }} + renderInput={(params) => ( + + )} + /> + Date: Fri, 24 Jun 2022 22:32:18 +0500 Subject: [PATCH 32/54] chore: close filterModal after applying/reseting --- web/src/containers/Settings/permission.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index 30deb6b..c6682ce 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -92,6 +92,8 @@ const Permission = () => { * take intersection of resultant arrays */ const applyFilter = () => { + setFilterModalOpen(false) + const uriFilteredPermissions = uriFilter.length > 0 ? permissions.filter((permission) => uriFilter.includes(permission.uri)) @@ -153,6 +155,7 @@ const Permission = () => { } const resetFilter = () => { + setFilterModalOpen(false) setUriFilter([]) setPrincipalFilter([]) setSettingFilter([]) From 907aa485fdf76ee5decc2f450ac27707fc636e7b Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 24 Jun 2022 23:15:41 +0500 Subject: [PATCH 33/54] chore: throw error when creating duplicate permission --- api/src/controllers/permission.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index c986d81..1fa2108 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -163,10 +163,17 @@ const createPermission = async ({ let group: GroupResponse | undefined switch (principalType) { - case 'user': + case 'user': { const userInDB = await User.findOne({ id: principalId }) if (!userInDB) throw new Error('User not found.') + const alreadyExists = await Permission.findOne({ + uri, + user: userInDB._id + }) + if (alreadyExists) + throw new Error('Permission already exists with provided URI and User.') + permission.user = userInDB._id user = { @@ -175,10 +182,20 @@ const createPermission = async ({ displayName: userInDB.displayName } break - case 'group': + } + case 'group': { const groupInDB = await Group.findOne({ groupId: principalId }) if (!groupInDB) throw new Error('Group not found.') + const alreadyExists = await Permission.findOne({ + uri, + group: groupInDB._id + }) + if (alreadyExists) + throw new Error( + 'Permission already exists with provided URI and Group.' + ) + permission.group = groupInDB._id group = { @@ -187,6 +204,7 @@ const createPermission = async ({ description: groupInDB.description } break + } default: throw new Error('Invalid principal type. Valid types are user or group.') } From 35439d7d518e22d8a2b947defedc6b749042b7d1 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 24 Jun 2022 23:19:19 +0500 Subject: [PATCH 34/54] chore: throw error when adding permission for admin user --- api/src/controllers/permission.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index 1fa2108..f8e3422 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -167,10 +167,14 @@ const createPermission = async ({ const userInDB = await User.findOne({ id: principalId }) if (!userInDB) throw new Error('User not found.') + if (userInDB.isAdmin) + throw new Error('Can not add permission for admin user.') + const alreadyExists = await Permission.findOne({ uri, user: userInDB._id }) + if (alreadyExists) throw new Error('Permission already exists with provided URI and User.') From 4ddfec0403fa97b97b2c1fa90a916b934aef4a52 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Sun, 26 Jun 2022 01:48:31 +0500 Subject: [PATCH 35/54] chore: add isAdmin field in user response --- api/public/swagger.yaml | 31 +++++++--------------------- api/src/controllers/group.ts | 2 +- api/src/controllers/permission.ts | 28 +++++++++++++++++++------ api/src/controllers/session.ts | 8 ++----- api/src/controllers/user.ts | 9 +++++--- api/src/routes/api/spec/user.spec.ts | 12 +++++++---- 6 files changed, 47 insertions(+), 43 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 9c78804..237b77e 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -314,10 +314,13 @@ components: type: string displayName: type: string + isAdmin: + type: boolean required: - id - username - displayName + - isAdmin type: object additionalProperties: false GroupResponse: @@ -535,24 +538,6 @@ components: - setting type: object additionalProperties: false - SessionResponse: - properties: - id: - type: number - format: double - username: - type: string - displayName: - type: string - isAdmin: - type: boolean - required: - - id - - username - - displayName - - isAdmin - type: object - additionalProperties: false ExecuteReturnJsonPayload: properties: _program: @@ -1066,7 +1051,7 @@ paths: type: array examples: 'Example 1': - value: [{id: 123, username: johnusername, displayName: John}, {id: 456, username: starkusername, displayName: Stark}] + value: [{id: 123, username: johnusername, displayName: John, isAdmin: false}, {id: 456, username: starkusername, displayName: Stark, isAdmin: true}] summary: 'Get list of all users (username, displayname). All users can request this.' tags: - User @@ -1509,7 +1494,7 @@ paths: type: array examples: 'Example 1': - value: [{permissionId: 123, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow'}}, {permissionId: 124, uri: /SASjsApi/code/execute, setting: Grant, group: {groupId: 1, name: DCGroup, description: 'This group represents Data Controller Users'}}] + 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'}}] summary: 'Get list of all permissions (uri, setting and userDetail).' tags: - Permission @@ -1528,7 +1513,7 @@ paths: $ref: '#/components/schemas/PermissionDetailsResponse' examples: 'Example 1': - value: {permissionId: 123, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow'}} + 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 @@ -1554,7 +1539,7 @@ paths: $ref: '#/components/schemas/PermissionDetailsResponse' examples: 'Example 1': - value: {permissionId: 123, uri: /SASjsApi/code/execute, setting: Grant, user: {id: 1, username: johnSnow01, displayName: 'John Snow'}} + 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 @@ -1607,7 +1592,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SessionResponse' + $ref: '#/components/schemas/UserResponse' examples: 'Example 1': value: {id: 123, username: johnusername, displayName: John, isAdmin: false} diff --git a/api/src/controllers/group.ts b/api/src/controllers/group.ts index 3a6ff66..1d324ef 100644 --- a/api/src/controllers/group.ts +++ b/api/src/controllers/group.ts @@ -198,7 +198,7 @@ const getGroup = async (findBy: GetGroupBy): Promise => { 'groupId name description isActive users -_id' ).populate( 'users', - 'id username displayName -_id' + 'id username displayName isAdmin -_id' )) as unknown as GroupDetailsResponse if (!group) throw { diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index f8e3422..608df5b 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -69,7 +69,12 @@ export class PermissionController { permissionId: 123, uri: '/SASjsApi/code/execute', setting: 'Grant', - user: { id: 1, username: 'johnSnow01', displayName: 'John Snow' } + user: { + id: 1, + username: 'johnSnow01', + displayName: 'John Snow', + isAdmin: false + } }, { permissionId: 124, @@ -95,7 +100,12 @@ export class PermissionController { permissionId: 123, uri: '/SASjsApi/code/execute', setting: 'Grant', - user: { id: 1, username: 'johnSnow01', displayName: 'John Snow' } + user: { + id: 1, + username: 'johnSnow01', + displayName: 'John Snow', + isAdmin: false + } }) @Post('/') public async createPermission( @@ -113,7 +123,12 @@ export class PermissionController { permissionId: 123, uri: '/SASjsApi/code/execute', setting: 'Grant', - user: { id: 1, username: 'johnSnow01', displayName: 'John Snow' } + user: { + id: 1, + username: 'johnSnow01', + displayName: 'John Snow', + isAdmin: false + } }) @Patch('{permissionId}') public async updatePermission( @@ -142,7 +157,7 @@ const getAllPermissions = async (): Promise => uri: 1, setting: 1 }) - .populate({ path: 'user', select: 'id username displayName -_id' }) + .populate({ path: 'user', select: 'id username displayName isAdmin -_id' }) .populate({ path: 'group', select: 'groupId name description -_id' @@ -183,7 +198,8 @@ const createPermission = async ({ user = { id: userInDB.id, username: userInDB.username, - displayName: userInDB.displayName + displayName: userInDB.displayName, + isAdmin: userInDB.isAdmin } break } @@ -241,7 +257,7 @@ const updatePermission = async ( uri: 1, setting: 1 }) - .populate({ path: 'user', select: 'id username displayName -_id' }) + .populate({ path: 'user', select: 'id username displayName isAdmin -_id' }) .populate({ path: 'group', select: 'groupId name description -_id' diff --git a/api/src/controllers/session.ts b/api/src/controllers/session.ts index bf53758..0571529 100644 --- a/api/src/controllers/session.ts +++ b/api/src/controllers/session.ts @@ -2,10 +2,6 @@ import express from 'express' import { Request, Security, Route, Tags, Example, Get } from 'tsoa' import { UserResponse } from './user' -interface SessionResponse extends UserResponse { - isAdmin: boolean -} - @Security('bearerAuth') @Route('SASjsApi/session') @Tags('Session') @@ -14,7 +10,7 @@ export class SessionController { * @summary Get session info (username). * */ - @Example({ + @Example({ id: 123, username: 'johnusername', displayName: 'John', @@ -23,7 +19,7 @@ export class SessionController { @Get('/') public async session( @Request() request: express.Request - ): Promise { + ): Promise { return session(request) } } diff --git a/api/src/controllers/user.ts b/api/src/controllers/user.ts index 1e72de6..f410853 100644 --- a/api/src/controllers/user.ts +++ b/api/src/controllers/user.ts @@ -24,6 +24,7 @@ export interface UserResponse { id: number username: string displayName: string + isAdmin: boolean } export interface UserDetailsResponse { @@ -48,12 +49,14 @@ export class UserController { { id: 123, username: 'johnusername', - displayName: 'John' + displayName: 'John', + isAdmin: false }, { id: 456, username: 'starkusername', - displayName: 'Stark' + displayName: 'Stark', + isAdmin: true } ]) @Get('/') @@ -200,7 +203,7 @@ export class UserController { const getAllUsers = async (): Promise => await User.find({}) - .select({ _id: 0, id: 1, username: 1, displayName: 1 }) + .select({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 }) .exec() const createUser = async (data: UserPayload): Promise => { diff --git a/api/src/routes/api/spec/user.spec.ts b/api/src/routes/api/spec/user.spec.ts index 1c4fd99..12e68d5 100644 --- a/api/src/routes/api/spec/user.spec.ts +++ b/api/src/routes/api/spec/user.spec.ts @@ -770,12 +770,14 @@ describe('user', () => { { id: expect.anything(), username: adminUser.username, - displayName: adminUser.displayName + displayName: adminUser.displayName, + isAdmin: adminUser.isAdmin }, { id: expect.anything(), username: user.username, - displayName: user.displayName + displayName: user.displayName, + isAdmin: user.isAdmin } ]) }) @@ -796,12 +798,14 @@ describe('user', () => { { id: expect.anything(), username: adminUser.username, - displayName: adminUser.displayName + displayName: adminUser.displayName, + isAdmin: adminUser.isAdmin }, { id: expect.anything(), username: 'randomUser', - displayName: user.displayName + displayName: user.displayName, + isAdmin: user.isAdmin } ]) }) From a75edbaa327ec2af49523c13996ac283061da7d8 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Sun, 26 Jun 2022 01:49:07 +0500 Subject: [PATCH 36/54] fix: do not show admin users in add permission modal --- web/src/containers/Settings/addPermissionModal.tsx | 9 +++++++-- web/src/utils/types.ts | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/src/containers/Settings/addPermissionModal.tsx b/web/src/containers/Settings/addPermissionModal.tsx index ac113b8..1395a85 100644 --- a/web/src/containers/Settings/addPermissionModal.tsx +++ b/web/src/containers/Settings/addPermissionModal.tsx @@ -66,8 +66,13 @@ const AddPermissionModal = ({ .get(`/SASjsApi/${principalType}`) .then((res: any) => { if (res.data) { - if (principalType === 'user') setUserPrincipals(res.data) - else setGroupPrincipals(res.data) + if (principalType === 'user') { + const users: UserResponse[] = res.data + const nonAdminUsers = users.filter((user) => !user.isAdmin) + setUserPrincipals(nonAdminUsers) + } else { + setGroupPrincipals(res.data) + } } }) .catch((err) => { diff --git a/web/src/utils/types.ts b/web/src/utils/types.ts index 9dd74d2..46fd226 100644 --- a/web/src/utils/types.ts +++ b/web/src/utils/types.ts @@ -2,6 +2,7 @@ export interface UserResponse { id: number username: string displayName: string + isAdmin: boolean } export interface GroupResponse { From 0a73a35547ce72d7bd33d53c2f49b0f55dbd1d8d Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 27 Jun 2022 23:21:48 +0500 Subject: [PATCH 37/54] chore: improve error handling --- api/src/controllers/permission.ts | 54 ++++++++++++++++++---- api/src/routes/api/permission.ts | 16 +++++-- api/src/routes/api/spec/permission.spec.ts | 30 ++++++------ web/src/containers/Settings/permission.tsx | 24 ++++++++-- 4 files changed, 91 insertions(+), 33 deletions(-) diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index 608df5b..a5d8bf6 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -180,10 +180,19 @@ const createPermission = async ({ switch (principalType) { case 'user': { const userInDB = await User.findOne({ id: principalId }) - if (!userInDB) throw new Error('User not found.') + if (!userInDB) + throw { + code: 404, + status: 'Not Found', + message: 'User not found.' + } if (userInDB.isAdmin) - throw new Error('Can not add permission for admin user.') + throw { + code: 400, + status: 'Bad Request', + message: 'Can not add permission for admin user.' + } const alreadyExists = await Permission.findOne({ uri, @@ -191,7 +200,11 @@ const createPermission = async ({ }) if (alreadyExists) - throw new Error('Permission already exists with provided URI and User.') + throw { + code: 409, + status: 'Conflict', + message: 'Permission already exists with provided URI and User.' + } permission.user = userInDB._id @@ -205,16 +218,23 @@ const createPermission = async ({ } case 'group': { const groupInDB = await Group.findOne({ groupId: principalId }) - if (!groupInDB) throw new Error('Group not found.') + if (!groupInDB) + throw { + code: 404, + status: 'Not Found', + message: 'Group not found.' + } const alreadyExists = await Permission.findOne({ uri, group: groupInDB._id }) if (alreadyExists) - throw new Error( - 'Permission already exists with provided URI and Group.' - ) + throw { + code: 409, + status: 'Conflict', + message: 'Permission already exists with provided URI and Group.' + } permission.group = groupInDB._id @@ -226,7 +246,11 @@ const createPermission = async ({ break } default: - throw new Error('Invalid principal type. Valid types are user or group.') + throw { + code: 400, + status: 'Bad Request', + message: 'Invalid principal type. Valid types are user or group.' + } } const savedPermission = await permission.save() @@ -262,13 +286,23 @@ const updatePermission = async ( path: 'group', select: 'groupId name description -_id' })) as unknown as PermissionDetailsResponse - if (!updatedPermission) throw new Error('Unable to update permission') + if (!updatedPermission) + throw { + code: 404, + status: 'Not Found', + message: 'Permission not found.' + } return updatedPermission } const deletePermission = async (id: number) => { const permission = await Permission.findOne({ id }) - if (!permission) throw new Error('Permission is not found.') + if (!permission) + throw { + code: 404, + status: 'Not Found', + message: 'Permission not found.' + } await Permission.deleteOne({ id }) } diff --git a/api/src/routes/api/permission.ts b/api/src/routes/api/permission.ts index 8abc0a1..0dd98b0 100644 --- a/api/src/routes/api/permission.ts +++ b/api/src/routes/api/permission.ts @@ -14,7 +14,9 @@ permissionRouter.get('/', authenticateAccessToken, async (req, res) => { const response = await controller.getAllPermissions() res.send(response) } catch (err: any) { - res.status(403).send(err.toString()) + const statusCode = err.code + delete err.code + res.status(statusCode).send(err.message) } }) @@ -30,7 +32,9 @@ permissionRouter.post( const response = await controller.createPermission(body) res.send(response) } catch (err: any) { - res.status(403).send(err.toString()) + const statusCode = err.code + delete err.code + res.status(statusCode).send(err.message) } } ) @@ -49,7 +53,9 @@ permissionRouter.patch( const response = await controller.updatePermission(permissionId, body) res.send(response) } catch (err: any) { - res.status(403).send(err.toString()) + const statusCode = err.code + delete err.code + res.status(statusCode).send(err.message) } } ) @@ -65,7 +71,9 @@ permissionRouter.delete( await controller.deletePermission(permissionId) res.status(200).send('Permission Deleted!') } catch (err: any) { - res.status(403).send(err.toString()) + const statusCode = err.code + delete err.code + res.status(statusCode).send(err.message) } } ) diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index b740540..57295b9 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -190,7 +190,7 @@ describe('permission', () => { expect(res.body).toEqual({}) }) - it('should respond with forbidden Request (403) if user is not found', async () => { + it('should respond with not found (404) if user is not found', async () => { const res = await request(app) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) @@ -198,13 +198,13 @@ describe('permission', () => { ...permission, principalId: 123 }) - .expect(403) + .expect(404) - expect(res.text).toEqual('Error: User not found.') + expect(res.text).toEqual('User not found.') expect(res.body).toEqual({}) }) - it('should respond with forbidden Request (403) if group is not found', async () => { + it('should respond with not found (404) if group is not found', async () => { const res = await request(app) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) @@ -212,13 +212,13 @@ describe('permission', () => { ...permission, principalType: 'group' }) - .expect(403) + .expect(404) - expect(res.text).toEqual('Error: Group not found.') + expect(res.text).toEqual('Group not found.') expect(res.body).toEqual({}) }) - it('should respond with forbidden Request (403) 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) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) @@ -226,10 +226,10 @@ describe('permission', () => { ...permission, principalType: 'invalid' }) - .expect(403) + .expect(400) expect(res.text).toEqual( - 'Error: Invalid principal type. Valid types are user or group.' + 'Invalid principal type. Valid types are user or group.' ) expect(res.body).toEqual({}) }) @@ -295,16 +295,16 @@ describe('permission', () => { expect(res.body).toEqual({}) }) - it('should respond with forbidden Request (403) if permission with provided id does not exists', async () => { + it('should respond with not found (404) if permission with provided id does not exists', async () => { const res = await request(app) .patch('/SASjsApi/permission/123') .auth(adminAccessToken, { type: 'bearer' }) .send({ setting: 'deny' }) - .expect(403) + .expect(404) - expect(res.text).toEqual('Error: Unable to update permission') + expect(res.text).toEqual('Permission not found.') expect(res.body).toEqual({}) }) }) @@ -324,14 +324,14 @@ describe('permission', () => { expect(res.text).toEqual('Permission Deleted!') }) - it('should respond with forbidden Request (403) if permission with provided id does not exists', async () => { + it('should respond with not found (404) if permission with provided id does not exists', async () => { const res = await request(app) .delete('/SASjsApi/permission/123') .auth(adminAccessToken, { type: 'bearer' }) .send() - .expect(403) + .expect(404) - expect(res.text).toEqual('Error: Permission is not found.') + expect(res.text).toEqual('Permission not found.') }) }) diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index c6682ce..d4652b4 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -78,7 +78,11 @@ const Permission = () => { }) .catch((err) => { setModalTitle('Abort') - setModalPayload(typeof err === 'object' ? err.toSting() : err) + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) setOpenModal(true) }) }, []) @@ -176,7 +180,11 @@ const Permission = () => { }) .catch((err) => { setModalTitle('Abort') - setModalPayload(typeof err === 'object' ? err.toSting() : err) + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) setOpenModal(true) }) .finally(() => { @@ -204,7 +212,11 @@ const Permission = () => { }) .catch((err) => { setModalTitle('Abort') - setModalPayload(typeof err === 'object' ? err.toSting() : err) + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) setOpenModal(true) }) .finally(() => { @@ -231,7 +243,11 @@ const Permission = () => { }) .catch((err) => { setModalTitle('Abort') - setModalPayload(typeof err === 'object' ? err.toSting() : err) + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) setOpenModal(true) }) .finally(() => { From ca64c13909a303662297f822fc51ac3056b3bf6e Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 28 Jun 2022 00:00:04 +0500 Subject: [PATCH 38/54] chore: add principal type and permission setting enums --- api/src/controllers/permission.ts | 20 +++++++++++++++----- api/src/routes/api/spec/permission.spec.ts | 12 ++++++------ api/src/utils/validation.ts | 9 +++++++-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index a5d8bf6..8b48612 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -17,6 +17,16 @@ import Group from '../model/Group' import { UserResponse } from './user' import { GroupResponse } from './group' +export enum PrincipalType { + user = 'user', + group = 'group' +} + +export enum PermissionSetting { + grant = 'Grant', + deny = 'Deny' +} + interface RegisterPermissionPayload { /** * Name of affected resource @@ -27,12 +37,12 @@ interface RegisterPermissionPayload { * The indication of whether (and to what extent) access is provided * @example "Grant" */ - setting: string + setting: PermissionSetting /** * Indicates the type of principal * @example "user" */ - principalType: string + principalType: PrincipalType /** * The id of user or group to which a rule is assigned. * @example 123 @@ -45,7 +55,7 @@ interface UpdatePermissionPayload { * The indication of whether (and to what extent) access is provided * @example "Grant" */ - setting: string + setting: PermissionSetting } export interface PermissionDetailsResponse { @@ -178,7 +188,7 @@ const createPermission = async ({ let group: GroupResponse | undefined switch (principalType) { - case 'user': { + case PrincipalType.user: { const userInDB = await User.findOne({ id: principalId }) if (!userInDB) throw { @@ -216,7 +226,7 @@ const createPermission = async ({ } break } - case 'group': { + case PrincipalType.group: { const groupInDB = await Group.findOne({ groupId: principalId }) if (!groupInDB) throw { diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index 57295b9..06b96fd 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -7,7 +7,9 @@ import { UserController, GroupController, ClientController, - PermissionController + PermissionController, + PrincipalType, + PermissionSetting } from '../../../controllers/' import { UserDetailsResponse, @@ -33,8 +35,8 @@ const user = { const permission = { uri: '/SASjsApi/code/execute', - setting: 'Grant', - principalType: 'user', + setting: PermissionSetting.grant, + principalType: PrincipalType.user, principalId: 123 } @@ -228,9 +230,7 @@ describe('permission', () => { }) .expect(400) - expect(res.text).toEqual( - 'Invalid principal type. Valid types are user or group.' - ) + expect(res.text).toEqual('"principalType" must be one of [user, group]') expect(res.body).toEqual({}) }) }) diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 28d7106..fd212d0 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -1,5 +1,6 @@ import Joi from 'joi' import { RunTimeType } from '.' +import { PermissionSetting, PrincipalType } from '../controllers' const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16) const passwordSchema = Joi.string().min(6).max(1024) @@ -89,8 +90,12 @@ export const registerClientValidation = (data: any): Joi.ValidationResult => export const registerPermissionValidation = (data: any): Joi.ValidationResult => Joi.object({ uri: Joi.string().required(), - setting: Joi.string().required(), - principalType: Joi.string().required(), + setting: Joi.string() + .required() + .valid(...Object.values(PermissionSetting)), + principalType: Joi.string() + .required() + .valid(...Object.values(PrincipalType)), principalId: Joi.number().required() }).validate(data) From 66a3537271a94905d733f24294eec88d2b900697 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 28 Jun 2022 06:50:35 +0500 Subject: [PATCH 39/54] chore: add specs --- api/src/routes/api/spec/permission.spec.ts | 100 ++++++++++++++++++--- api/src/utils/validation.ts | 4 +- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index 06b96fd..23c1fbc 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -192,7 +192,69 @@ describe('permission', () => { expect(res.body).toEqual({}) }) - it('should respond with not found (404) if user is not found', async () => { + it('should respond with Bad Request if principal type is not valid', async () => { + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + principalType: 'invalid' + }) + .expect(400) + + expect(res.text).toEqual('"principalType" must be one of [user, group]') + 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 principalId is not a number', async () => { + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + principalId: 'someCharacters' + }) + .expect(400) + + expect(res.text).toEqual('"principalId" must be a number') + expect(res.body).toEqual({}) + }) + + it('should respond with Bad Request if adding permission for admin user', async () => { + const adminUser = await userController.createUser({ + ...user, + username: 'adminUser', + isAdmin: true + }) + + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + principalId: adminUser.id + }) + .expect(400) + + expect(res.text).toEqual('Can not add permission for admin user.') + expect(res.body).toEqual({}) + }) + + it('should respond with Not Found (404) if user is not found', async () => { const res = await request(app) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) @@ -206,7 +268,7 @@ describe('permission', () => { expect(res.body).toEqual({}) }) - it('should respond with not found (404) if group is not found', async () => { + it('should respond with Not Found (404) if group is not found', async () => { const res = await request(app) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) @@ -220,17 +282,21 @@ describe('permission', () => { expect(res.body).toEqual({}) }) - it('should respond with Bad Request if principal type is not valid', async () => { + it('should respond with Conflict (409) if permission already exists', async () => { + await permissionController.createPermission({ + ...permission, + principalId: dbUser.id + }) + const res = await request(app) .post('/SASjsApi/permission') .auth(adminAccessToken, { type: 'bearer' }) - .send({ - ...permission, - principalType: 'invalid' - }) - .expect(400) + .send({ ...permission, principalId: dbUser.id }) + .expect(409) - expect(res.text).toEqual('"principalType" must be one of [user, group]') + expect(res.text).toEqual( + 'Permission already exists with provided URI and User.' + ) expect(res.body).toEqual({}) }) }) @@ -295,12 +361,26 @@ 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 not found (404) if permission with provided id does not exists', async () => { const res = await request(app) .patch('/SASjsApi/permission/123') .auth(adminAccessToken, { type: 'bearer' }) .send({ - setting: 'deny' + setting: PermissionSetting.deny }) .expect(404) diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index fd212d0..789b355 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -101,7 +101,9 @@ export const registerPermissionValidation = (data: any): Joi.ValidationResult => export const updatePermissionValidation = (data: any): Joi.ValidationResult => Joi.object({ - setting: Joi.string().required() + setting: Joi.string() + .required() + .valid(...Object.values(PermissionSetting)) }).validate(data) export const deployValidation = (data: any): Joi.ValidationResult => From 70f279a49cc3201f256086ee71b0ab167112eb66 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 28 Jun 2022 09:23:53 +0500 Subject: [PATCH 40/54] chore: update swagger.yaml --- api/public/swagger.yaml | 1484 ++++++++++++++++++----------------- api/src/utils/validation.ts | 3 +- 2 files changed, 748 insertions(+), 739 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 237b77e..e4b5a58 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -5,6 +5,225 @@ components: requestBodies: {} responses: {} schemas: + GroupResponse: + properties: + groupId: + type: number + format: double + name: + type: string + description: + type: string + required: + - groupId + - name + - description + type: object + additionalProperties: false + UserResponse: + properties: + id: + type: number + format: double + username: + type: string + displayName: + type: string + isAdmin: + type: boolean + required: + - id + - username + - displayName + - isAdmin + type: object + additionalProperties: false + GroupDetailsResponse: + properties: + groupId: + type: number + format: double + name: + type: string + description: + type: string + isActive: + type: boolean + users: + items: + $ref: '#/components/schemas/UserResponse' + type: array + required: + - groupId + - name + - description + - isActive + - users + type: object + additionalProperties: false + GroupPayload: + properties: + name: + type: string + description: 'Name of the group' + example: DCGroup + description: + type: string + description: 'Description of the group' + example: 'This group represents Data Controller Users' + isActive: + type: boolean + description: 'Group should be active or not, defaults to true' + example: 'true' + required: + - name + - description + type: object + additionalProperties: false + _LeanDocument__LeanDocument_T__: + properties: {} + type: object + Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__: + properties: + _id: + $ref: '#/components/schemas/_LeanDocument__LeanDocument_T__' + description: 'This documents _id.' + __v: + description: 'This documents __v.' + id: + description: 'The string version of this documents _id.' + type: object + description: 'From T, pick a set of properties whose keys are in the union K' + Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_: + $ref: '#/components/schemas/Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__' + description: 'Construct a type with the properties of T except for those in type K.' + LeanDocument_this_: + $ref: '#/components/schemas/Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_' + IGroup: + $ref: '#/components/schemas/LeanDocument_this_' + UserDetailsResponse: + properties: + id: + type: number + format: double + displayName: + type: string + username: + type: string + isActive: + type: boolean + isAdmin: + type: boolean + autoExec: + type: string + groups: + items: + $ref: '#/components/schemas/GroupResponse' + type: array + required: + - id + - displayName + - username + - isActive + - isAdmin + type: object + additionalProperties: false + UserPayload: + properties: + displayName: + type: string + description: 'Display name for user' + example: 'John Snow' + username: + type: string + description: 'Username for user' + example: johnSnow01 + password: + type: string + description: 'Password for user' + isAdmin: + type: boolean + description: 'Account should be admin or not, defaults to false' + example: 'false' + isActive: + type: boolean + description: 'Account should be active or not, defaults to true' + example: 'true' + autoExec: + type: string + description: 'User-specific auto-exec code' + example: "" + required: + - displayName + - username + - password + 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/GroupResponse' + 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 TokenResponse: properties: accessToken: @@ -305,160 +524,6 @@ components: - tree type: object additionalProperties: false - UserResponse: - properties: - id: - type: number - format: double - username: - type: string - displayName: - type: string - isAdmin: - type: boolean - required: - - id - - username - - displayName - - isAdmin - type: object - additionalProperties: false - GroupResponse: - properties: - groupId: - type: number - format: double - name: - type: string - description: - type: string - required: - - groupId - - name - - description - type: object - additionalProperties: false - UserDetailsResponse: - properties: - id: - type: number - format: double - displayName: - type: string - username: - type: string - isActive: - type: boolean - isAdmin: - type: boolean - autoExec: - type: string - groups: - items: - $ref: '#/components/schemas/GroupResponse' - type: array - required: - - id - - displayName - - username - - isActive - - isAdmin - type: object - additionalProperties: false - UserPayload: - properties: - displayName: - type: string - description: 'Display name for user' - example: 'John Snow' - username: - type: string - description: 'Username for user' - example: johnSnow01 - password: - type: string - description: 'Password for user' - isAdmin: - type: boolean - description: 'Account should be admin or not, defaults to false' - example: 'false' - isActive: - type: boolean - description: 'Account should be active or not, defaults to true' - example: 'true' - autoExec: - type: string - description: 'User-specific auto-exec code' - example: "" - required: - - displayName - - username - - password - type: object - additionalProperties: false - GroupDetailsResponse: - properties: - groupId: - type: number - format: double - name: - type: string - description: - type: string - isActive: - type: boolean - users: - items: - $ref: '#/components/schemas/UserResponse' - type: array - required: - - groupId - - name - - description - - isActive - - users - type: object - additionalProperties: false - GroupPayload: - properties: - name: - type: string - description: 'Name of the group' - example: DCGroup - description: - type: string - description: 'Description of the group' - example: 'This group represents Data Controller Users' - isActive: - type: boolean - description: 'Group should be active or not, defaults to true' - example: 'true' - required: - - name - - description - type: object - additionalProperties: false - _LeanDocument__LeanDocument_T__: - properties: {} - type: object - Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__: - properties: - id: - description: 'The string version of this documents _id.' - _id: - $ref: '#/components/schemas/_LeanDocument__LeanDocument_T__' - description: 'This documents _id.' - __v: - description: 'This documents __v.' - type: object - description: 'From T, pick a set of properties whose keys are in the union K' - Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_: - $ref: '#/components/schemas/Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__' - description: 'Construct a type with the properties of T except for those in type K.' - LeanDocument_this_: - $ref: '#/components/schemas/Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_' - IGroup: - $ref: '#/components/schemas/LeanDocument_this_' InfoResponse: properties: mode: @@ -483,61 +548,6 @@ components: - runTimes 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/GroupResponse' - required: - - permissionId - - uri - - setting - type: object - additionalProperties: false - RegisterPermissionPayload: - properties: - uri: - type: string - description: 'Name of affected resource' - example: /SASjsApi/code/execute - setting: - type: string - description: 'The indication of whether (and to what extent) access is provided' - example: Grant - principalType: - type: string - 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: - type: string - description: 'The indication of whether (and to what extent) access is provided' - example: Grant - required: - - setting - type: object - additionalProperties: false ExecuteReturnJsonPayload: properties: _program: @@ -559,6 +569,534 @@ info: name: '4GL Ltd' openapi: 3.0.0 paths: + /SASjsApi/group: + get: + operationId: GetAllGroups + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/GroupResponse' + type: array + examples: + 'Example 1': + value: [{groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users'}] + summary: 'Get list of all groups (groupName and groupDescription). All users can request this.' + tags: + - Group + security: + - + bearerAuth: [] + parameters: [] + post: + operationId: CreateGroup + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GroupDetailsResponse' + examples: + 'Example 1': + value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []} + summary: 'Create a new group. Admin only.' + tags: + - Group + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GroupPayload' + '/SASjsApi/group/by/groupname/{name}': + get: + operationId: GetGroupByGroupName + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GroupDetailsResponse' + summary: 'Get list of members of a group (userName). All users can request this.' + tags: + - Group + security: + - + bearerAuth: [] + parameters: + - + description: 'The group''s name' + in: path + name: name + required: true + schema: + type: string + '/SASjsApi/group/{groupId}': + get: + operationId: GetGroup + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GroupDetailsResponse' + summary: 'Get list of members of a group (userName). All users can request this.' + tags: + - Group + security: + - + bearerAuth: [] + parameters: + - + description: 'The group''s identifier' + in: path + name: groupId + required: true + schema: + format: double + type: number + example: 1234 + delete: + operationId: DeleteGroup + responses: + '200': + description: Ok + content: + application/json: + schema: + allOf: + - {$ref: '#/components/schemas/IGroup'} + - {properties: {_id: {}}, required: [_id], type: object} + summary: 'Delete a group. Admin task only.' + tags: + - Group + security: + - + bearerAuth: [] + parameters: + - + description: 'The group''s identifier' + in: path + name: groupId + required: true + schema: + format: double + type: number + example: 1234 + '/SASjsApi/group/{groupId}/{userId}': + post: + operationId: AddUserToGroup + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GroupDetailsResponse' + examples: + 'Example 1': + value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []} + summary: 'Add a user to a group. Admin task only.' + tags: + - Group + security: + - + bearerAuth: [] + parameters: + - + description: 'The group''s identifier' + in: path + name: groupId + required: true + schema: + format: double + type: number + example: '1234' + - + description: 'The user''s identifier' + in: path + name: userId + required: true + schema: + format: double + type: number + example: '6789' + delete: + operationId: RemoveUserFromGroup + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GroupDetailsResponse' + examples: + 'Example 1': + value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []} + summary: 'Remove a user to a group. Admin task only.' + tags: + - Group + security: + - + bearerAuth: [] + parameters: + - + description: 'The group''s identifier' + in: path + name: groupId + required: true + schema: + format: double + type: number + example: '1234' + - + description: 'The user''s identifier' + in: path + name: userId + required: true + schema: + format: double + type: number + example: '6789' + /SASjsApi/user: + get: + operationId: GetAllUsers + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/UserResponse' + type: array + examples: + 'Example 1': + value: [{id: 123, username: johnusername, displayName: John, isAdmin: false}, {id: 456, username: starkusername, displayName: Stark, isAdmin: true}] + summary: 'Get list of all users (username, displayname). All users can request this.' + tags: + - User + security: + - + bearerAuth: [] + parameters: [] + post: + operationId: CreateUser + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + examples: + 'Example 1': + value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} + summary: 'Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.' + tags: + - User + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserPayload' + '/SASjsApi/user/by/username/{username}': + get: + operationId: GetUserByUsername + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + description: 'Only Admin or user itself will get user autoExec code.' + summary: 'Get user properties - such as group memberships, userName, displayName.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The User''s username' + in: path + name: username + required: true + schema: + type: string + example: johnSnow01 + patch: + operationId: UpdateUserByUsername + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + examples: + 'Example 1': + value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} + summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The User''s username' + in: path + name: username + required: true + schema: + type: string + example: johnSnow01 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserPayload' + delete: + operationId: DeleteUserByUsername + responses: + '204': + description: 'No content' + summary: 'Delete a user. Can be performed either by admins, or the user in question.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The User''s username' + in: path + name: username + required: true + schema: + type: string + example: johnSnow01 + requestBody: + required: true + content: + application/json: + schema: + properties: + password: + type: string + type: object + '/SASjsApi/user/{userId}': + get: + operationId: GetUser + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + description: 'Only Admin or user itself will get user autoExec code.' + summary: 'Get user properties - such as group memberships, userName, displayName.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The user''s identifier' + in: path + name: userId + required: true + schema: + format: double + type: number + example: 1234 + patch: + operationId: UpdateUser + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + examples: + 'Example 1': + value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} + summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The user''s identifier' + in: path + name: userId + required: true + schema: + format: double + type: number + example: '1234' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserPayload' + delete: + operationId: DeleteUser + responses: + '204': + description: 'No content' + summary: 'Delete a user. Can be performed either by admins, or the user in question.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The user''s identifier' + in: path + name: userId + required: true + schema: + format: double + type: number + example: 1234 + requestBody: + required: true + content: + application/json: + schema: + properties: + password: + type: string + type: object + /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'}}] + 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 /SASjsApi/auth/token: post: operationId: Token @@ -1037,431 +1575,6 @@ paths: - bearerAuth: [] parameters: [] - /SASjsApi/user: - get: - operationId: GetAllUsers - responses: - '200': - description: Ok - content: - application/json: - schema: - items: - $ref: '#/components/schemas/UserResponse' - type: array - examples: - 'Example 1': - value: [{id: 123, username: johnusername, displayName: John, isAdmin: false}, {id: 456, username: starkusername, displayName: Stark, isAdmin: true}] - summary: 'Get list of all users (username, displayname). All users can request this.' - tags: - - User - security: - - - bearerAuth: [] - parameters: [] - post: - operationId: CreateUser - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/UserDetailsResponse' - examples: - 'Example 1': - value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} - summary: 'Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.' - tags: - - User - security: - - - bearerAuth: [] - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UserPayload' - '/SASjsApi/user/by/username/{username}': - get: - operationId: GetUserByUsername - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/UserDetailsResponse' - description: 'Only Admin or user itself will get user autoExec code.' - summary: 'Get user properties - such as group memberships, userName, displayName.' - tags: - - User - security: - - - bearerAuth: [] - parameters: - - - description: 'The User''s username' - in: path - name: username - required: true - schema: - type: string - example: johnSnow01 - patch: - operationId: UpdateUserByUsername - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/UserDetailsResponse' - examples: - 'Example 1': - value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} - summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.' - tags: - - User - security: - - - bearerAuth: [] - parameters: - - - description: 'The User''s username' - in: path - name: username - required: true - schema: - type: string - example: johnSnow01 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UserPayload' - delete: - operationId: DeleteUserByUsername - responses: - '204': - description: 'No content' - summary: 'Delete a user. Can be performed either by admins, or the user in question.' - tags: - - User - security: - - - bearerAuth: [] - parameters: - - - description: 'The User''s username' - in: path - name: username - required: true - schema: - type: string - example: johnSnow01 - requestBody: - required: true - content: - application/json: - schema: - properties: - password: - type: string - type: object - '/SASjsApi/user/{userId}': - get: - operationId: GetUser - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/UserDetailsResponse' - description: 'Only Admin or user itself will get user autoExec code.' - summary: 'Get user properties - such as group memberships, userName, displayName.' - tags: - - User - security: - - - bearerAuth: [] - parameters: - - - description: 'The user''s identifier' - in: path - name: userId - required: true - schema: - format: double - type: number - example: 1234 - patch: - operationId: UpdateUser - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/UserDetailsResponse' - examples: - 'Example 1': - value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} - summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.' - tags: - - User - security: - - - bearerAuth: [] - parameters: - - - description: 'The user''s identifier' - in: path - name: userId - required: true - schema: - format: double - type: number - example: '1234' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UserPayload' - delete: - operationId: DeleteUser - responses: - '204': - description: 'No content' - summary: 'Delete a user. Can be performed either by admins, or the user in question.' - tags: - - User - security: - - - bearerAuth: [] - parameters: - - - description: 'The user''s identifier' - in: path - name: userId - required: true - schema: - format: double - type: number - example: 1234 - requestBody: - required: true - content: - application/json: - schema: - properties: - password: - type: string - type: object - /SASjsApi/group: - get: - operationId: GetAllGroups - responses: - '200': - description: Ok - content: - application/json: - schema: - items: - $ref: '#/components/schemas/GroupResponse' - type: array - examples: - 'Example 1': - value: [{groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users'}] - summary: 'Get list of all groups (groupName and groupDescription). All users can request this.' - tags: - - Group - security: - - - bearerAuth: [] - parameters: [] - post: - operationId: CreateGroup - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/GroupDetailsResponse' - examples: - 'Example 1': - value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []} - summary: 'Create a new group. Admin only.' - tags: - - Group - security: - - - bearerAuth: [] - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/GroupPayload' - '/SASjsApi/group/by/groupname/{name}': - get: - operationId: GetGroupByGroupName - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/GroupDetailsResponse' - summary: 'Get list of members of a group (userName). All users can request this.' - tags: - - Group - security: - - - bearerAuth: [] - parameters: - - - description: 'The group''s name' - in: path - name: name - required: true - schema: - type: string - '/SASjsApi/group/{groupId}': - get: - operationId: GetGroup - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/GroupDetailsResponse' - summary: 'Get list of members of a group (userName). All users can request this.' - tags: - - Group - security: - - - bearerAuth: [] - parameters: - - - description: 'The group''s identifier' - in: path - name: groupId - required: true - schema: - format: double - type: number - example: 1234 - delete: - operationId: DeleteGroup - responses: - '200': - description: Ok - content: - application/json: - schema: - allOf: - - {$ref: '#/components/schemas/IGroup'} - - {properties: {_id: {}}, required: [_id], type: object} - summary: 'Delete a group. Admin task only.' - tags: - - Group - security: - - - bearerAuth: [] - parameters: - - - description: 'The group''s identifier' - in: path - name: groupId - required: true - schema: - format: double - type: number - example: 1234 - '/SASjsApi/group/{groupId}/{userId}': - post: - operationId: AddUserToGroup - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/GroupDetailsResponse' - examples: - 'Example 1': - value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []} - summary: 'Add a user to a group. Admin task only.' - tags: - - Group - security: - - - bearerAuth: [] - parameters: - - - description: 'The group''s identifier' - in: path - name: groupId - required: true - schema: - format: double - type: number - example: '1234' - - - description: 'The user''s identifier' - in: path - name: userId - required: true - schema: - format: double - type: number - example: '6789' - delete: - operationId: RemoveUserFromGroup - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/GroupDetailsResponse' - examples: - 'Example 1': - value: {groupId: 123, name: DCGroup, description: 'This group represents Data Controller Users', isActive: true, users: []} - summary: 'Remove a user to a group. Admin task only.' - tags: - - Group - security: - - - bearerAuth: [] - parameters: - - - description: 'The group''s identifier' - in: path - name: groupId - required: true - schema: - format: double - type: number - example: '1234' - - - description: 'The user''s identifier' - in: path - name: userId - required: true - schema: - format: double - type: number - example: '6789' /SASjsApi/info: get: operationId: Info @@ -1480,109 +1593,6 @@ paths: - 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, 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'}}] - 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 /SASjsApi/session: get: operationId: Session diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 789b355..cd91e80 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -1,6 +1,5 @@ import Joi from 'joi' -import { RunTimeType } from '.' -import { PermissionSetting, PrincipalType } from '../controllers' +import { PermissionSetting, PrincipalType } from '../controllers/permission' const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16) const passwordSchema = Joi.string().min(6).max(1024) From 7d916ec3e9ef579dde1b73015715cd01098c2018 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 29 Jun 2022 23:06:58 +0500 Subject: [PATCH 41/54] feat: add authorize middleware for validating permissions --- api/src/middlewares/authenticateToken.ts | 5 ++-- api/src/middlewares/authorize.ts | 34 ++++++++++++++++++++++++ api/src/middlewares/index.ts | 1 + 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 api/src/middlewares/authorize.ts diff --git a/api/src/middlewares/authenticateToken.ts b/api/src/middlewares/authenticateToken.ts index 90c7027..23d5d0a 100644 --- a/api/src/middlewares/authenticateToken.ts +++ b/api/src/middlewares/authenticateToken.ts @@ -3,6 +3,7 @@ import jwt from 'jsonwebtoken' import { csrfProtection } from '../app' import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils' import { desktopUser } from './desktop' +import { authorize } from './authorize' export const authenticateAccessToken: RequestHandler = async ( req, @@ -24,7 +25,7 @@ export const authenticateAccessToken: RequestHandler = async ( if (user) { if (user.isActive) { req.user = user - return csrfProtection(req, res, next) + return csrfProtection(req, res, () => authorize(req, res, next)) } else return res.sendStatus(401) } } @@ -34,7 +35,7 @@ export const authenticateAccessToken: RequestHandler = async ( authenticateToken( req, res, - next, + () => authorize(req, res, next), process.env.ACCESS_TOKEN_SECRET as string, 'accessToken' ) diff --git a/api/src/middlewares/authorize.ts b/api/src/middlewares/authorize.ts new file mode 100644 index 0000000..004b7f7 --- /dev/null +++ b/api/src/middlewares/authorize.ts @@ -0,0 +1,34 @@ +import { RequestHandler } from 'express' +import User from '../model/User' +import Permission from '../model/Permission' +import { PermissionSetting } from '../controllers/permission' + +export const authorize: RequestHandler = async (req, res, next) => { + let permission + const user = req.user + if (user) { + // no need to check for permissions when user is admin + if (user.isAdmin) return next() + + const dbUser = await User.findOne({ id: user.userId }) + if (!dbUser) return res.sendStatus(401) + + const uri = req.baseUrl + req.path + + // find permission w.r.t user + permission = await Permission.findOne({ uri, user: dbUser._id }) + + if (permission && permission.setting === PermissionSetting.grant) + return next() + + // find permission w.r.t user's groups + for (const group of dbUser.groups) { + permission = await Permission.findOne({ uri, group }) + if (permission && permission.setting === PermissionSetting.grant) + return next() + } + + return res.sendStatus(401) + } + return res.sendStatus(401) +} diff --git a/api/src/middlewares/index.ts b/api/src/middlewares/index.ts index 7798de3..8e64643 100644 --- a/api/src/middlewares/index.ts +++ b/api/src/middlewares/index.ts @@ -2,3 +2,4 @@ export * from './authenticateToken' export * from './desktop' export * from './verifyAdmin' export * from './verifyAdminIfNeeded' +export * from './authorize' From f3dfc7083fbfb4b447521341b1a86730fb90b4c0 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 1 Jul 2022 16:50:24 +0500 Subject: [PATCH 42/54] fix: add permission authorization middleware to only specific routes --- api/src/middlewares/authenticateToken.ts | 5 +-- api/src/middlewares/authorize.ts | 8 ++-- api/src/routes/api/code.ts | 3 +- api/src/routes/api/drive.ts | 14 ++++--- api/src/routes/api/permission.ts | 29 +++++++++----- api/src/routes/api/spec/drive.spec.ts | 45 +++++++++++++++++++--- api/src/routes/api/spec/permission.spec.ts | 12 +++++- api/src/routes/api/spec/stp.spec.ts | 18 ++++++++- api/src/routes/api/stp.ts | 4 +- 9 files changed, 106 insertions(+), 32 deletions(-) diff --git a/api/src/middlewares/authenticateToken.ts b/api/src/middlewares/authenticateToken.ts index 23d5d0a..90c7027 100644 --- a/api/src/middlewares/authenticateToken.ts +++ b/api/src/middlewares/authenticateToken.ts @@ -3,7 +3,6 @@ import jwt from 'jsonwebtoken' import { csrfProtection } from '../app' import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils' import { desktopUser } from './desktop' -import { authorize } from './authorize' export const authenticateAccessToken: RequestHandler = async ( req, @@ -25,7 +24,7 @@ export const authenticateAccessToken: RequestHandler = async ( if (user) { if (user.isActive) { req.user = user - return csrfProtection(req, res, () => authorize(req, res, next)) + return csrfProtection(req, res, next) } else return res.sendStatus(401) } } @@ -35,7 +34,7 @@ export const authenticateAccessToken: RequestHandler = async ( authenticateToken( req, res, - () => authorize(req, res, next), + next, process.env.ACCESS_TOKEN_SECRET as string, 'accessToken' ) diff --git a/api/src/middlewares/authorize.ts b/api/src/middlewares/authorize.ts index 004b7f7..dc336d4 100644 --- a/api/src/middlewares/authorize.ts +++ b/api/src/middlewares/authorize.ts @@ -13,13 +13,15 @@ export const authorize: RequestHandler = async (req, res, next) => { const dbUser = await User.findOne({ id: user.userId }) if (!dbUser) return res.sendStatus(401) - const uri = req.baseUrl + req.path + const uri = req.baseUrl + req.route.path // find permission w.r.t user permission = await Permission.findOne({ uri, user: dbUser._id }) - if (permission && permission.setting === PermissionSetting.grant) - return next() + if (permission) { + if (permission.setting === PermissionSetting.grant) return next() + else res.sendStatus(401) + } // find permission w.r.t user's groups for (const group of dbUser.groups) { diff --git a/api/src/routes/api/code.ts b/api/src/routes/api/code.ts index 09171c0..4c7fb57 100644 --- a/api/src/routes/api/code.ts +++ b/api/src/routes/api/code.ts @@ -1,12 +1,13 @@ import express from 'express' import { runCodeValidation } from '../../utils' import { CodeController } from '../../controllers/' +import { authorize } from '../../middlewares' const runRouter = express.Router() const controller = new CodeController() -runRouter.post('/execute', async (req, res) => { +runRouter.post('/execute', authorize, async (req, res) => { const { error, value: body } = runCodeValidation(req.body) if (error) return res.status(400).send(error.details[0].message) diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts index 6126946..6b9115e 100644 --- a/api/src/routes/api/drive.ts +++ b/api/src/routes/api/drive.ts @@ -3,6 +3,7 @@ import { deleteFile, readFile } from '@sasjs/utils' import { publishAppStream } from '../appStream' +import { authorize } from '../../middlewares' import { multerSingle } from '../../middlewares/multer' import { DriveController } from '../../controllers/' import { @@ -19,7 +20,7 @@ const controller = new DriveController() const driveRouter = express.Router() -driveRouter.post('/deploy', async (req, res) => { +driveRouter.post('/deploy', authorize, async (req, res) => { const { error, value: body } = deployValidation(req.body) if (error) return res.status(400).send(error.details[0].message) @@ -48,6 +49,7 @@ driveRouter.post('/deploy', async (req, res) => { driveRouter.post( '/deploy/upload', + authorize, (...arg) => multerSingle('file', arg), async (req, res) => { if (!req.file) return res.status(400).send('"file" is not present.') @@ -111,7 +113,7 @@ driveRouter.post( } ) -driveRouter.get('/file', async (req, res) => { +driveRouter.get('/file', authorize, async (req, res) => { const { error: errQ, value: query } = fileParamValidation(req.query) if (errQ) return res.status(400).send(errQ.details[0].message) @@ -123,7 +125,7 @@ driveRouter.get('/file', async (req, res) => { } }) -driveRouter.get('/folder', async (req, res) => { +driveRouter.get('/folder', authorize, async (req, res) => { const { error: errQ, value: query } = folderParamValidation(req.query) if (errQ) return res.status(400).send(errQ.details[0].message) @@ -136,7 +138,7 @@ driveRouter.get('/folder', async (req, res) => { } }) -driveRouter.delete('/file', async (req, res) => { +driveRouter.delete('/file', authorize, async (req, res) => { const { error: errQ, value: query } = fileParamValidation(req.query) if (errQ) return res.status(400).send(errQ.details[0].message) @@ -151,6 +153,7 @@ driveRouter.delete('/file', async (req, res) => { driveRouter.post( '/file', + authorize, (...arg) => multerSingle('file', arg), async (req, res) => { const { error: errQ, value: query } = fileParamValidation(req.query) @@ -179,6 +182,7 @@ driveRouter.post( driveRouter.patch( '/file', + authorize, (...arg) => multerSingle('file', arg), async (req, res) => { const { error: errQ, value: query } = fileParamValidation(req.query) @@ -205,7 +209,7 @@ driveRouter.patch( } ) -driveRouter.get('/fileTree', async (req, res) => { +driveRouter.get('/fileTree', authorize, async (req, res) => { try { const response = await controller.getFileTree() res.send(response) diff --git a/api/src/routes/api/permission.ts b/api/src/routes/api/permission.ts index 0dd98b0..d354744 100644 --- a/api/src/routes/api/permission.ts +++ b/api/src/routes/api/permission.ts @@ -1,6 +1,10 @@ import express from 'express' import { PermissionController } from '../../controllers/' -import { authenticateAccessToken, verifyAdmin } from '../../middlewares' +import { + authenticateAccessToken, + verifyAdmin, + authorize +} from '../../middlewares' import { registerPermissionValidation, updatePermissionValidation @@ -9,16 +13,21 @@ import { const permissionRouter = express.Router() const controller = new PermissionController() -permissionRouter.get('/', authenticateAccessToken, async (req, res) => { - try { - const response = await controller.getAllPermissions() - res.send(response) - } catch (err: any) { - const statusCode = err.code - delete err.code - res.status(statusCode).send(err.message) +permissionRouter.get( + '/', + authenticateAccessToken, + authorize, + async (req, res) => { + try { + const response = await controller.getAllPermissions() + res.send(response) + } catch (err: any) { + const statusCode = err.code + delete err.code + res.status(statusCode).send(err.message) + } } -}) +) permissionRouter.post( '/', diff --git a/api/src/routes/api/spec/drive.spec.ts b/api/src/routes/api/spec/drive.spec.ts index e9e6e8a..d9475c4 100644 --- a/api/src/routes/api/spec/drive.spec.ts +++ b/api/src/routes/api/spec/drive.spec.ts @@ -29,7 +29,12 @@ jest .mockImplementation(() => path.join(tmpFolder, 'uploads')) import appPromise from '../../../app' -import { UserController } from '../../../controllers/' +import { + UserController, + PermissionController, + PermissionSetting, + PrincipalType +} from '../../../controllers/' import { getTreeExample } from '../../../controllers/internal' import { generateAccessToken, saveTokensInDB } from '../../../utils/' const { getFilesFolder } = fileUtilModules @@ -48,6 +53,7 @@ describe('drive', () => { let con: Mongoose let mongoServer: MongoMemoryServer const controller = new UserController() + const permissionController = new PermissionController() let accessToken: string @@ -58,11 +64,31 @@ describe('drive', () => { con = await mongoose.connect(mongoServer.getUri()) const dbUser = await controller.createUser(user) - accessToken = generateAccessToken({ - clientId, - userId: dbUser.id + accessToken = await generateAndSaveToken(dbUser.id) + permissionController.createPermission({ + uri: '/SASjsApi/drive/deploy', + principalType: PrincipalType.user, + principalId: dbUser.id, + setting: PermissionSetting.grant + }) + permissionController.createPermission({ + uri: '/SASjsApi/drive/deploy/upload', + principalType: PrincipalType.user, + principalId: dbUser.id, + setting: PermissionSetting.grant + }) + permissionController.createPermission({ + uri: '/SASjsApi/drive/file', + principalType: PrincipalType.user, + principalId: dbUser.id, + setting: PermissionSetting.grant + }) + permissionController.createPermission({ + uri: '/SASjsApi/drive/folder', + principalType: PrincipalType.user, + principalId: dbUser.id, + setting: PermissionSetting.grant }) - await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken') }) afterAll(async () => { @@ -945,3 +971,12 @@ describe('drive', () => { const getExampleService = (): ServiceMember => ((getTreeExample().members[0] as FolderMember).members[0] as FolderMember) .members[0] as ServiceMember + +const generateAndSaveToken = async (userId: number) => { + const adminAccessToken = generateAccessToken({ + clientId, + userId + }) + await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken') + return adminAccessToken +} diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index 23c1fbc..ecd3587 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -440,17 +440,25 @@ describe('permission', () => { }) it('should give a list of all permissions when user is not admin', async () => { - const accessToken = await generateSaveTokenAndCreateUser({ + const dbUser = await userController.createUser({ ...user, username: 'get' + user.username }) + 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) .get('/SASjsApi/permission/') .auth(accessToken, { type: 'bearer' }) .send() .expect(200) - expect(res.body).toHaveLength(2) + expect(res.body).toHaveLength(3) }) }) }) diff --git a/api/src/routes/api/spec/stp.spec.ts b/api/src/routes/api/spec/stp.spec.ts index f80ab8b..eb34570 100644 --- a/api/src/routes/api/spec/stp.spec.ts +++ b/api/src/routes/api/spec/stp.spec.ts @@ -4,7 +4,12 @@ import mongoose, { Mongoose } from 'mongoose' import { MongoMemoryServer } from 'mongodb-memory-server' import request from 'supertest' import appPromise from '../../../app' -import { UserController } from '../../../controllers/' +import { + UserController, + PermissionController, + PermissionSetting, + PrincipalType +} from '../../../controllers/' import { generateAccessToken, saveTokensInDB, @@ -41,12 +46,21 @@ describe('stp', () => { let con: Mongoose let mongoServer: MongoMemoryServer let accessToken: string + const userController = new UserController() + const permissionController = new PermissionController() beforeAll(async () => { app = await appPromise mongoServer = await MongoMemoryServer.create() con = await mongoose.connect(mongoServer.getUri()) - accessToken = await generateSaveTokenAndCreateUser(user) + const dbUser = await userController.createUser(user) + accessToken = await generateAndSaveToken(dbUser.id) + await permissionController.createPermission({ + uri: '/SASjsApi/stp/execute', + principalType: PrincipalType.user, + principalId: dbUser.id, + setting: PermissionSetting.grant + }) }) afterAll(async () => { diff --git a/api/src/routes/api/stp.ts b/api/src/routes/api/stp.ts index 858feb5..14a3dea 100644 --- a/api/src/routes/api/stp.ts +++ b/api/src/routes/api/stp.ts @@ -2,13 +2,14 @@ import express from 'express' import { executeProgramRawValidation } from '../../utils' import { STPController } from '../../controllers/' import { FileUploadController } from '../../controllers/internal' +import { authorize } from '../../middlewares' const stpRouter = express.Router() const fileUploadController = new FileUploadController() const controller = new STPController() -stpRouter.get('/execute', async (req, res) => { +stpRouter.get('/execute', authorize, async (req, res) => { const { error, value: query } = executeProgramRawValidation(req.query) if (error) return res.status(400).send(error.details[0].message) @@ -32,6 +33,7 @@ stpRouter.get('/execute', async (req, res) => { stpRouter.post( '/execute', + authorize, fileUploadController.preUploadMiddleware, fileUploadController.getMulterUploadObject().any(), async (req, res: any) => { From e516b7716da5ff7e23350a5f77cfa073b1171175 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Sat, 2 Jul 2022 01:03:53 +0500 Subject: [PATCH 43/54] fix: update permission response --- api/public/swagger.yaml | 4 +- api/src/controllers/group.ts | 2 +- api/src/controllers/permission.ts | 25 +++++-- .../Settings/deletePermissionModal.tsx | 2 +- web/src/containers/Settings/index.tsx | 2 +- web/src/containers/Settings/permission.tsx | 67 ++++++++++++++++++- web/src/utils/types.ts | 7 +- 7 files changed, 94 insertions(+), 15 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index e4b5a58..4784099 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -171,7 +171,7 @@ components: user: $ref: '#/components/schemas/UserResponse' group: - $ref: '#/components/schemas/GroupResponse' + $ref: '#/components/schemas/GroupDetailsResponse' required: - permissionId - uri @@ -1008,7 +1008,7 @@ paths: 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'}}] + 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 diff --git a/api/src/controllers/group.ts b/api/src/controllers/group.ts index 1d324ef..0c37de9 100644 --- a/api/src/controllers/group.ts +++ b/api/src/controllers/group.ts @@ -20,7 +20,7 @@ export interface GroupResponse { description: string } -interface GroupDetailsResponse { +export interface GroupDetailsResponse { groupId: number name: string description: string diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index 8b48612..3dabaa7 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -15,7 +15,7 @@ import Permission from '../model/Permission' import User from '../model/User' import Group from '../model/Group' import { UserResponse } from './user' -import { GroupResponse } from './group' +import { GroupDetailsResponse } from './group' export enum PrincipalType { user = 'user', @@ -63,7 +63,7 @@ export interface PermissionDetailsResponse { uri: string setting: string user?: UserResponse - group?: GroupResponse + group?: GroupDetailsResponse } @Security('bearerAuth') @@ -93,7 +93,9 @@ export class PermissionController { group: { groupId: 1, name: 'DCGroup', - description: 'This group represents Data Controller Users' + description: 'This group represents Data Controller Users', + isActive: true, + users: [] } } ]) @@ -170,7 +172,12 @@ const getAllPermissions = async (): Promise => .populate({ path: 'user', select: 'id username displayName isAdmin -_id' }) .populate({ path: 'group', - select: 'groupId name description -_id' + select: 'groupId name description -_id', + populate: { + path: 'users', + select: 'id username displayName isAdmin -_id', + options: { limit: 15 } + } })) as unknown as PermissionDetailsResponse[] const createPermission = async ({ @@ -185,7 +192,7 @@ const createPermission = async ({ }) let user: UserResponse | undefined - let group: GroupResponse | undefined + let group: GroupDetailsResponse | undefined switch (principalType) { case PrincipalType.user: { @@ -251,7 +258,13 @@ const createPermission = async ({ group = { groupId: groupInDB.groupId, name: groupInDB.name, - description: groupInDB.description + description: groupInDB.description, + isActive: groupInDB.isActive, + users: groupInDB.populate({ + path: 'users', + select: 'id username displayName isAdmin -_id', + options: { limit: 15 } + }) as unknown as UserResponse[] } break } diff --git a/web/src/containers/Settings/deletePermissionModal.tsx b/web/src/containers/Settings/deletePermissionModal.tsx index 59527bd..23736f8 100644 --- a/web/src/containers/Settings/deletePermissionModal.tsx +++ b/web/src/containers/Settings/deletePermissionModal.tsx @@ -29,7 +29,7 @@ const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => { setOpen(false)} open={open}> - Are you sure to delete this permission? + Are you sure you want to delete this permission? diff --git a/web/src/containers/Settings/index.tsx b/web/src/containers/Settings/index.tsx index a93d400..71a8960 100644 --- a/web/src/containers/Settings/index.tsx +++ b/web/src/containers/Settings/index.tsx @@ -47,7 +47,7 @@ const Settings = () => { > {appContext.mode === ModeType.Server && ( - + )} diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index d4652b4..9f69793 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -12,7 +12,9 @@ import { Grid, CircularProgress, IconButton, - Tooltip + Tooltip, + Typography, + Popover } from '@mui/material' import FilterListIcon from '@mui/icons-material/FilterList' @@ -29,6 +31,7 @@ import UpdatePermissionModal from './updatePermissionModal' import DeleteModal from './deletePermissionModal' import { + GroupDetailsResponse, PermissionResponse, RegisterPermissionPayload } from '../../utils/types' @@ -401,8 +404,66 @@ const PermissionTable = ({ } const displayPrincipal = (permission: PermissionResponse) => { - if (permission.user) return permission.user?.username - if (permission.group) return permission.group?.name + if (permission.user) return permission.user.username + if (permission.group) return +} + +type DisplayGroupProps = { + group: GroupDetailsResponse +} + +const DisplayGroup = ({ group }: DisplayGroupProps) => { + const [anchorEl, setAnchorEl] = useState(null) + + const handlePopoverOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handlePopoverClose = () => { + setAnchorEl(null) + } + + const open = Boolean(anchorEl) + + return ( +
    + + {group.name} + + + + Group Users + + {group.users.map((user) => ( + + {user.username} + + ))} + +
    + ) } const displayPrincipalType = (permission: PermissionResponse) => { diff --git a/web/src/utils/types.ts b/web/src/utils/types.ts index 46fd226..4f0a80a 100644 --- a/web/src/utils/types.ts +++ b/web/src/utils/types.ts @@ -11,12 +11,17 @@ export interface GroupResponse { description: string } +export interface GroupDetailsResponse extends GroupResponse { + isActive: boolean + users: UserResponse[] +} + export interface PermissionResponse { permissionId: number uri: string setting: string user?: UserResponse - group?: GroupResponse + group?: GroupDetailsResponse } export interface RegisterPermissionPayload { From b5f595a25c50550d62482409353c7629c5a5c3e0 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 4 Jul 2022 04:27:58 +0500 Subject: [PATCH 44/54] fix: controller fixed for deleting permission --- api/src/controllers/permission.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/controllers/permission.ts b/api/src/controllers/permission.ts index 3dabaa7..14f4c18 100644 --- a/api/src/controllers/permission.ts +++ b/api/src/controllers/permission.ts @@ -320,12 +320,12 @@ const updatePermission = async ( } const deletePermission = async (id: number) => { - const permission = await Permission.findOne({ id }) + const permission = await Permission.findOne({ permissionId: id }) if (!permission) throw { code: 404, status: 'Not Found', message: 'Permission not found.' } - await Permission.deleteOne({ id }) + await Permission.deleteOne({ permissionId: id }) } From 4c35e0480230aaf8288f14badc3763554f66b45e Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 4 Jul 2022 16:00:23 +0500 Subject: [PATCH 45/54] chore: add snackbar for showing success alert --- web/src/components/snackbar.tsx | 62 ++++++++++++++++++++++ web/src/containers/Settings/permission.tsx | 32 +++++++---- 2 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 web/src/components/snackbar.tsx diff --git a/web/src/components/snackbar.tsx b/web/src/components/snackbar.tsx new file mode 100644 index 0000000..ed4328c --- /dev/null +++ b/web/src/components/snackbar.tsx @@ -0,0 +1,62 @@ +import React, { Dispatch, SetStateAction } from 'react' +import Snackbar from '@mui/material/Snackbar' +import MuiAlert, { AlertProps } from '@mui/material/Alert' +import Slide, { SlideProps } from '@mui/material/Slide' + +const Alert = React.forwardRef(function Alert( + props, + ref +) { + return +}) + +const Transition = (props: SlideProps) => { + return +} + +export enum AlertSeverityType { + Success = 'success', + Warning = 'warning', + Info = 'info', + Error = 'error' +} + +type BootstrapSnackbarProps = { + open: boolean + setOpen: Dispatch> + message: string + severity: AlertSeverityType +} + +const BootstrapSnackbar = ({ + open, + setOpen, + message, + severity +}: BootstrapSnackbarProps) => { + const handleClose = ( + event: React.SyntheticEvent | Event, + reason?: string + ) => { + if (reason === 'clickaway') { + return + } + + setOpen(false) + } + + return ( + + + {message} + + + ) +} + +export default BootstrapSnackbar diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index 9f69793..86f5fc6 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -29,6 +29,7 @@ import PermissionFilterModal from './permissionFilterModal' import AddPermissionModal from './addPermissionModal' import UpdatePermissionModal from './updatePermissionModal' import DeleteModal from './deletePermissionModal' +import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar' import { GroupDetailsResponse, @@ -52,6 +53,11 @@ const Permission = () => { const [openModal, setOpenModal] = useState(false) const [modalTitle, setModalTitle] = useState('') const [modalPayload, setModalPayload] = useState('') + const [openSnackbar, setOpenSnackbar] = useState(false) + const [snackbarMessage, setSnackbarMessage] = useState('') + const [snackbarSeverity, setSnackbarSeverity] = useState( + AlertSeverityType.Success + ) const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false) const [updatePermissionModalOpen, setUpdatePermissionModalOpen] = useState(false) @@ -177,9 +183,9 @@ const Permission = () => { .post('/SASjsApi/permission', addPermissionPayload) .then((res: any) => { fetchPermissions() - setModalTitle('Success') - setModalPayload('Permission added Successfully.') - setOpenModal(true) + setSnackbarMessage('Permission added!') + setSnackbarSeverity(AlertSeverityType.Success) + setOpenSnackbar(true) }) .catch((err) => { setModalTitle('Abort') @@ -209,9 +215,9 @@ const Permission = () => { }) .then((res: any) => { fetchPermissions() - setModalTitle('Success') - setModalPayload('Permission updated Successfully.') - setOpenModal(true) + setSnackbarMessage('Permission updated!') + setSnackbarSeverity(AlertSeverityType.Success) + setOpenSnackbar(true) }) .catch((err) => { setModalTitle('Abort') @@ -240,9 +246,9 @@ const Permission = () => { .delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`) .then((res: any) => { fetchPermissions() - setModalTitle('Success') - setModalPayload('Permission deleted Successfully.') - setOpenModal(true) + setSnackbarMessage('Permission deleted!') + setSnackbarSeverity(AlertSeverityType.Success) + setOpenSnackbar(true) }) .catch((err) => { setModalTitle('Abort') @@ -294,6 +300,12 @@ const Permission = () => { />
    + { disableRestoreFocus > - Group Users + Group Members {group.users.map((user) => ( From e54a09db19ec8690e54a40760531a4e06d250974 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 4 Jul 2022 17:14:17 +0500 Subject: [PATCH 46/54] fix: add authorize middleware for appStreams --- api/src/middlewares/authorize.ts | 4 ++-- api/src/routes/appStream/index.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/src/middlewares/authorize.ts b/api/src/middlewares/authorize.ts index dc336d4..124b332 100644 --- a/api/src/middlewares/authorize.ts +++ b/api/src/middlewares/authorize.ts @@ -5,7 +5,7 @@ import { PermissionSetting } from '../controllers/permission' export const authorize: RequestHandler = async (req, res, next) => { let permission - const user = req.user + const user = req.user || req.session.user if (user) { // no need to check for permissions when user is admin if (user.isAdmin) return next() @@ -13,7 +13,7 @@ export const authorize: RequestHandler = async (req, res, next) => { const dbUser = await User.findOne({ id: user.userId }) if (!dbUser) return res.sendStatus(401) - const uri = req.baseUrl + req.route.path + const uri = req.baseUrl + req.path // find permission w.r.t user permission = await Permission.findOne({ uri, user: dbUser._id }) diff --git a/api/src/routes/appStream/index.ts b/api/src/routes/appStream/index.ts index 3954039..1152644 100644 --- a/api/src/routes/appStream/index.ts +++ b/api/src/routes/appStream/index.ts @@ -1,5 +1,6 @@ import path from 'path' import express, { Request } from 'express' +import { authorize } from '../../middlewares/authorize' import { folderExists } from '@sasjs/utils' import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils' @@ -9,7 +10,7 @@ const appStreams: { [key: string]: string } = {} const router = express.Router() -router.get('/', async (req, res) => { +router.get('/', authorize, async (req, res) => { const content = appStreamHtml(process.appStreamConfig) res.cookie('XSRF-TOKEN', req.csrfToken()) @@ -66,7 +67,7 @@ export const publishAppStream = async ( return {} } -router.get(`/*`, function (req: Request, res, next) { +router.get(`/*`, authorize, function (req: Request, res, next) { const reqPath = req.path.replace(/^\//, '') // Redirecting to url with trailing slash for appStream base URL only From b10e9326058193dd65a57fab2d2f05b7b06096e7 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 4 Jul 2022 19:14:06 +0500 Subject: [PATCH 47/54] feat: added get authorizedRoutes api endpoint --- api/public/swagger.yaml | 28 ++++++++ api/src/controllers/info.ts | 19 +++++ api/src/routes/api/info.ts | 10 +++ api/src/utils/getAuthorizedRoutes.ts | 17 +++++ api/src/utils/index.ts | 1 + api/src/utils/validation.ts | 5 +- .../Settings/addPermissionModal.tsx | 70 ++++++++----------- web/src/containers/Settings/permission.tsx | 1 - 8 files changed, 109 insertions(+), 42 deletions(-) create mode 100644 api/src/utils/getAuthorizedRoutes.ts diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 4784099..a92bd29 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -548,6 +548,16 @@ components: - runTimes type: object additionalProperties: false + AuthorizedRoutesResponse: + properties: + routes: + items: + type: string + type: array + required: + - routes + type: object + additionalProperties: false ExecuteReturnJsonPayload: properties: _program: @@ -1593,6 +1603,24 @@ paths: - Info security: [] parameters: [] + /SASjsApi/info/authorizedRoutes: + get: + operationId: AuthorizedRoutes + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizedRoutesResponse' + examples: + 'Example 1': + value: {routes: [/AppStream, /SASjsApi/stp/execute]} + summary: 'Get authorized routes.' + tags: + - Info + security: [] + parameters: [] /SASjsApi/session: get: operationId: Session diff --git a/api/src/controllers/info.ts b/api/src/controllers/info.ts index 5c4cf86..08cd1d5 100644 --- a/api/src/controllers/info.ts +++ b/api/src/controllers/info.ts @@ -1,4 +1,8 @@ import { Route, Tags, Example, Get } from 'tsoa' +import { getAuthorizedRoutes } from '../utils' +export interface AuthorizedRoutesResponse { + URIs: string[] +} export interface InfoResponse { mode: string @@ -36,4 +40,19 @@ export class InfoController { } return response } + + /** + * @summary Get authorized routes. + * + */ + @Example({ + URIs: ['/AppStream', '/SASjsApi/stp/execute'] + }) + @Get('/authorizedRoutes') + public authorizedRoutes(): AuthorizedRoutesResponse { + const response = { + URIs: getAuthorizedRoutes() + } + return response + } } diff --git a/api/src/routes/api/info.ts b/api/src/routes/api/info.ts index fb71f66..0b5a587 100644 --- a/api/src/routes/api/info.ts +++ b/api/src/routes/api/info.ts @@ -13,4 +13,14 @@ infoRouter.get('/', async (req, res) => { } }) +infoRouter.get('/authorizedRoutes', async (req, res) => { + const controller = new InfoController() + try { + const response = controller.authorizedRoutes() + res.send(response) + } catch (err: any) { + res.status(403).send(err.toString()) + } +}) + export default infoRouter diff --git a/api/src/utils/getAuthorizedRoutes.ts b/api/src/utils/getAuthorizedRoutes.ts new file mode 100644 index 0000000..7d63b9f --- /dev/null +++ b/api/src/utils/getAuthorizedRoutes.ts @@ -0,0 +1,17 @@ +export const getAuthorizedRoutes = () => { + const streamingApps = Object.keys(process.appStreamConfig) + const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`) + return [...StaticAuthorizedRoutes, ...streamingAppsRoutes] +} + +const StaticAuthorizedRoutes = [ + '/AppStream', + '/SASjsApi/code/execute', + '/SASjsApi/stp/execute', + '/SASjsApi/drive/deploy', + '/SASjsApi/drive/upload', + '/SASjsApi/drive/file', + '/SASjsApi/drive/folder', + '/SASjsApi/drive/fileTree', + '/SASjsApi/permission' +] diff --git a/api/src/utils/index.ts b/api/src/utils/index.ts index 36e1efc..13bc904 100644 --- a/api/src/utils/index.ts +++ b/api/src/utils/index.ts @@ -8,6 +8,7 @@ export * from './file' export * from './generateAccessToken' export * from './generateAuthCode' export * from './generateRefreshToken' +export * from './getAuthorizedRoutes' export * from './getCertificates' export * from './getDesktopFields' export * from './getPreProgramVariables' diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index cd91e80..0789fa5 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -1,5 +1,6 @@ import Joi from 'joi' import { PermissionSetting, PrincipalType } from '../controllers/permission' +import { getAuthorizedRoutes } from './getAuthorizedRoutes' const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16) const passwordSchema = Joi.string().min(6).max(1024) @@ -88,7 +89,9 @@ export const registerClientValidation = (data: any): Joi.ValidationResult => export const registerPermissionValidation = (data: any): Joi.ValidationResult => Joi.object({ - uri: Joi.string().required(), + uri: Joi.string() + .required() + .valid(...getAuthorizedRoutes()), setting: Joi.string() .required() .valid(...Object.values(PermissionSetting)), diff --git a/web/src/containers/Settings/addPermissionModal.tsx b/web/src/containers/Settings/addPermissionModal.tsx index 1395a85..2a147dc 100644 --- a/web/src/containers/Settings/addPermissionModal.tsx +++ b/web/src/containers/Settings/addPermissionModal.tsx @@ -1,10 +1,4 @@ -import React, { - useState, - useEffect, - useMemo, - Dispatch, - SetStateAction -} from 'react' +import React, { useState, useEffect, Dispatch, SetStateAction } from 'react' import axios from 'axios' import { Button, @@ -13,15 +7,14 @@ import { DialogContent, DialogActions, TextField, - CircularProgress + CircularProgress, + Autocomplete } from '@mui/material' import { styled } from '@mui/material/styles' -import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete' import { BootstrapDialogTitle } from '../../components/dialogTitle' import { - PermissionResponse, UserResponse, GroupResponse, RegisterPermissionPayload @@ -39,18 +32,16 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({ type AddPermissionModalProps = { open: boolean handleOpen: Dispatch> - permissions: PermissionResponse[] addPermission: (addPermissionPayload: RegisterPermissionPayload) => void } -const filter = createFilterOptions() - const AddPermissionModal = ({ open, handleOpen, - permissions, addPermission }: AddPermissionModalProps) => { + const [URIs, setURIs] = useState([]) + const [loadingURIs, setLoadingURIs] = useState(false) const [uri, setUri] = useState() const [principalType, setPrincipalType] = useState('user') const [userPrincipal, setUserPrincipal] = useState() @@ -60,6 +51,23 @@ const AddPermissionModal = ({ const [userPrincipals, setUserPrincipals] = useState([]) const [groupPrincipals, setGroupPrincipals] = useState([]) + useEffect(() => { + setLoadingURIs(true) + axios + .get('/SASjsApi/info/authorizedRoutes') + .then((res: any) => { + if (res.data) { + setURIs(res.data.URIs) + } + }) + .catch((err) => { + console.log(err) + }) + .finally(() => { + setLoadingURIs(false) + }) + }, []) + useEffect(() => { setLoadingPrincipals(true) axios @@ -97,12 +105,6 @@ const AddPermissionModal = ({ addPermission(addPermissionPayload) } - const URIs = useMemo(() => { - return permissions - .map((permission) => permission.uri) - .filter((uri, index, array) => array.indexOf(uri) === index) - }, [permissions]) - const addButtonDisabled = !uri || (principalType === 'user' ? !userPrincipal : !groupPrincipal) @@ -118,29 +120,17 @@ const AddPermissionModal = ({ setUri(newValue)} - filterOptions={(options, params) => { - const filtered = filter(options, params) - - const { inputValue } = params - - const isExisting = options.some( - (option) => inputValue === option + onChange={(event: any, newValue: string) => setUri(newValue)} + renderInput={(params) => + loadingURIs ? ( + + ) : ( + ) - if (inputValue !== '' && !isExisting) { - filtered.push(inputValue) - } - return filtered - }} - selectOnFocus - clearOnBlur - handleHomeEndKeys - options={URIs} - renderOption={(props, option) =>
  • {option}
  • } - freeSolo - renderInput={(params) => } + } />
    diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index 86f5fc6..81b2180 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -330,7 +330,6 @@ const Permission = () => { Date: Mon, 4 Jul 2022 20:13:46 +0500 Subject: [PATCH 48/54] chore: conditionally call authorize middleware from authenticateToken --- api/public/swagger.yaml | 6 +- api/src/middlewares/authenticateToken.ts | 18 ++++- api/src/routes/api/code.ts | 3 +- api/src/routes/api/drive.ts | 14 ++-- api/src/routes/api/index.ts | 7 +- api/src/routes/api/permission.ts | 86 +++++++++------------- api/src/routes/api/spec/permission.spec.ts | 13 ++++ api/src/routes/api/stp.ts | 4 +- api/src/utils/appStreamConfig.ts | 3 +- 9 files changed, 79 insertions(+), 75 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index a92bd29..afa176f 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -550,12 +550,12 @@ components: additionalProperties: false AuthorizedRoutesResponse: properties: - routes: + URIs: items: type: string type: array required: - - routes + - URIs type: object additionalProperties: false ExecuteReturnJsonPayload: @@ -1615,7 +1615,7 @@ paths: $ref: '#/components/schemas/AuthorizedRoutesResponse' examples: 'Example 1': - value: {routes: [/AppStream, /SASjsApi/stp/execute]} + value: {URIs: [/AppStream, /SASjsApi/stp/execute]} summary: 'Get authorized routes.' tags: - Info diff --git a/api/src/middlewares/authenticateToken.ts b/api/src/middlewares/authenticateToken.ts index 90c7027..be39165 100644 --- a/api/src/middlewares/authenticateToken.ts +++ b/api/src/middlewares/authenticateToken.ts @@ -1,8 +1,14 @@ import { RequestHandler, Request, Response, NextFunction } from 'express' import jwt from 'jsonwebtoken' import { csrfProtection } from '../app' -import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils' +import { + fetchLatestAutoExec, + ModeType, + verifyTokenInDB, + getAuthorizedRoutes +} from '../utils' import { desktopUser } from './desktop' +import { authorize } from './authorize' export const authenticateAccessToken: RequestHandler = async ( req, @@ -15,6 +21,12 @@ export const authenticateAccessToken: RequestHandler = async ( return next() } + const authorizedRoutes = getAuthorizedRoutes() + const uri = req.baseUrl + req.path + const nextFunction = authorizedRoutes.includes(uri) + ? () => authorize(req, res, next) + : next + // if request is coming from web and has valid session // it can be validated. if (req.session?.loggedIn) { @@ -24,7 +36,7 @@ export const authenticateAccessToken: RequestHandler = async ( if (user) { if (user.isActive) { req.user = user - return csrfProtection(req, res, next) + return csrfProtection(req, res, nextFunction) } else return res.sendStatus(401) } } @@ -34,7 +46,7 @@ export const authenticateAccessToken: RequestHandler = async ( authenticateToken( req, res, - next, + nextFunction, process.env.ACCESS_TOKEN_SECRET as string, 'accessToken' ) diff --git a/api/src/routes/api/code.ts b/api/src/routes/api/code.ts index 4c7fb57..09171c0 100644 --- a/api/src/routes/api/code.ts +++ b/api/src/routes/api/code.ts @@ -1,13 +1,12 @@ import express from 'express' import { runCodeValidation } from '../../utils' import { CodeController } from '../../controllers/' -import { authorize } from '../../middlewares' const runRouter = express.Router() const controller = new CodeController() -runRouter.post('/execute', authorize, async (req, res) => { +runRouter.post('/execute', async (req, res) => { const { error, value: body } = runCodeValidation(req.body) if (error) return res.status(400).send(error.details[0].message) diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts index 6b9115e..6126946 100644 --- a/api/src/routes/api/drive.ts +++ b/api/src/routes/api/drive.ts @@ -3,7 +3,6 @@ import { deleteFile, readFile } from '@sasjs/utils' import { publishAppStream } from '../appStream' -import { authorize } from '../../middlewares' import { multerSingle } from '../../middlewares/multer' import { DriveController } from '../../controllers/' import { @@ -20,7 +19,7 @@ const controller = new DriveController() const driveRouter = express.Router() -driveRouter.post('/deploy', authorize, async (req, res) => { +driveRouter.post('/deploy', async (req, res) => { const { error, value: body } = deployValidation(req.body) if (error) return res.status(400).send(error.details[0].message) @@ -49,7 +48,6 @@ driveRouter.post('/deploy', authorize, async (req, res) => { driveRouter.post( '/deploy/upload', - authorize, (...arg) => multerSingle('file', arg), async (req, res) => { if (!req.file) return res.status(400).send('"file" is not present.') @@ -113,7 +111,7 @@ driveRouter.post( } ) -driveRouter.get('/file', authorize, async (req, res) => { +driveRouter.get('/file', async (req, res) => { const { error: errQ, value: query } = fileParamValidation(req.query) if (errQ) return res.status(400).send(errQ.details[0].message) @@ -125,7 +123,7 @@ driveRouter.get('/file', authorize, async (req, res) => { } }) -driveRouter.get('/folder', authorize, async (req, res) => { +driveRouter.get('/folder', async (req, res) => { const { error: errQ, value: query } = folderParamValidation(req.query) if (errQ) return res.status(400).send(errQ.details[0].message) @@ -138,7 +136,7 @@ driveRouter.get('/folder', authorize, async (req, res) => { } }) -driveRouter.delete('/file', authorize, async (req, res) => { +driveRouter.delete('/file', async (req, res) => { const { error: errQ, value: query } = fileParamValidation(req.query) if (errQ) return res.status(400).send(errQ.details[0].message) @@ -153,7 +151,6 @@ driveRouter.delete('/file', authorize, async (req, res) => { driveRouter.post( '/file', - authorize, (...arg) => multerSingle('file', arg), async (req, res) => { const { error: errQ, value: query } = fileParamValidation(req.query) @@ -182,7 +179,6 @@ driveRouter.post( driveRouter.patch( '/file', - authorize, (...arg) => multerSingle('file', arg), async (req, res) => { const { error: errQ, value: query } = fileParamValidation(req.query) @@ -209,7 +205,7 @@ driveRouter.patch( } ) -driveRouter.get('/fileTree', authorize, async (req, res) => { +driveRouter.get('/fileTree', async (req, res) => { try { const response = await controller.getFileTree() res.send(response) diff --git a/api/src/routes/api/index.ts b/api/src/routes/api/index.ts index 45aa73b..04e4a19 100644 --- a/api/src/routes/api/index.ts +++ b/api/src/routes/api/index.ts @@ -36,7 +36,12 @@ router.use('/group', desktopRestrict, groupRouter) router.use('/stp', authenticateAccessToken, stpRouter) router.use('/code', authenticateAccessToken, codeRouter) router.use('/user', desktopRestrict, userRouter) -router.use('/permission', desktopRestrict, permissionRouter) +router.use( + '/permission', + desktopRestrict, + authenticateAccessToken, + permissionRouter +) router.use( '/', diff --git a/api/src/routes/api/permission.ts b/api/src/routes/api/permission.ts index d354744..1cab853 100644 --- a/api/src/routes/api/permission.ts +++ b/api/src/routes/api/permission.ts @@ -1,10 +1,6 @@ import express from 'express' import { PermissionController } from '../../controllers/' -import { - authenticateAccessToken, - verifyAdmin, - authorize -} from '../../middlewares' +import { verifyAdmin } from '../../middlewares' import { registerPermissionValidation, updatePermissionValidation @@ -13,65 +9,49 @@ import { const permissionRouter = express.Router() const controller = new PermissionController() -permissionRouter.get( - '/', - authenticateAccessToken, - authorize, - async (req, res) => { - try { - const response = await controller.getAllPermissions() - res.send(response) - } catch (err: any) { - const statusCode = err.code - delete err.code - res.status(statusCode).send(err.message) - } +permissionRouter.get('/', async (req, res) => { + try { + const response = await controller.getAllPermissions() + res.send(response) + } catch (err: any) { + const statusCode = err.code + delete err.code + res.status(statusCode).send(err.message) } -) +}) -permissionRouter.post( - '/', - authenticateAccessToken, - verifyAdmin, - async (req, res) => { - const { error, value: body } = registerPermissionValidation(req.body) - if (error) return res.status(400).send(error.details[0].message) +permissionRouter.post('/', verifyAdmin, async (req, res) => { + const { error, value: body } = registerPermissionValidation(req.body) + if (error) return res.status(400).send(error.details[0].message) - try { - const response = await controller.createPermission(body) - res.send(response) - } catch (err: any) { - const statusCode = err.code - delete err.code - res.status(statusCode).send(err.message) - } + try { + const response = await controller.createPermission(body) + res.send(response) + } catch (err: any) { + const statusCode = err.code + delete err.code + res.status(statusCode).send(err.message) } -) +}) -permissionRouter.patch( - '/:permissionId', - authenticateAccessToken, - verifyAdmin, - async (req: any, res) => { - const { permissionId } = req.params +permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => { + const { permissionId } = req.params - const { error, value: body } = updatePermissionValidation(req.body) - if (error) return res.status(400).send(error.details[0].message) + const { error, value: body } = updatePermissionValidation(req.body) + if (error) return res.status(400).send(error.details[0].message) - try { - const response = await controller.updatePermission(permissionId, body) - res.send(response) - } catch (err: any) { - const statusCode = err.code - delete err.code - res.status(statusCode).send(err.message) - } + try { + const response = await controller.updatePermission(permissionId, body) + res.send(response) + } catch (err: any) { + const statusCode = err.code + delete err.code + res.status(statusCode).send(err.message) } -) +}) permissionRouter.delete( '/:permissionId', - authenticateAccessToken, verifyAdmin, async (req: any, res) => { const { permissionId } = req.params diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index ecd3587..2c66bab 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -150,6 +150,19 @@ describe('permission', () => { expect(res.body).toEqual({}) }) + it('should respond with Bad Request if uri is not valid', async () => { + const res = await request(app) + .post('/SASjsApi/permission') + .auth(adminAccessToken, { type: 'bearer' }) + .send({ + ...permission, + uri: '/some/random/api/endpoint' + }) + .expect(400) + + expect(res.body).toEqual({}) + }) + it('should respond with Bad Request if setting is missing', async () => { const res = await request(app) .post('/SASjsApi/permission') diff --git a/api/src/routes/api/stp.ts b/api/src/routes/api/stp.ts index 14a3dea..858feb5 100644 --- a/api/src/routes/api/stp.ts +++ b/api/src/routes/api/stp.ts @@ -2,14 +2,13 @@ import express from 'express' import { executeProgramRawValidation } from '../../utils' import { STPController } from '../../controllers/' import { FileUploadController } from '../../controllers/internal' -import { authorize } from '../../middlewares' const stpRouter = express.Router() const fileUploadController = new FileUploadController() const controller = new STPController() -stpRouter.get('/execute', authorize, async (req, res) => { +stpRouter.get('/execute', async (req, res) => { const { error, value: query } = executeProgramRawValidation(req.query) if (error) return res.status(400).send(error.details[0].message) @@ -33,7 +32,6 @@ stpRouter.get('/execute', authorize, async (req, res) => { stpRouter.post( '/execute', - authorize, fileUploadController.preUploadMiddleware, fileUploadController.getMulterUploadObject().any(), async (req, res: any) => { diff --git a/api/src/utils/appStreamConfig.ts b/api/src/utils/appStreamConfig.ts index c293b35..f4f137d 100644 --- a/api/src/utils/appStreamConfig.ts +++ b/api/src/utils/appStreamConfig.ts @@ -5,6 +5,8 @@ import { AppStreamConfig } from '../types' import { getAppStreamConfigPath } from './file' export const loadAppStreamConfig = async () => { + process.appStreamConfig = {} + if (process.env.NODE_ENV === 'test') return const appStreamConfigPath = getAppStreamConfigPath() @@ -21,7 +23,6 @@ export const loadAppStreamConfig = async () => { } catch (_) { appStreamConfig = {} } - process.appStreamConfig = {} for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) { const { appLoc, streamWebFolder, streamLogo } = entry From 0b759a5594f8bf97b0034e2c5109f58d64c67d3c Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 5 Jul 2022 02:34:33 +0500 Subject: [PATCH 49/54] chore: code fixes API + web --- api/src/middlewares/authenticateToken.ts | 6 +-- api/src/middlewares/authorize.ts | 50 +++++++++---------- api/src/routes/appStream/index.ts | 6 +-- api/src/utils/getAuthorizedRoutes.ts | 24 +++++++-- web/package.json | 4 +- web/src/components/login.tsx | 2 +- .../Settings/updatePermissionModal.tsx | 6 ++- 7 files changed, 57 insertions(+), 41 deletions(-) diff --git a/api/src/middlewares/authenticateToken.ts b/api/src/middlewares/authenticateToken.ts index e368384..b53b83c 100644 --- a/api/src/middlewares/authenticateToken.ts +++ b/api/src/middlewares/authenticateToken.ts @@ -5,7 +5,7 @@ import { fetchLatestAutoExec, ModeType, verifyTokenInDB, - getAuthorizedRoutes + isAuthorizingRoute } from '../utils' import { desktopUser } from './desktop' import { authorize } from './authorize' @@ -21,9 +21,7 @@ export const authenticateAccessToken: RequestHandler = async ( return next() } - const authorizedRoutes = getAuthorizedRoutes() - const uri = req.baseUrl + req.path - const nextFunction = authorizedRoutes.includes(uri) + const nextFunction = isAuthorizingRoute(req) ? () => authorize(req, res, next) : next diff --git a/api/src/middlewares/authorize.ts b/api/src/middlewares/authorize.ts index 124b332..20c85fa 100644 --- a/api/src/middlewares/authorize.ts +++ b/api/src/middlewares/authorize.ts @@ -2,35 +2,35 @@ import { RequestHandler } from 'express' import User from '../model/User' import Permission from '../model/Permission' import { PermissionSetting } from '../controllers/permission' +import { getUri } from '../utils' export const authorize: RequestHandler = async (req, res, next) => { - let permission - const user = req.user || req.session.user - if (user) { - // no need to check for permissions when user is admin - if (user.isAdmin) return next() - - const dbUser = await User.findOne({ id: user.userId }) - if (!dbUser) return res.sendStatus(401) - - const uri = req.baseUrl + req.path - - // find permission w.r.t user - permission = await Permission.findOne({ uri, user: dbUser._id }) - - if (permission) { - if (permission.setting === PermissionSetting.grant) return next() - else res.sendStatus(401) - } - - // find permission w.r.t user's groups - for (const group of dbUser.groups) { - permission = await Permission.findOne({ uri, group }) - if (permission && permission.setting === PermissionSetting.grant) - return next() - } + const { user } = req + if (!user) { return res.sendStatus(401) } + + // no need to check for permissions when user is admin + if (user.isAdmin) return next() + + const dbUser = await User.findOne({ id: user.userId }) + if (!dbUser) return res.sendStatus(401) + + const uri = getUri(req) + + // find permission w.r.t user + const permission = await Permission.findOne({ uri, user: dbUser._id }) + + if (permission) { + if (permission.setting === PermissionSetting.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() + } return res.sendStatus(401) } diff --git a/api/src/routes/appStream/index.ts b/api/src/routes/appStream/index.ts index 1152644..ca2e279 100644 --- a/api/src/routes/appStream/index.ts +++ b/api/src/routes/appStream/index.ts @@ -1,6 +1,6 @@ import path from 'path' import express, { Request } from 'express' -import { authorize } from '../../middlewares/authorize' +import { authenticateAccessToken } from '../../middlewares' import { folderExists } from '@sasjs/utils' import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils' @@ -10,7 +10,7 @@ const appStreams: { [key: string]: string } = {} const router = express.Router() -router.get('/', authorize, async (req, res) => { +router.get('/', authenticateAccessToken, async (req, res) => { const content = appStreamHtml(process.appStreamConfig) res.cookie('XSRF-TOKEN', req.csrfToken()) @@ -67,7 +67,7 @@ export const publishAppStream = async ( return {} } -router.get(`/*`, authorize, function (req: Request, res, next) { +router.get(`/*`, authenticateAccessToken, function (req: Request, res, next) { const reqPath = req.path.replace(/^\//, '') // Redirecting to url with trailing slash for appStream base URL only diff --git a/api/src/utils/getAuthorizedRoutes.ts b/api/src/utils/getAuthorizedRoutes.ts index 7d63b9f..aafe4c4 100644 --- a/api/src/utils/getAuthorizedRoutes.ts +++ b/api/src/utils/getAuthorizedRoutes.ts @@ -1,8 +1,4 @@ -export const getAuthorizedRoutes = () => { - const streamingApps = Object.keys(process.appStreamConfig) - const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`) - return [...StaticAuthorizedRoutes, ...streamingAppsRoutes] -} +import { Request } from 'express' const StaticAuthorizedRoutes = [ '/AppStream', @@ -15,3 +11,21 @@ const StaticAuthorizedRoutes = [ '/SASjsApi/drive/fileTree', '/SASjsApi/permission' ] + +export const getAuthorizedRoutes = () => { + const streamingApps = Object.keys(process.appStreamConfig) + const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`) + return [...StaticAuthorizedRoutes, ...streamingAppsRoutes] +} + +export const getUri = (req: Request) => { + const { baseUrl, path: reqPath } = req + + const appStream = reqPath.split('/')[1] + + // removing trailing slash of URLs + return (baseUrl + '/' + appStream).replace(/\/$/, '') +} + +export const isAuthorizingRoute = (req: Request): boolean => + getAuthorizedRoutes().includes(getUri(req)) diff --git a/web/package.json b/web/package.json index a3a5898..8f6681d 100644 --- a/web/package.json +++ b/web/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "scripts": { - "start": "npx webpack-dev-server --config webpack.dev.ts --hot", - "build": "npx webpack --config webpack.prod.ts" + "start": "webpack-dev-server --config webpack.dev.ts --hot", + "build": "webpack --config webpack.prod.ts" }, "dependencies": { "@emotion/react": "^11.4.1", diff --git a/web/src/components/login.tsx b/web/src/components/login.tsx index 706a714..c679a8d 100644 --- a/web/src/components/login.tsx +++ b/web/src/components/login.tsx @@ -22,7 +22,7 @@ const Login = () => { username, password }).catch((err: any) => { - setErrorMessage(err.response.data) + setErrorMessage(err.response?.data || err.toString()) return {} }) diff --git a/web/src/containers/Settings/updatePermissionModal.tsx b/web/src/containers/Settings/updatePermissionModal.tsx index 796017d..55d92de 100644 --- a/web/src/containers/Settings/updatePermissionModal.tsx +++ b/web/src/containers/Settings/updatePermissionModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, Dispatch, SetStateAction } from 'react' +import React, { useState, Dispatch, SetStateAction, useEffect } from 'react' import { Button, Grid, @@ -38,6 +38,10 @@ const UpdatePermissionModal = ({ }: UpdatePermissionModalProps) => { const [permissionSetting, setPermissionSetting] = useState('Grant') + useEffect(() => { + if (permission) setPermissionSetting(permission.setting) + }, [permission]) + return ( handleOpen(false)} open={open}> Date: Tue, 5 Jul 2022 03:26:37 +0500 Subject: [PATCH 50/54] chore: fixed specs --- api/src/middlewares/authenticateToken.ts | 2 +- api/src/middlewares/verifyAdmin.ts | 3 ++- api/src/routes/api/spec/drive.spec.ts | 8 ++++---- api/src/routes/api/spec/permission.spec.ts | 14 +++++++++----- api/src/utils/getAuthorizedRoutes.ts | 2 +- api/src/utils/specs/extractHeaders.spec.ts | 2 +- api/src/utils/specs/parseLogToArray.spec.ts | 2 +- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/api/src/middlewares/authenticateToken.ts b/api/src/middlewares/authenticateToken.ts index b53b83c..24ed1e8 100644 --- a/api/src/middlewares/authenticateToken.ts +++ b/api/src/middlewares/authenticateToken.ts @@ -68,7 +68,7 @@ const authenticateToken = ( tokenType: 'accessToken' | 'refreshToken' ) => { const { MODE } = process.env - if (MODE?.trim() !== 'server') { + if (MODE === ModeType.Desktop) { req.user = { userId: 1234, clientId: 'desktopModeClientId', diff --git a/api/src/middlewares/verifyAdmin.ts b/api/src/middlewares/verifyAdmin.ts index 4ca26f7..ea04ada 100644 --- a/api/src/middlewares/verifyAdmin.ts +++ b/api/src/middlewares/verifyAdmin.ts @@ -1,8 +1,9 @@ import { RequestHandler } from 'express' +import { ModeType } from '../utils' export const verifyAdmin: RequestHandler = (req, res, next) => { const { MODE } = process.env - if (MODE?.trim() !== 'server') return next() + if (MODE === ModeType.Desktop) return next() const { user } = req if (!user?.isAdmin) return res.status(401).send('Admin account required') diff --git a/api/src/routes/api/spec/drive.spec.ts b/api/src/routes/api/spec/drive.spec.ts index d9475c4..3bf2109 100644 --- a/api/src/routes/api/spec/drive.spec.ts +++ b/api/src/routes/api/spec/drive.spec.ts @@ -65,25 +65,25 @@ describe('drive', () => { const dbUser = await controller.createUser(user) accessToken = await generateAndSaveToken(dbUser.id) - permissionController.createPermission({ + await permissionController.createPermission({ uri: '/SASjsApi/drive/deploy', principalType: PrincipalType.user, principalId: dbUser.id, setting: PermissionSetting.grant }) - permissionController.createPermission({ + await permissionController.createPermission({ uri: '/SASjsApi/drive/deploy/upload', principalType: PrincipalType.user, principalId: dbUser.id, setting: PermissionSetting.grant }) - permissionController.createPermission({ + await permissionController.createPermission({ uri: '/SASjsApi/drive/file', principalType: PrincipalType.user, principalId: dbUser.id, setting: PermissionSetting.grant }) - permissionController.createPermission({ + await permissionController.createPermission({ uri: '/SASjsApi/drive/folder', principalType: PrincipalType.user, principalId: dbUser.id, diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index 2c66bab..aca367e 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -120,10 +120,14 @@ describe('permission', () => { expect(res.body).toEqual({}) }) - it('should respond with Unauthorized if access token is not of an admin account', async () => { - const accessToken = await generateSaveTokenAndCreateUser({ - ...user, - username: 'create' + user.username + it('should respond with Unauthorized if access token is not of an admin account even if user has permission', 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) @@ -459,7 +463,7 @@ describe('permission', () => { }) const accessToken = await generateAndSaveToken(dbUser.id) await permissionController.createPermission({ - uri: '/SASjsApi/permission/', + uri: '/SASjsApi/permission', principalType: PrincipalType.user, principalId: dbUser.id, setting: PermissionSetting.grant diff --git a/api/src/utils/getAuthorizedRoutes.ts b/api/src/utils/getAuthorizedRoutes.ts index aafe4c4..d332a34 100644 --- a/api/src/utils/getAuthorizedRoutes.ts +++ b/api/src/utils/getAuthorizedRoutes.ts @@ -5,7 +5,7 @@ const StaticAuthorizedRoutes = [ '/SASjsApi/code/execute', '/SASjsApi/stp/execute', '/SASjsApi/drive/deploy', - '/SASjsApi/drive/upload', + '/SASjsApi/drive/deploy/upload', '/SASjsApi/drive/file', '/SASjsApi/drive/folder', '/SASjsApi/drive/fileTree', diff --git a/api/src/utils/specs/extractHeaders.spec.ts b/api/src/utils/specs/extractHeaders.spec.ts index 9d29f04..780b326 100644 --- a/api/src/utils/specs/extractHeaders.spec.ts +++ b/api/src/utils/specs/extractHeaders.spec.ts @@ -1,4 +1,4 @@ -import { extractHeaders } from '..' +import { extractHeaders } from '../extractHeaders' describe('extractHeaders', () => { it('should return valid http headers', () => { diff --git a/api/src/utils/specs/parseLogToArray.spec.ts b/api/src/utils/specs/parseLogToArray.spec.ts index 3bd0de4..821f041 100644 --- a/api/src/utils/specs/parseLogToArray.spec.ts +++ b/api/src/utils/specs/parseLogToArray.spec.ts @@ -1,4 +1,4 @@ -import { parseLogToArray } from '..' +import { parseLogToArray } from '../parseLogToArray' describe('parseLogToArray', () => { it('should parse log to array type', () => { From 7edb47a4cb1c8d43e9bae6896f16119a394d7f4d Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 5 Jul 2022 03:40:54 +0500 Subject: [PATCH 51/54] chore: build fix --- api/scripts/compileSysInit.ts | 2 +- api/scripts/copySASjsCore.ts | 6 +++++- api/src/app.ts | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/scripts/compileSysInit.ts b/api/scripts/compileSysInit.ts index f094324..4c04f64 100644 --- a/api/scripts/compileSysInit.ts +++ b/api/scripts/compileSysInit.ts @@ -6,7 +6,7 @@ import { readFile, SASJsFileType } from '@sasjs/utils' -import { apiRoot, sysInitCompiledPath } from '../src/utils' +import { apiRoot, sysInitCompiledPath } from '../src/utils/file' const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core') diff --git a/api/scripts/copySASjsCore.ts b/api/scripts/copySASjsCore.ts index 6dcb02e..614c8ee 100644 --- a/api/scripts/copySASjsCore.ts +++ b/api/scripts/copySASjsCore.ts @@ -8,7 +8,11 @@ import { listFilesInFolder } from '@sasjs/utils' -import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils' +import { + apiRoot, + sasJSCoreMacros, + sasJSCoreMacrosInfo +} from '../src/utils/file' const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core') diff --git a/api/src/app.ts b/api/src/app.ts index de27567..79d26c8 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -11,7 +11,6 @@ import cors from 'cors' import helmet from 'helmet' import { - connectDB, copySASjsCore, CorsType, getWebBuildFolder, From 1108d3dd7bef7d2782dda129f1e5d8ac8fb9f17b Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 5 Jul 2022 05:30:13 +0500 Subject: [PATCH 52/54] chore: quick fix --- api/src/utils/getAuthorizedRoutes.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/api/src/utils/getAuthorizedRoutes.ts b/api/src/utils/getAuthorizedRoutes.ts index d332a34..1d46eeb 100644 --- a/api/src/utils/getAuthorizedRoutes.ts +++ b/api/src/utils/getAuthorizedRoutes.ts @@ -21,10 +21,14 @@ export const getAuthorizedRoutes = () => { export const getUri = (req: Request) => { const { baseUrl, path: reqPath } = req - const appStream = reqPath.split('/')[1] + if (baseUrl === '/AppStream') { + const appStream = reqPath.split('/')[1] - // removing trailing slash of URLs - return (baseUrl + '/' + appStream).replace(/\/$/, '') + // removing trailing slash of URLs + return (baseUrl + '/' + appStream).replace(/\/$/, '') + } + + return baseUrl + reqPath } export const isAuthorizingRoute = (req: Request): boolean => From a10b87930c8f7227609cbf4f1da0bf472eb04b63 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 5 Jul 2022 15:29:44 +0500 Subject: [PATCH 53/54] chore: quick fix --- api/src/utils/getAuthorizedRoutes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/utils/getAuthorizedRoutes.ts b/api/src/utils/getAuthorizedRoutes.ts index 1d46eeb..93412fa 100644 --- a/api/src/utils/getAuthorizedRoutes.ts +++ b/api/src/utils/getAuthorizedRoutes.ts @@ -28,7 +28,7 @@ export const getUri = (req: Request) => { return (baseUrl + '/' + appStream).replace(/\/$/, '') } - return baseUrl + reqPath + return (baseUrl + reqPath).replace(/\/$/, '') } export const isAuthorizingRoute = (req: Request): boolean => From ddd179bbeef258bbecb599f6dc836503887faf1a Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 5 Jul 2022 16:18:14 +0500 Subject: [PATCH 54/54] chore: added specs for verifying permissions --- api/src/routes/api/spec/permission.spec.ts | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/api/src/routes/api/spec/permission.spec.ts b/api/src/routes/api/spec/permission.spec.ts index aca367e..39b3f71 100644 --- a/api/src/routes/api/spec/permission.spec.ts +++ b/api/src/routes/api/spec/permission.spec.ts @@ -4,6 +4,7 @@ import { MongoMemoryServer } from 'mongodb-memory-server' import request from 'supertest' import appPromise from '../../../app' import { + DriveController, UserController, GroupController, ClientController, @@ -17,6 +18,27 @@ import { } from '../../../controllers' import { generateAccessToken, saveTokensInDB } from '../../../utils' +const deployPayload = { + appLoc: 'string', + streamWebFolder: 'string', + fileTree: { + members: [ + { + name: 'string', + type: 'folder', + members: [ + 'string', + { + name: 'string', + type: 'service', + code: 'string' + } + ] + } + ] + } +} + const clientId = 'someclientID' const adminUser = { displayName: 'Test Admin', @@ -478,6 +500,51 @@ describe('permission', () => { expect(res.body).toHaveLength(3) }) }) + + describe.only('verify', () => { + beforeAll(async () => { + await permissionController.createPermission({ + ...permission, + uri: '/SASjsApi/drive/deploy', + principalId: dbUser.id + }) + }) + + beforeEach(() => { + jest + .spyOn(DriveController.prototype, 'deploy') + .mockImplementation((deployPayload) => + Promise.resolve({ + status: 'success', + message: 'Files deployed successfully to @sasjs/server.' + }) + ) + }) + + afterEach(() => { + jest.resetAllMocks() + }) + + it('should create files in SASJS drive', async () => { + const accessToken = await generateAndSaveToken(dbUser.id) + + await request(app) + .get('/SASjsApi/drive/deploy') + .auth(accessToken, { type: 'bearer' }) + .send(deployPayload) + .expect(200) + }) + + it('should respond unauthorized', async () => { + const accessToken = await generateAndSaveToken(dbUser.id) + + await request(app) + .get('/SASjsApi/drive/deploy/upload') + .auth(accessToken, { type: 'bearer' }) + .send() + .expect(401) + }) + }) }) const generateSaveTokenAndCreateUser = async (