mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add batch delete api and batch create api
This commit is contained in:
@@ -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' }))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString()));
|
||||
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = []
|
||||
|
||||
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" })
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Omit } from 'utility-types';
|
||||
import { ISecret } from '../../models';
|
||||
|
||||
export type ModifySecretPayload = Omit<ISecret, "user" | "version" | "environment" | "workspace">;
|
||||
// User input for CRUD operations on secrets routes
|
||||
export type SecretUserInput = Omit<ISecret, "user" | "version" | "environment" | "workspace">;
|
||||
|
||||
export type SafeUpdateSecret = Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>;
|
||||
// Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading
|
||||
export type SanitizedSecretModify = Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>;
|
||||
|
||||
// Used for modeling sanitized secrets before create. To be used for converting user input for creating new secrets
|
||||
export type SanitizedSecretForCreate = Omit<ISecret, "version" | "_id">;
|
||||
|
||||
Reference in New Issue
Block a user