mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
11 Commits
doc/add-gi
...
approvals-
Author | SHA1 | Date | |
---|---|---|---|
625fa0725e | |||
e32cb8c24f | |||
de724d2804 | |||
42d94521a4 | |||
203a603769 | |||
e1a88b2d1a | |||
53237dd52c | |||
6a906b17ad | |||
f38a364d3b | |||
4749e243bb | |||
eb055e8b16 |
@ -39,7 +39,8 @@ import {
|
|||||||
password as v1PasswordRouter,
|
password as v1PasswordRouter,
|
||||||
stripe as v1StripeRouter,
|
stripe as v1StripeRouter,
|
||||||
integration as v1IntegrationRouter,
|
integration as v1IntegrationRouter,
|
||||||
integrationAuth as v1IntegrationAuthRouter
|
integrationAuth as v1IntegrationAuthRouter,
|
||||||
|
secretApprovalRequest as v1SecretApprovalRequest
|
||||||
} from './routes/v1';
|
} from './routes/v1';
|
||||||
import {
|
import {
|
||||||
signup as v2SignupRouter,
|
signup as v2SignupRouter,
|
||||||
@ -59,7 +60,7 @@ import { healthCheck } from './routes/status';
|
|||||||
|
|
||||||
import { getLogger } from './utils/logger';
|
import { getLogger } from './utils/logger';
|
||||||
import { RouteNotFoundError } from './utils/errors';
|
import { RouteNotFoundError } from './utils/errors';
|
||||||
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
import { handleMongoInvalidDataError, requestErrorHandler } from './middleware/requestErrorHandler';
|
||||||
|
|
||||||
// patch async route params to handle Promise Rejections
|
// patch async route params to handle Promise Rejections
|
||||||
patchRouterParam();
|
patchRouterParam();
|
||||||
@ -110,6 +111,7 @@ app.use('/api/v1/password', v1PasswordRouter);
|
|||||||
app.use('/api/v1/stripe', v1StripeRouter);
|
app.use('/api/v1/stripe', v1StripeRouter);
|
||||||
app.use('/api/v1/integration', v1IntegrationRouter);
|
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||||
|
app.use('/api/v1/secrets-approval-request', v1SecretApprovalRequest)
|
||||||
|
|
||||||
// v2 routes
|
// v2 routes
|
||||||
app.use('/api/v2/signup', v2SignupRouter);
|
app.use('/api/v2/signup', v2SignupRouter);
|
||||||
@ -136,6 +138,9 @@ app.use((req, res, next) => {
|
|||||||
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
|
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// handle mongo validation errors
|
||||||
|
app.use(handleMongoInvalidDataError);
|
||||||
|
|
||||||
//* Error Handling Middleware (must be after all routing logic)
|
//* Error Handling Middleware (must be after all routing logic)
|
||||||
app.use(requestErrorHandler)
|
app.use(requestErrorHandler)
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import * as stripeController from './stripeController';
|
|||||||
import * as userActionController from './userActionController';
|
import * as userActionController from './userActionController';
|
||||||
import * as userController from './userController';
|
import * as userController from './userController';
|
||||||
import * as workspaceController from './workspaceController';
|
import * as workspaceController from './workspaceController';
|
||||||
|
import * as secretApprovalController from './secretApprovalController';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
authController,
|
authController,
|
||||||
@ -31,5 +32,6 @@ export {
|
|||||||
stripeController,
|
stripeController,
|
||||||
userActionController,
|
userActionController,
|
||||||
userController,
|
userController,
|
||||||
workspaceController
|
workspaceController,
|
||||||
|
secretApprovalController
|
||||||
};
|
};
|
||||||
|
320
backend/src/controllers/v1/secretApprovalController.ts
Normal file
320
backend/src/controllers/v1/secretApprovalController.ts
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import SecretApprovalRequest, { ApprovalStatus, ChangeType, IApprover, IRequestedChange } from '../../models/secretApprovalRequest';
|
||||||
|
import { Builder, IBuilder } from "builder-pattern"
|
||||||
|
import { secretObjectHasRequiredFields, validateSecrets } from '../../helpers/secret';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { SECRET_PERSONAL, SECRET_SHARED } from '../../variables';
|
||||||
|
import { BadRequestError, ResourceNotFound, UnauthorizedRequestError } from '../../utils/errors';
|
||||||
|
import { ISecret, Membership, Secret, Workspace } from '../../models';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
export const createApprovalRequest = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, environment, requestedChanges } = req.body;
|
||||||
|
|
||||||
|
// validate workspace
|
||||||
|
const workspaceFromDB = await Workspace.findById(workspaceId)
|
||||||
|
if (!workspaceFromDB) {
|
||||||
|
throw ResourceNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const environmentBelongsToWorkspace = _.some(workspaceFromDB.environments, { slug: environment })
|
||||||
|
if (!environmentBelongsToWorkspace) {
|
||||||
|
throw ResourceNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for secret duplicates
|
||||||
|
const hasSecretIdDuplicates = requestedChanges.length !== _.uniqBy(requestedChanges, 'modifiedSecretParentId').length;
|
||||||
|
if (hasSecretIdDuplicates) {
|
||||||
|
throw BadRequestError({ message: "Request cannot contain changes for duplicate secrets" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the workspace has approvers set
|
||||||
|
if (!workspaceFromDB.approvers.length) {
|
||||||
|
throw BadRequestError({ message: "There are no designated approvers for this project, you must set approvers first before making a request" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const approverIds = _.compact(_.map(workspaceFromDB.approvers, "userId"))
|
||||||
|
const approversFormatted: IApprover[] = approverIds.map(id => {
|
||||||
|
return { "userId": id, status: ApprovalStatus.PENDING }
|
||||||
|
})
|
||||||
|
|
||||||
|
const listOfSecretIdsToModify = _.compact(_.map(requestedChanges, "modifiedSecretParentId"))
|
||||||
|
|
||||||
|
// Ensure that the user requesting changes for the set of secrets can indeed interact with said secrets
|
||||||
|
if (listOfSecretIdsToModify.length > 0) {
|
||||||
|
await validateSecrets({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
secretIds: listOfSecretIdsToModify
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedRequestedChangesList: IRequestedChange[] = []
|
||||||
|
requestedChanges.forEach((requestedChange: IRequestedChange) => {
|
||||||
|
const secretDetailsIsValid = secretObjectHasRequiredFields(requestedChange.modifiedSecretDetails)
|
||||||
|
if (!secretDetailsIsValid) {
|
||||||
|
throw BadRequestError({ message: "One or more required fields are missing from your modified secret" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestedChange.modifiedSecretParentId && (requestedChange.type != ChangeType.DELETE.toString() && requestedChange.type != ChangeType.CREATE.toString())) {
|
||||||
|
throw BadRequestError({ message: "modifiedSecretParentId can only be empty when secret change type is DELETE or CREATE" })
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizedRequestedChangesList.push(Builder<IRequestedChange>()
|
||||||
|
.modifiedSecretParentId(requestedChange.modifiedSecretParentId)
|
||||||
|
.modifiedSecretDetails(requestedChange.modifiedSecretDetails)
|
||||||
|
.approvers(approversFormatted)
|
||||||
|
.type(requestedChange.type).build())
|
||||||
|
});
|
||||||
|
|
||||||
|
const newApprovalRequest = await SecretApprovalRequest.create({
|
||||||
|
workspace: workspaceId,
|
||||||
|
requestedByUserId: req.user._id.toString(),
|
||||||
|
environment: environment,
|
||||||
|
requestedChanges: sanitizedRequestedChangesList
|
||||||
|
})
|
||||||
|
|
||||||
|
const populatedNewApprovalRequest = await newApprovalRequest.populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||||
|
|
||||||
|
return res.send({ approvalRequest: populatedNewApprovalRequest });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllApprovalRequestsForUser = async (req: Request, res: Response) => {
|
||||||
|
const approvalRequests = await SecretApprovalRequest.find({
|
||||||
|
requestedByUserId: req.user._id.toString()
|
||||||
|
}).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||||
|
.sort({ updatedAt: -1 })
|
||||||
|
|
||||||
|
res.send({ approvalRequests: approvalRequests })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllApprovalRequestsThatRequireUserApproval = async (req: Request, res: Response) => {
|
||||||
|
const approvalRequests = await SecretApprovalRequest.find({
|
||||||
|
'requestedChanges.approvers.userId': req.user._id.toString()
|
||||||
|
}).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||||
|
.sort({ updatedAt: -1 })
|
||||||
|
|
||||||
|
res.send({ approvalRequests: approvalRequests })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const approveApprovalRequest = async (req: Request, res: Response) => {
|
||||||
|
const { requestedChangeIds } = req.body;
|
||||||
|
const { reviewId } = req.params
|
||||||
|
|
||||||
|
const approvalRequestFromDB = await SecretApprovalRequest.findById(reviewId)
|
||||||
|
if (!approvalRequestFromDB) {
|
||||||
|
throw ResourceNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedChangesFromDB: IRequestedChange[] = approvalRequestFromDB.requestedChanges
|
||||||
|
const filteredChangesByIds = requestedChangesFromDB.filter(change => requestedChangeIds.includes(change._id.toString()))
|
||||||
|
if (filteredChangesByIds.length != requestedChangeIds.length) {
|
||||||
|
throw BadRequestError({ message: "All requestedChangeIds should exist in this approval request" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const changesThatRequireUserApproval = _.filter(filteredChangesByIds, change => {
|
||||||
|
return _.some(change.approvers, approver => {
|
||||||
|
return approver.userId.toString() == req.user._id.toString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!changesThatRequireUserApproval.length) {
|
||||||
|
throw UnauthorizedRequestError({ message: "Your approval is not required for this review" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changesThatRequireUserApproval.length != filteredChangesByIds.length) {
|
||||||
|
throw BadRequestError({ message: "You may only request to approve changes that require your approval" })
|
||||||
|
}
|
||||||
|
|
||||||
|
changesThatRequireUserApproval.forEach((requestedChange) => {
|
||||||
|
const overallChangeStatus = requestedChange.status
|
||||||
|
const currentLoggedInUserId = req.user._id.toString()
|
||||||
|
if (overallChangeStatus == ApprovalStatus.PENDING.toString()) {
|
||||||
|
requestedChange.approvers.forEach((approver) => {
|
||||||
|
if (approver.userId.toString() == currentLoggedInUserId && approver.status == ApprovalStatus.PENDING.toString()) {
|
||||||
|
approver.status = ApprovalStatus.APPROVED
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let updateOverallStatusToApproved = true
|
||||||
|
requestedChange.approvers.forEach((approver) => {
|
||||||
|
if (approver.status != ApprovalStatus.APPROVED.toString()) {
|
||||||
|
updateOverallStatusToApproved = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (updateOverallStatusToApproved) {
|
||||||
|
requestedChange.status = ApprovalStatus.APPROVED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedApprovalRequest = await SecretApprovalRequest.findByIdAndUpdate(reviewId, {
|
||||||
|
requestedChanges: requestedChangesFromDB
|
||||||
|
}, { new: true }).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||||
|
|
||||||
|
res.send({ approvalRequest: updatedApprovalRequest })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const rejectApprovalRequest = async (req: Request, res: Response) => {
|
||||||
|
const { requestedChangeIds } = req.body;
|
||||||
|
const { reviewId } = req.params
|
||||||
|
|
||||||
|
const approvalRequestFromDB = await SecretApprovalRequest.findById(reviewId)
|
||||||
|
if (!approvalRequestFromDB) {
|
||||||
|
throw ResourceNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedChangesFromDB: IRequestedChange[] = approvalRequestFromDB.requestedChanges
|
||||||
|
const filteredChangesByIds = requestedChangesFromDB.filter(change => requestedChangeIds.includes(change._id.toString()))
|
||||||
|
if (filteredChangesByIds.length != requestedChangeIds.length) {
|
||||||
|
throw BadRequestError({ message: "All requestedChangeIds should exist in this approval request" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const changesThatRequireUserApproval = _.filter(filteredChangesByIds, change => {
|
||||||
|
return _.some(change.approvers, approver => {
|
||||||
|
return approver.userId.toString() == req.user._id.toString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!changesThatRequireUserApproval.length) {
|
||||||
|
throw UnauthorizedRequestError({ message: "Your approval is not required for this review" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changesThatRequireUserApproval.length != filteredChangesByIds.length) {
|
||||||
|
throw BadRequestError({ message: "You may only request to reject changes that require your approval" })
|
||||||
|
}
|
||||||
|
|
||||||
|
changesThatRequireUserApproval.forEach((requestedChange) => {
|
||||||
|
const overallChangeStatus = requestedChange.status
|
||||||
|
const currentLoggedInUserId = req.user._id.toString()
|
||||||
|
if (overallChangeStatus == ApprovalStatus.PENDING.toString()) {
|
||||||
|
requestedChange.approvers.forEach((approver) => {
|
||||||
|
if (approver.userId.toString() == currentLoggedInUserId && approver.status == ApprovalStatus.PENDING.toString()) {
|
||||||
|
approver.status = ApprovalStatus.REJECTED
|
||||||
|
requestedChange.status = ApprovalStatus.REJECTED
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedApprovalRequest = await SecretApprovalRequest.findByIdAndUpdate(reviewId, {
|
||||||
|
requestedChanges: requestedChangesFromDB
|
||||||
|
}, { new: true }).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||||
|
|
||||||
|
res.send({ approvalRequest: updatedApprovalRequest })
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mergeApprovalRequestSecrets = async (req: Request, res: Response) => {
|
||||||
|
const { requestedChangeIds } = req.body;
|
||||||
|
const { reviewId } = req.params
|
||||||
|
|
||||||
|
// only the user who requested the set of changes can merge it
|
||||||
|
const approvalRequestFromDB = await SecretApprovalRequest.findOne({ _id: reviewId, requestedByUserId: req.user._id })
|
||||||
|
if (!approvalRequestFromDB) {
|
||||||
|
throw ResourceNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that this user is a member of this workspace
|
||||||
|
const membershipDetails = await Membership.find({ user: req.user._id, workspace: approvalRequestFromDB.workspace })
|
||||||
|
if (!membershipDetails) {
|
||||||
|
throw UnauthorizedRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter not merged, approved, and change ids specified in this request
|
||||||
|
const filteredChangesToMerge: IRequestedChange[] = approvalRequestFromDB.requestedChanges.filter(change => change.merged == false && change.status == ApprovalStatus.APPROVED && requestedChangeIds.includes(change._id.toString()))
|
||||||
|
|
||||||
|
if (filteredChangesToMerge.length != requestedChangeIds.length) {
|
||||||
|
throw BadRequestError({ message: "One or more changes in this approval is either already merged/not approved or do not exist" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretsToCreate: ISecret[] = []
|
||||||
|
const secretsToUpdate: any[] = []
|
||||||
|
const secretsIdsToDelete: any[] = []
|
||||||
|
const secretIdsToModify: any[] = []
|
||||||
|
|
||||||
|
filteredChangesToMerge.forEach((requestedChange: any) => {
|
||||||
|
const overallChangeStatus = requestedChange.status
|
||||||
|
const currentLoggedInUserId = req.user._id.toString()
|
||||||
|
if (overallChangeStatus == ApprovalStatus.APPROVED.toString()) {
|
||||||
|
if (ChangeType.CREATE.toString() == requestedChange.type) {
|
||||||
|
const modifiedSecret = requestedChange.modifiedSecretDetails.toObject()
|
||||||
|
|
||||||
|
secretsToCreate.push({
|
||||||
|
...modifiedSecret,
|
||||||
|
user: requestedChange.modifiedSecretDetails.type === SECRET_PERSONAL ? currentLoggedInUserId : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ChangeType.UPDATE.toString() == requestedChange.type) {
|
||||||
|
const modifiedSecret = requestedChange.modifiedSecretDetails.toObject()
|
||||||
|
secretIdsToModify.push(requestedChange.modifiedSecretParentId)
|
||||||
|
|
||||||
|
secretsToUpdate.push({
|
||||||
|
filter: { _id: requestedChange.modifiedSecretParentId },
|
||||||
|
update: {
|
||||||
|
$set: {
|
||||||
|
...modifiedSecret,
|
||||||
|
user: requestedChange.modifiedSecretDetails.type === SECRET_PERSONAL ? currentLoggedInUserId : undefined,
|
||||||
|
},
|
||||||
|
$inc: {
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ChangeType.DELETE.toString() == requestedChange.type) {
|
||||||
|
secretsIdsToDelete.push({
|
||||||
|
_id: requestedChange.modifiedSecretParentId.toString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedChange.merged = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ensure all secrets that are to be updated exist
|
||||||
|
const numSecretsFromDBThatRequireUpdate = await Secret.countDocuments({ _id: { $in: secretIdsToModify } });
|
||||||
|
const numSecretsFromDBThatRequireDelete = await Secret.countDocuments({ _id: { $in: secretsIdsToDelete } });
|
||||||
|
|
||||||
|
if (numSecretsFromDBThatRequireUpdate != secretIdsToModify.length || numSecretsFromDBThatRequireDelete != secretsIdsToDelete.length) {
|
||||||
|
throw BadRequestError({ message: "You cannot merge changes for secrets that no longer exist" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add add CRUD operations into a single list of operations
|
||||||
|
const allOperationsForBulkWrite: any[] = [];
|
||||||
|
|
||||||
|
for (const updateStatement of secretsToUpdate) {
|
||||||
|
allOperationsForBulkWrite.push({ updateOne: updateStatement });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const secretId of secretsIdsToDelete) {
|
||||||
|
allOperationsForBulkWrite.push({ deleteOne: { filter: { _id: secretId } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const createStatement of secretsToCreate) {
|
||||||
|
allOperationsForBulkWrite.push({ insertOne: { document: createStatement } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// start transaction
|
||||||
|
const session = await mongoose.startSession();
|
||||||
|
session.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Secret.bulkWrite(allOperationsForBulkWrite);
|
||||||
|
await SecretApprovalRequest.updateOne({ _id: reviewId, 'requestedChanges._id': { $in: requestedChangeIds } },
|
||||||
|
{ $set: { 'requestedChanges.$.merged': true } })
|
||||||
|
|
||||||
|
const updatedApproval = await SecretApprovalRequest.findById(reviewId).populate(["requestedChanges.modifiedSecretParentId", { path: 'requestedChanges.approvers.userId', select: 'firstName lastName _id' }])
|
||||||
|
|
||||||
|
res.send(updatedApproval)
|
||||||
|
} catch (error) {
|
||||||
|
await session.abortTransaction();
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
session.endSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@ -16,7 +16,8 @@ import {
|
|||||||
} from "../../helpers/workspace";
|
} from "../../helpers/workspace";
|
||||||
import { addMemberships } from "../../helpers/membership";
|
import { addMemberships } from "../../helpers/membership";
|
||||||
import { ADMIN } from "../../variables";
|
import { ADMIN } from "../../variables";
|
||||||
|
import { BadRequestError, ResourceNotFound, UnauthorizedRequestError } from "../../utils/errors";
|
||||||
|
import _ from "lodash";
|
||||||
/**
|
/**
|
||||||
* Return public keys of members of workspace with id [workspaceId]
|
* Return public keys of members of workspace with id [workspaceId]
|
||||||
* @param req
|
* @param req
|
||||||
@ -303,6 +304,112 @@ export const getWorkspaceIntegrationAuthorizations = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addApproverForWorkspaceAndEnvironment = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
interface Approver {
|
||||||
|
environment: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { approvers }: { approvers: Approver[] } = req.body;
|
||||||
|
|
||||||
|
const workspaceFromDB = await Workspace.findById(workspaceId)
|
||||||
|
|
||||||
|
if (!workspaceFromDB) {
|
||||||
|
throw ResourceNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const allAvailableWorkspaceEnvironments = _.map(workspaceFromDB.environments, 'slug');
|
||||||
|
const environmentsFromApprovers = _.map(approvers, "environment")
|
||||||
|
const filteredApprovers = environmentsFromApprovers.map(environment => allAvailableWorkspaceEnvironments.includes(environment))
|
||||||
|
|
||||||
|
// validate environments
|
||||||
|
if (filteredApprovers.length != environmentsFromApprovers.length) {
|
||||||
|
const err = `One or more environments set for approver(s) is invalid`
|
||||||
|
throw BadRequestError({ message: err })
|
||||||
|
}
|
||||||
|
|
||||||
|
const approverIds = _.map(approvers, "userId")
|
||||||
|
|
||||||
|
// validate approvers membership
|
||||||
|
const approversMemberships = await Membership.find({
|
||||||
|
workspace: workspaceId,
|
||||||
|
user: { $in: approverIds }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!approversMemberships) {
|
||||||
|
throw ResourceNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approversMemberships.length != approverIds.length) {
|
||||||
|
throw UnauthorizedRequestError({ message: "Approvers must be apart of the workspace they are being added to" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedWorkspace = await Workspace.findByIdAndUpdate(workspaceId,
|
||||||
|
{
|
||||||
|
$addToSet: {
|
||||||
|
approvers: {
|
||||||
|
$each: approvers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { new: true })
|
||||||
|
|
||||||
|
return res.json(updatedWorkspace)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const removeApproverForWorkspaceAndEnvironment = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
interface Approver {
|
||||||
|
environment: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { approvers }: { approvers: Approver[] } = req.body;
|
||||||
|
|
||||||
|
const workspaceFromDB = await Workspace.findById(workspaceId)
|
||||||
|
|
||||||
|
if (!workspaceFromDB) {
|
||||||
|
throw ResourceNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const allAvailableWorkspaceEnvironments = _.map(workspaceFromDB.environments, 'slug');
|
||||||
|
const environmentsFromApprovers = _.map(approvers, "environment")
|
||||||
|
const filteredApprovers = environmentsFromApprovers.map(environment => allAvailableWorkspaceEnvironments.includes(environment))
|
||||||
|
|
||||||
|
// validate environments
|
||||||
|
if (filteredApprovers.length != environmentsFromApprovers.length) {
|
||||||
|
const err = `One or more environments set for approver(s) is invalid`
|
||||||
|
throw BadRequestError({ message: err })
|
||||||
|
}
|
||||||
|
|
||||||
|
const approverIds = _.map(approvers, "userId")
|
||||||
|
|
||||||
|
// validate approvers membership
|
||||||
|
const approversMemberships = await Membership.find({
|
||||||
|
workspace: workspaceId,
|
||||||
|
user: { $in: approverIds }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!approversMemberships) {
|
||||||
|
throw ResourceNotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approversMemberships.length != approverIds.length) {
|
||||||
|
throw UnauthorizedRequestError({ message: "Approvers must be apart of the workspace they are being added to" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedWorkspace = await Workspace.findByIdAndUpdate(workspaceId, { $pullAll: { approvers: approvers } }, { new: true })
|
||||||
|
|
||||||
|
return res.json(updatedWorkspace)
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return service service tokens for workspace [workspaceId] belonging to user
|
* Return service service tokens for workspace [workspaceId] belonging to user
|
||||||
* @param req
|
* @param req
|
||||||
|
@ -713,10 +713,27 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
|||||||
return reformatedSecrets;
|
return reformatedSecrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const secretObjectHasRequiredFields = (secretObject: ISecret) => {
|
||||||
|
if (!secretObject.type ||
|
||||||
|
!(secretObject.type === SECRET_PERSONAL || secretObject.type === SECRET_SHARED) ||
|
||||||
|
!secretObject.secretKeyCiphertext ||
|
||||||
|
!secretObject.secretKeyIV ||
|
||||||
|
!secretObject.secretKeyTag ||
|
||||||
|
(typeof secretObject.secretValueCiphertext !== 'string') ||
|
||||||
|
!secretObject.secretValueIV ||
|
||||||
|
!secretObject.secretValueTag) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
validateSecrets,
|
validateSecrets,
|
||||||
v1PushSecrets,
|
v1PushSecrets,
|
||||||
v2PushSecrets,
|
v2PushSecrets,
|
||||||
pullSecrets,
|
pullSecrets,
|
||||||
reformatPullSecrets
|
reformatPullSecrets,
|
||||||
|
secretObjectHasRequiredFields
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { ErrorRequestHandler } from "express";
|
import { ErrorRequestHandler } from "express";
|
||||||
|
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { InternalServerError, UnauthorizedRequestError } from "../utils/errors";
|
import { InternalServerError, UnauthorizedRequestError, UnprocessableEntityError } from "../utils/errors";
|
||||||
import { getLogger } from "../utils/logger";
|
import { getLogger } from "../utils/logger";
|
||||||
import RequestError, { LogLevel } from "../utils/requestError";
|
import RequestError, { LogLevel } from "../utils/requestError";
|
||||||
import { NODE_ENV } from "../config";
|
import { NODE_ENV } from "../config";
|
||||||
|
|
||||||
import { TokenExpiredError } from 'jsonwebtoken';
|
import mongoose from "mongoose";
|
||||||
|
|
||||||
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
|
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
|
||||||
if (res.headersSent) return next();
|
if (res.headersSent) return next();
|
||||||
@ -35,3 +35,16 @@ export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | E
|
|||||||
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
|
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const handleMongoInvalidDataError = (err: any, req: any, res: any, next: any) => {
|
||||||
|
if (err instanceof mongoose.Error.ValidationError) {
|
||||||
|
const errors: any = {};
|
||||||
|
for (const field in err.errors) {
|
||||||
|
errors[field] = err.errors[field].message;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw UnprocessableEntityError({ message: JSON.stringify(errors) })
|
||||||
|
} else {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ export interface ISecret {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretSchema = new Schema<ISecret>(
|
export const secretSchema = new Schema<ISecret>(
|
||||||
{
|
{
|
||||||
version: {
|
version: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
import mongoose, { Schema, model } from 'mongoose';
|
import mongoose, { Schema, model } from 'mongoose';
|
||||||
import Secret, { ISecret } from './secret';
|
import Secret, { ISecret, secretSchema } from './secret';
|
||||||
|
|
||||||
|
export interface IRequestedChange {
|
||||||
|
_id: string
|
||||||
|
userId: mongoose.Types.ObjectId;
|
||||||
|
status: ApprovalStatus;
|
||||||
|
modifiedSecretDetails: ISecret,
|
||||||
|
modifiedSecretParentId: mongoose.Types.ObjectId,
|
||||||
|
type: string,
|
||||||
|
approvers: IApprover[]
|
||||||
|
merged: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface ISecretApprovalRequest {
|
interface ISecretApprovalRequest {
|
||||||
secret: mongoose.Types.ObjectId;
|
environment: string;
|
||||||
requestedChanges: ISecret;
|
workspace: mongoose.Types.ObjectId;
|
||||||
requestedBy: mongoose.Types.ObjectId;
|
requestedChanges: IRequestedChange[];
|
||||||
approvers: IApprover[];
|
requestedByUserId: mongoose.Types.ObjectId;
|
||||||
status: ApprovalStatus;
|
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
requestType: RequestType;
|
requestType: ChangeType;
|
||||||
requestId: string;
|
requestId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IApprover {
|
export interface IApprover {
|
||||||
userId: mongoose.Types.ObjectId;
|
userId: mongoose.Types.ObjectId;
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
}
|
}
|
||||||
@ -23,54 +33,80 @@ export enum ApprovalStatus {
|
|||||||
REJECTED = 'rejected'
|
REJECTED = 'rejected'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RequestType {
|
export enum ChangeType {
|
||||||
UPDATE = 'update',
|
UPDATE = 'update',
|
||||||
DELETE = 'delete',
|
DELETE = 'delete',
|
||||||
CREATE = 'create'
|
CREATE = 'create'
|
||||||
}
|
}
|
||||||
|
|
||||||
const approverSchema = new mongoose.Schema({
|
const approverSchema = new mongoose.Schema({
|
||||||
user: {
|
userId: {
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
ref: 'User',
|
ref: 'User',
|
||||||
required: true
|
required: false,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [ApprovalStatus],
|
enum: [ApprovalStatus],
|
||||||
default: ApprovalStatus.PENDING
|
default: ApprovalStatus.PENDING
|
||||||
}
|
}
|
||||||
|
}, { timestamps: true });
|
||||||
|
|
||||||
|
|
||||||
|
// extend the Secret Schema by taking all but removing _id and version fields
|
||||||
|
const SecretModificationSchema = new Schema({
|
||||||
|
...secretSchema.obj,
|
||||||
|
}, {
|
||||||
|
_id: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
SecretModificationSchema.remove("version")
|
||||||
|
|
||||||
|
|
||||||
|
const requestedChangeSchema = new mongoose.Schema(
|
||||||
{
|
{
|
||||||
secret: {
|
_id: { type: mongoose.Schema.Types.ObjectId, auto: true },
|
||||||
|
modifiedSecretDetails: SecretModificationSchema,
|
||||||
|
modifiedSecretParentId: { // used to fetch the current version of this secret for comparing
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
ref: 'Secret'
|
ref: 'Secret'
|
||||||
},
|
},
|
||||||
requestedChanges: Secret,
|
type: {
|
||||||
requestedBy: {
|
type: String,
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
enum: ChangeType,
|
||||||
ref: 'User'
|
required: true
|
||||||
},
|
},
|
||||||
approvers: [approverSchema],
|
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ApprovalStatus,
|
enum: ApprovalStatus,
|
||||||
default: ApprovalStatus.PENDING
|
default: ApprovalStatus.PENDING // the overall status of the requested change
|
||||||
},
|
},
|
||||||
timestamp: {
|
approvers: [approverSchema],
|
||||||
type: Date,
|
merged: {
|
||||||
default: Date.now
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
requestType: {
|
{ timestamps: true }
|
||||||
type: String,
|
);
|
||||||
enum: RequestType,
|
|
||||||
required: true
|
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||||
|
{
|
||||||
|
environment: {
|
||||||
|
type: String, // The secret changes were requested for
|
||||||
|
ref: 'Secret'
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId, // workspace id of the secret
|
||||||
|
ref: 'Workspace'
|
||||||
|
},
|
||||||
|
requestedChanges: [requestedChangeSchema], // the changes that the requested user wants to make to the existing secret
|
||||||
|
requestedByUserId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
},
|
},
|
||||||
requestId: {
|
requestId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -78,6 +114,8 @@ const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const SecretApprovalRequest = model<ISecretApprovalRequest>('SecretApprovalRequest', secretApprovalRequestSchema);
|
secretApprovalRequestSchema.index({ 'requestedChanges.approvers.userId': 1 });
|
||||||
|
|
||||||
|
const SecretApprovalRequest = model<ISecretApprovalRequest>('secret_approval_request', secretApprovalRequestSchema);
|
||||||
|
|
||||||
export default SecretApprovalRequest;
|
export default SecretApprovalRequest;
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import mongoose, { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
|
||||||
|
export interface DesignatedApprovers {
|
||||||
|
environment: string,
|
||||||
|
approvers: [mongoose.Schema.Types.ObjectId]
|
||||||
|
}
|
||||||
|
|
||||||
export interface IWorkspace {
|
export interface IWorkspace {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
name: string;
|
name: string;
|
||||||
organization: Types.ObjectId;
|
organization: Types.ObjectId;
|
||||||
|
approvers: [DesignatedApprovers];
|
||||||
environments: Array<{
|
environments: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@ -11,6 +18,16 @@ export interface IWorkspace {
|
|||||||
autoCapitalization: boolean;
|
autoCapitalization: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const approverSchema = new mongoose.Schema({
|
||||||
|
userId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
}, { _id: false });
|
||||||
|
|
||||||
const workspaceSchema = new Schema<IWorkspace>({
|
const workspaceSchema = new Schema<IWorkspace>({
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -20,6 +37,7 @@ const workspaceSchema = new Schema<IWorkspace>({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
approvers: [approverSchema],
|
||||||
organization: {
|
organization: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Organization',
|
ref: 'Organization',
|
||||||
|
@ -15,6 +15,7 @@ import password from './password';
|
|||||||
import stripe from './stripe';
|
import stripe from './stripe';
|
||||||
import integration from './integration';
|
import integration from './integration';
|
||||||
import integrationAuth from './integrationAuth';
|
import integrationAuth from './integrationAuth';
|
||||||
|
import secretApprovalRequest from './secretApprovalsRequest'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
signup,
|
signup,
|
||||||
@ -33,5 +34,6 @@ export {
|
|||||||
password,
|
password,
|
||||||
stripe,
|
stripe,
|
||||||
integration,
|
integration,
|
||||||
integrationAuth
|
integrationAuth,
|
||||||
|
secretApprovalRequest
|
||||||
};
|
};
|
||||||
|
65
backend/src/routes/v1/secretApprovalsRequest.ts
Normal file
65
backend/src/routes/v1/secretApprovalsRequest.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { requireAuth, validateRequest } from '../../middleware';
|
||||||
|
import { secretApprovalController } from '../../controllers/v1';
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
body('workspaceId').exists(),
|
||||||
|
body('environment').exists(),
|
||||||
|
body('requestedChanges').isArray(),
|
||||||
|
validateRequest,
|
||||||
|
secretApprovalController.createApprovalRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/sent',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
secretApprovalController.getAllApprovalRequestsForUser
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/approvals-needed',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
secretApprovalController.getAllApprovalRequestsThatRequireUserApproval
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:reviewId/approve',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
body('requestedChangeIds').isArray(),
|
||||||
|
validateRequest,
|
||||||
|
secretApprovalController.approveApprovalRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:reviewId/reject',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
body('requestedChangeIds').isArray(),
|
||||||
|
validateRequest,
|
||||||
|
secretApprovalController.rejectApprovalRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:reviewId/merge',
|
||||||
|
body('requestedChangeIds').isArray(),
|
||||||
|
validateRequest,
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
secretApprovalController.mergeApprovalRequestSecrets
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@ -134,6 +134,34 @@ router.get(
|
|||||||
workspaceController.getWorkspaceIntegrationAuthorizations
|
workspaceController.getWorkspaceIntegrationAuthorizations
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:workspaceId/approvers',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
body("approvers").isArray(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.addApproverForWorkspaceAndEnvironment
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:workspaceId/approvers',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
body("approvers").isArray(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.removeApproverForWorkspaceAndEnvironment
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:workspaceId/service-tokens', // deprecate
|
'/:workspaceId/service-tokens', // deprecate
|
||||||
requireAuth({
|
requireAuth({
|
||||||
|
@ -19,6 +19,15 @@ export const MethodNotAllowedError = (error?: Partial<RequestErrorContext>) => n
|
|||||||
stack: error?.stack
|
stack: error?.stack
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const UnprocessableEntityError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||||
|
statusCode: error?.statusCode ?? 422,
|
||||||
|
type: error?.type ?? 'unprocessable_entity',
|
||||||
|
message: error?.message ?? 'The server understands the content of the request, but it was unable to process it because it contains invalid data',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
});
|
||||||
|
|
||||||
export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
export const UnauthorizedRequestError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
logLevel: error?.logLevel ?? LogLevel.INFO,
|
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||||
statusCode: error?.statusCode ?? 401,
|
statusCode: error?.statusCode ?? 401,
|
||||||
@ -46,6 +55,15 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
|
|||||||
stack: error?.stack
|
stack: error?.stack
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ResourceNotFound = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
|
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||||
|
statusCode: error?.statusCode ?? 404,
|
||||||
|
type: error?.type ?? 'resource_not_found',
|
||||||
|
message: error?.message ?? 'The requested resource was not found',
|
||||||
|
context: error?.context,
|
||||||
|
stack: error?.stack
|
||||||
|
});
|
||||||
|
|
||||||
export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||||
statusCode: error?.statusCode ?? 500,
|
statusCode: error?.statusCode ?? 500,
|
||||||
|
Reference in New Issue
Block a user