From f015e6be6e465a1fcd271572fe17c3472193d990 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 1 Jan 2023 02:12:24 -0500 Subject: [PATCH] Add batch delete api and batch create api --- backend/src/middleware/validateRequest.ts | 6 +- backend/src/routes/v2/secret.ts | 180 ++++++++++------------ backend/src/types/secret/types.ts | 9 +- 3 files changed, 95 insertions(+), 100 deletions(-) diff --git a/backend/src/middleware/validateRequest.ts b/backend/src/middleware/validateRequest.ts index 484b02cab..3bc106189 100644 --- a/backend/src/middleware/validateRequest.ts +++ b/backend/src/middleware/validateRequest.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { validationResult } from 'express-validator'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors'; /** * Validate intended inputs on [req] via express-validator @@ -15,12 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { - return next(BadRequestError({context: {errors: errors.array}})) + return next(ValidationError({ context: { errors: `One or more of your paramters are invalid [error=${errors.array}]` } })) } return next(); } catch (err) { - return next(UnauthorizedRequestError({message: 'Unauthenticated requests are not allowed. Try logging in'})) + return next(UnauthorizedRequestError({ message: 'Unauthenticated requests are not allowed. Try logging in' })) } }; diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index 078a04106..e9c6a9bd9 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,57 +1,70 @@ import express, { Request, Response } from 'express'; import { requireAuth, requireWorkspaceAuth, validateRequest } from '../../middleware'; import { ISecret, Secret } from '../../models'; -import { decryptSymmetric } from '../../utils/crypto'; -import { getLogger } from '../../utils/logger'; import { body, param, query, check } from 'express-validator'; -import { BadRequestError, InternalServerError, UnauthorizedRequestError } from '../../utils/errors'; +import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; -import { ModifySecretPayload, SafeUpdateSecret } from '../../types/secret/types'; -import { AnyBulkWriteOperation } from 'mongodb'; +import { SanitizedSecretModify, SecretUserInput, SanitizedSecretForCreate } from '../../types/secret/types'; import to from 'await-to-js'; -import { Types } from 'mongoose'; +import mongoose, { Types } from 'mongoose'; +import { AnyBulkWriteOperation } from 'mongodb'; +const { ValidationError } = mongoose.Error; const router = express.Router(); /** - * Create a single secret for a given workspace and environment + * Create many secrets for a given workspace and environmentName */ router.post( - '/', requireAuth, - body('secret').exists().isObject(), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - async (req: Request, res: Response) => { - try { - const { secret }: { secret: ISecret[] } = req.body; - const newlyCreatedSecret = await Secret.create(secret) - res.status(200).json(newlyCreatedSecret) - } catch { - throw BadRequestError({ message: "Unable to create the secret" }) - } - } -); - -/** - * Create many secrets - */ -router.post( - '/bulk-create', requireAuth, + '/batch-create/workspace/:workspaceId/environment/:environmentName', + requireAuth, requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], acceptedStatuses: [COMPLETED, GRANTED] }), + param('workspaceId').exists().isMongoId().trim(), + param('environmentName').exists().trim(), body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), + validateRequest, async (req: Request, res: Response) => { - try { - const { secrets }: { secrets: ISecret[] } = req.body; - const newlyCreatedSecrets = await Secret.insertMany(secrets) - res.status(200).json(newlyCreatedSecrets) - } catch { - throw BadRequestError({ message: "Unable to create the secret" }) + const secretsToCreate: SecretUserInput[] = req.body.secrets; + const { workspaceId, environmentName } = req.params + const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] + + secretsToCreate.forEach(rawSecret => { + const safeUpdateFields: SanitizedSecretForCreate = { + secretKeyCiphertext: rawSecret.secretKeyCiphertext, + secretKeyIV: rawSecret.secretKeyIV, + secretKeyTag: rawSecret.secretKeyTag, + secretKeyHash: rawSecret.secretKeyHash, + secretValueCiphertext: rawSecret.secretValueCiphertext, + secretValueIV: rawSecret.secretValueIV, + secretValueTag: rawSecret.secretValueTag, + secretValueHash: rawSecret.secretValueHash, + secretCommentCiphertext: rawSecret.secretCommentCiphertext, + secretCommentIV: rawSecret.secretCommentIV, + secretCommentTag: rawSecret.secretCommentTag, + secretCommentHash: rawSecret.secretCommentHash, + workspace: new Types.ObjectId(workspaceId), + environment: environmentName, + type: rawSecret.type, + user: new Types.ObjectId(req.user._id) + } + + sanitizedSecretesToCreate.push(safeUpdateFields) + }) + + const [bulkCreateError, newlyCreatedSecrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) + + if (bulkCreateError) { + if (bulkCreateError instanceof ValidationError) { + throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack }) + } + + throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) } + + res.status(200).send(newlyCreatedSecrets) } ); @@ -75,68 +88,45 @@ router.get( ); /** - * Get a single secret by secret id - */ -router.get( - '/:bulk', requireAuth, param('secretId').exists().trim(), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - validateRequest, async (req: Request, res: Response) => { - try { - const secretFromDB = await Secret.findById(req.params.secretId) - return res.status(200).send(secretFromDB); - } catch (e) { - throw BadRequestError({ message: "Unable to find the requested secret" }) - } - } -); - -/** - * Delete a single secret by secret id + * Batch delete secrets in a given workspace and environment name */ router.delete( - '/:secretId', + '/batch/workspace/:workspaceId/environment/:environmentName', requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), - param('secretId').exists().trim(), - validateRequest, async (req: Request, res: Response) => { - try { - const secretFromDB = await Secret.deleteOne({ - _id: req.params.secretId - }) - return res.status(200).send(secretFromDB); - } catch (e) { - throw BadRequestError({ message: "Unable to find the requested secret" }) - } - } -); - -/** - * Delete many secrets by secret ids - */ -router.delete( - '/batch', - requireAuth, - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED] - }), + param('workspaceId').exists().isMongoId().trim(), + param('environmentName').exists().trim(), body('secretIds').exists().isArray(), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + acceptedStatuses: [COMPLETED, GRANTED] + }), validateRequest, async (req: Request, res: Response) => { - try { - const secretIdsToDelete: string[] = req.body.secretIds - const secretFromDB = await Secret.deleteMany({ - _id: { $in: secretIdsToDelete } - }) - return res.status(200).send(secretFromDB); - } catch (error) { - throw BadRequestError({ message: `Unable to delete the requested secrets by ids [${req.body.secretIds}]` }) + const { workspaceId, environmentName } = req.params + const secretIdsToDelete: string[] = req.body.secretIds + + const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + if (secretIdsUserCanDeleteError) { + throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` }) } + + const secretsUserCanDeleteSet: Set = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString())); + const deleteOperationsToPerform: AnyBulkWriteOperation[] = [] + + secretIdsToDelete.forEach(secretIdToDelete => { + if (secretsUserCanDeleteSet.has(secretIdToDelete)) { + const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } } + deleteOperationsToPerform.push(deleteOperation) + } else { + throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" }) + } + }) + + const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) + if (bulkModificationInfoError) { + throw InternalServerError({ message: "Unable to apply modifications, please try again" }) + } + + res.status(200).send() } ); @@ -144,7 +134,7 @@ router.delete( * Apply modifications to many existing secrets in a given workspace and environment */ router.patch( - '/bulk-modify/:workspaceId/:environmentName', + '/batch-modify/:workspaceId/:environmentName', requireAuth, body('secrets').exists().isArray().custom((value) => value.every((item: ISecret) => typeof item === 'object')), param('workspaceId').exists().isMongoId().trim(), @@ -155,7 +145,7 @@ router.patch( }), validateRequest, async (req: Request, res: Response) => { const { workspaceId, environmentName } = req.params - const secretsModificationsRequested: ModifySecretPayload[] = req.body.secrets; + const secretsModificationsRequested: SecretUserInput[] = req.body.secrets; const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) if (secretIdsUserCanModifyError) { @@ -167,7 +157,7 @@ router.patch( secretsModificationsRequested.forEach(userModifiedSecret => { if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { - const safeUpdateFields: SafeUpdateSecret = { + const sanitizedSecret: SanitizedSecretModify = { secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, secretKeyIV: userModifiedSecret.secretKeyIV, secretKeyTag: userModifiedSecret.secretKeyTag, @@ -182,7 +172,7 @@ router.patch( secretCommentHash: userModifiedSecret.secretCommentHash, } - const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: safeUpdateFields } } } + const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } } updateOperationsToPerform.push(updateOperation) } else { throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) diff --git a/backend/src/types/secret/types.ts b/backend/src/types/secret/types.ts index c4a8d0cc9..a773d778c 100644 --- a/backend/src/types/secret/types.ts +++ b/backend/src/types/secret/types.ts @@ -1,6 +1,11 @@ import { Omit } from 'utility-types'; import { ISecret } from '../../models'; -export type ModifySecretPayload = Omit; +// User input for CRUD operations on secrets routes +export type SecretUserInput = Omit; -export type SafeUpdateSecret = Partial>; +// Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading +export type SanitizedSecretModify = Partial>; + +// Used for modeling sanitized secrets before create. To be used for converting user input for creating new secrets +export type SanitizedSecretForCreate = Omit;