feat(secret-approval): implemented the new policy based approval system bare version

This commit is contained in:
Akhil Mohan
2023-10-05 20:34:50 +05:30
parent fc43511f5d
commit 63588b4e44
27 changed files with 1033 additions and 165 deletions
@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import {
ProjectPermissionActions,
@@ -7,6 +7,7 @@ import {
} from "../../ee/services/ProjectRoleService";
import { validateRequest } from "../../helpers/validation";
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
import { getSecretPolicyOfBoard } from "../../services/SecretApprovalService";
import { BadRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/secretApproval";
@@ -107,3 +108,18 @@ export const getSecretApprovalPolicy = async (req: Request, res: Response) => {
approvals: doc
});
};
export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response) => {
const {
query: { workspaceId, environment, secretPath }
} = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
);
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
return res.send({ policy: secretApprovalPolicy });
};
@@ -2,11 +2,12 @@ import { Request, Response } from "express";
import { getUserProjectPermissions } from "../../ee/services/ProjectRoleService";
import { validateRequest } from "../../helpers/validation";
import { Folder } from "../../models";
import { SecretApprovalRequest } from "../../models/secretApprovalRequest";
import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest";
import * as reqValidator from "../../validation/secretApprovalRequest";
import { getFolderWithPathFromId } from "../../services/FolderService";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ISecretApprovalPolicy } from "../../models/secretApprovalPolicy";
import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService";
export const getSecretApprovalRequests = async (req: Request, res: Response) => {
const {
@@ -29,6 +30,7 @@ export const getSecretApprovalRequests = async (req: Request, res: Response) =>
([key, value]) => value === undefined && delete query[key as keyof typeof query]
);
const approvalRequests = await SecretApprovalRequest.find(query)
.sort({ createdAt: -1 })
.limit(limit)
.skip(offset)
.populate("policy")
@@ -64,11 +66,12 @@ export const getSecretApprovalRequestDetails = async (req: Request, res: Respons
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate("policy")
.populate({
path: "commits.secret",
path: "commits.secretVersion",
populate: {
path: "tags"
}
})
.populate("commits.secret", "version")
.populate("commits.newVersion.tags");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
@@ -126,3 +129,43 @@ export const updateSecretApprovalRequestStatus = async (req: Request, res: Respo
return res.send({ status });
};
export const mergeSecretApprovalRequest = async (req: Request, res: Response) => {
const {
body: { id }
} = await validateRequest(reqValidator.mergeSecretApprovalRequest, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId === membership.id)
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }),
{}
);
const hasMinApproval =
secretApprovalRequest.policy.approvals <=
secretApprovalRequest.policy.approvers.filter(
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
).length;
if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" });
const approval = await performSecretApprovalRequestMerge(id, req.authData);
return res.send({ approval });
};
@@ -596,7 +596,7 @@ export const createSecret = async (req: Request, res: Response) => {
});
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) {
if (secretApprovalPolicy && membership && type !== "personal") {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
@@ -701,7 +701,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
});
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) {
if (secretApprovalPolicy && membership && type !== "personal") {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
@@ -712,6 +712,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
[CommitType.UPDATE]: [
{
secretName,
newSecretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
@@ -784,7 +785,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
});
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) {
if (secretApprovalPolicy && membership && type !== "personal") {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
@@ -846,7 +847,7 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.CREATE]: secrets
[CommitType.CREATE]: secrets.filter(({ type }) => type === "shared")
}
});
return res.send({ approval: secretApprovalRequest });
@@ -887,7 +888,7 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.UPDATE]: secrets
[CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared")
}
});
return res.send({ approval: secretApprovalRequest });
@@ -928,7 +929,7 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: secrets
[CommitType.DELETE]: secrets.filter(({ type }) => type === "shared")
}
});
return res.send({ approval: secretApprovalRequest });
+32 -16
View File
@@ -32,6 +32,25 @@ export interface ISecretApprovalSecChange {
tags?: string[];
}
export type ISecretCommits<T = Types.ObjectId, J = Types.ObjectId> = Array<
| {
newVersion: ISecretApprovalSecChange;
op: CommitType.CREATE;
}
| {
// secret is recorded to get the latest version, we can keep ref to secret for pulling change as it will also get changed
// on merge
secretVersion: J;
secret: T;
newVersion: Partial<Omit<ISecretApprovalSecChange, "_id">> & { _id: Types.ObjectId };
op: CommitType.UPDATE;
}
| {
secret: T;
secretVersion: J;
op: CommitType.DELETE;
}
>;
export interface ISecretApprovalRequest {
_id: Types.ObjectId;
committer: Types.ObjectId;
@@ -45,21 +64,8 @@ export interface ISecretApprovalRequest {
hasMerged: boolean;
status: "open" | "close";
policy: Types.ObjectId;
commits: Array<
| {
newVersion: ISecretApprovalSecChange;
op: CommitType.CREATE;
}
| {
secret: Types.ObjectId;
newVersion: Partial<ISecretApprovalSecChange>;
op: CommitType.UPDATE;
}
| {
secret: Types.ObjectId;
op: CommitType.DELETE;
}
>;
commits: ISecretCommits;
conflicts: Array<{ secretId: string; op: CommitType }>;
}
const secretApprovalSecretChangeSchema = new Schema<ISecretApprovalSecChange>({
@@ -157,9 +163,19 @@ const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
{
secret: { type: Types.ObjectId, ref: "Secret" },
newVersion: secretApprovalSecretChangeSchema,
secretVersion: { type: Types.ObjectId, ref: "SecretVersion" },
op: { type: String, enum: [CommitType], required: true }
}
]
],
conflicts: {
type: [
{
secretId: { type: String, required: true },
op: { type: String, enum: [CommitType], required: true }
}
],
default: []
}
},
{
timestamps: true
@@ -12,6 +12,14 @@ router.get(
secretApprovalPolicyController.getSecretApprovalPolicy
);
router.get(
"/board",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalPolicyController.getSecretApprovalPolicyOfBoard
);
router.post(
"/",
requireAuth({
@@ -20,6 +20,14 @@ router.get(
secretApprovalRequestController.getSecretApprovalRequestDetails
);
router.post(
"/merge",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.mergeSecretApprovalRequest
);
router.post(
"/:id",
requireAuth({
+432 -34
View File
@@ -11,11 +11,16 @@ import {
CommitType,
ISecretApprovalRequest,
ISecretApprovalSecChange,
ISecretCommits,
SecretApprovalRequest
} from "../models/secretApprovalRequest";
import { BadRequestError } from "../utils/errors";
import { getFolderByPath } from "./FolderService";
import { SECRET_SHARED } from "../variables";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_SHARED } from "../variables";
import TelemetryService from "./TelemetryService";
import { EEAuditLogService, EESecretService } from "../ee/services";
import { EventType, SecretVersion } from "../ee/models";
import { AuthData } from "../interfaces/middleware";
// if glob pattern score is 1, if not exist score is 0 and if its not both then its exact path meaning score 2
const getPolicyScore = (policy: ISecretApprovalPolicy) =>
@@ -43,6 +48,35 @@ export const getSecretPolicyOfBoard = async (
return finalPolicy;
};
const getLatestSecretVersion = async (secretIds: Types.ObjectId[]) => {
const latestSecretVersions = await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
},
type: SECRET_SHARED
}
},
{
$sort: { version: -1 }
},
{
$group: {
_id: "$secret",
version: { $max: "$version" },
versionId: { $max: "$_id" }, // id of latest secret versionId
secret: { $first: "$$ROOT" }
}
}
]).exec();
// reduced with secret id and latest version as document
return latestSecretVersions.reduce(
(prev, curr) => ({ ...prev, [curr._id.toString()]: curr.secret }),
{}
);
};
type TApprovalCreateSecret = Omit<ISecretApprovalSecChange, "_id" | "version"> & {
secretName: string;
};
@@ -50,6 +84,7 @@ type TApprovalUpdateSecret = Partial<Omit<ISecretApprovalSecChange, "_id" | "ver
secretName: string;
newSecretName?: string;
};
type TGenerateSecretApprovalRequestArg = {
workspaceId: string;
environment: string;
@@ -151,22 +186,21 @@ export const generateSecretApprovalRequest = async ({
}, {})
);
// check update secret exists
const secretsToBeUpdated = await Secret.find({
const oldSecrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
environment,
type: SECRET_SHARED,
secretBlindIndex: {
$in: updatedSecret.map(({ secretName }) => secretBlindIndexes[secretName])
}
})
.select("+secretBlindIndex")
.or(
updatedSecret.map(({ secretName }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: SECRET_SHARED
}))
)
.lean()
.exec();
if (secretsToBeUpdated.length !== updatedSecret.length)
if (oldSecrets.length !== updatedSecret.length)
throw BadRequestError({ message: "Secrets already exist" });
// finally check updating blindindex exist
const nameUpdatedSecrets = updatedSecret.filter(({ newSecretName }) => Boolean(newSecretName));
const newSecretBlindIndexes = await Promise.all(
@@ -188,24 +222,29 @@ export const generateSecretApprovalRequest = async ({
environment,
secretBlindIndex: { $in: Object.values(newSecretBlindIndexes) }
});
if (doesAnySecretExistWithNewIndex)
if (doesAnySecretExistWithNewIndex.length)
throw BadRequestError({ message: "Secret with new name already exist" });
const oldSecretsGroupById = oldSecrets.reduce<Record<string, ISecret>>(
(prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }),
{}
);
const latestSecretVersions = await getLatestSecretVersion(
updatedSecret.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id)
);
commits.push(
...updatedSecret.map((el) => {
const oldSecret = secretsToBeUpdated.find(
(sec) => sec?.secretBlindIndex === secretBlindIndexes[el.secretName]
);
if (!oldSecret) throw BadRequestError({ message: "Secret not found" });
const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id;
return {
op: CommitType.UPDATE as const,
secret: oldSecret._id,
secret: secretId,
secretVersion: latestSecretVersions[secretId.toString()]._id,
newVersion: {
...el,
secretBlindIndex: newSecretBlindIndexes?.[el.secretName],
_id: new Types.ObjectId(),
version: oldSecret.version || 1
version: oldSecretsGroupById[secretBlindIndexes[el.secretName]].version || 1
}
};
})
@@ -233,29 +272,35 @@ export const generateSecretApprovalRequest = async ({
const secretsToDelete = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
environment,
type: SECRET_SHARED,
secretBlindIndex: {
$in: deletedSecrets.map(({ secretName }) => secretBlindIndexes[secretName])
}
})
.or(
deletedSecrets.map(({ secretName }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: SECRET_SHARED
}))
)
.select({ secretBlindIndexes: 1 })
.select({ secretBlindIndex: 1, _id: 1 })
.lean()
.exec();
if (secretsToDelete.length !== deletedSecrets.length)
throw BadRequestError({ message: "Deleted secrets not found" });
const oldSecretsGroupById = secretsToDelete.reduce<Record<string, ISecret>>(
(prev, curr) => ({ ...prev, [curr?.secretBlindIndex || ""]: curr }),
{}
);
const latestSecretVersions = await getLatestSecretVersion(
deletedSecrets.map((el) => oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id)
);
commits.push(
...deletedSecrets.map((el) => ({
op: CommitType.DELETE as const,
secret: (
secretsToDelete.find(
(sec) => sec?.secretBlindIndex === secretBlindIndexes[el.secretName]
) as ISecret
)._id
}))
...deletedSecrets.map((el) => {
const secretId = oldSecretsGroupById[secretBlindIndexes[el.secretName]]._id;
return {
op: CommitType.DELETE as const,
secret: secretId,
secretVersion: latestSecretVersions[secretId.toString()]
};
})
);
}
@@ -270,3 +315,356 @@ export const generateSecretApprovalRequest = async ({
await secretApprovalRequest.save();
return secretApprovalRequest;
};
// validation for a merge conditions happen in another function in controller
export const performSecretApprovalRequestMerge = async (id: string, authData: AuthData) => {
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate<{ commits: ISecretCommits<ISecret> }>({
path: "commits.secret",
select: "+secretBlindIndex",
populate: {
path: "tags"
}
})
.select("+commits.newVersion.secretBlindIndex");
if (!secretApprovalRequest) throw BadRequestError({ message: "Approval request not found" });
const workspaceId = secretApprovalRequest.workspace;
const environment = secretApprovalRequest.environment;
const folderId = secretApprovalRequest.folderId;
const postHogClient = await TelemetryService.getPostHogClient();
const conflicts: Array<{ id: string; op: CommitType }> = [];
const secretCreationCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.CREATE
) as Array<{ op: CommitType.CREATE; newVersion: ISecretApprovalSecChange }>;
if (secretCreationCommits.length) {
// the created secrets already exist thus creation conflict ones
const conflictedSecrets = await Secret.find({
workspace: workspaceId,
environment,
folder: folderId,
secretBlindIndex: {
$in: secretCreationCommits.map(({ newVersion }) => newVersion.secretBlindIndex)
}
})
.select("+secretBlindIndex")
.lean();
const conflictGroupByBlindIndex = conflictedSecrets.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.secretBlindIndex || ""]: true }),
{}
);
const nonConflictSecrets = secretCreationCommits.filter(
({ newVersion }) => !conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""]
);
secretCreationCommits
.filter(({ newVersion }) => conflictGroupByBlindIndex[newVersion.secretBlindIndex || ""])
.forEach((el) => {
conflicts.push({ op: CommitType.CREATE, id: el.newVersion._id.toString() });
});
// create secret
const newlyCreatedSecrets: ISecret[] = await Secret.insertMany(
nonConflictSecrets.map(
({
newVersion: {
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
algorithm,
keyEncoding,
tags
}
}) => ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_SHARED,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
folder: folderId,
algorithm: algorithm || ALGORITHM_AES_256_GCM,
keyEncoding: keyEncoding || ENCODING_SCHEME_UTF8,
tags,
skipMultilineEncoding,
secretBlindIndex
})
)
);
await EESecretService.addSecretVersions({
secretVersions: newlyCreatedSecrets.map(
(secret) =>
new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type: secret.type,
folder: folderId,
tags: secret.tags,
skipMultilineEncoding: secret?.skipMultilineEncoding,
environment: secret.environment,
isDeleted: false,
secretBlindIndex: secret.secretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
})
)
});
// question to team where to keep secretKey
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.CREATE_SECRETS,
metadata: {
environment,
secretPath: "/",
secrets: newlyCreatedSecrets.map(({ version, _id }) => ({
secretId: _id.toString(),
secretKey: "",
secretVersion: version
}))
}
},
{
workspaceId
}
);
}
const secretUpdationCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.UPDATE
) as Array<{
op: CommitType.UPDATE;
newVersion: Partial<Omit<ISecretApprovalSecChange, "_id">> & { _id: Types.ObjectId };
secret: ISecret;
}>;
if (secretUpdationCommits.length) {
const conflictedByNewBlindIndex = await Secret.find({
workspace: workspaceId,
environment,
folder: folderId,
secretBlindIndex: {
$in: secretUpdationCommits
.map(({ newVersion }) => newVersion?.secretBlindIndex)
.filter(Boolean)
}
})
.select("+secretBlindIndex")
.lean();
const conflictGroupByBlindIndex = conflictedByNewBlindIndex.reduce<Record<string, boolean>>(
(prev, curr) => (curr?.secretBlindIndex ? { ...prev, [curr.secretBlindIndex]: true } : prev),
{}
);
secretUpdationCommits
.filter(
({ newVersion }) =>
newVersion.secretBlindIndex && conflictGroupByBlindIndex[newVersion.secretBlindIndex]
)
.forEach((el) => {
conflicts.push({ op: CommitType.UPDATE, id: el.newVersion._id.toString() });
});
const nonConflictSecrets = secretUpdationCommits.filter(({ newVersion }) =>
newVersion?.secretBlindIndex ? !conflictGroupByBlindIndex[newVersion.secretBlindIndex] : true
);
await Secret.bulkWrite(
// id and version are stripped off
nonConflictSecrets.map(
({
newVersion: {
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
tags
},
secret
}) => ({
updateOne: {
filter: {
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
secretBlindIndex: secret.secretBlindIndex,
type: SECRET_SHARED
},
update: {
$inc: {
version: 1
},
secretKeyIV,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex,
tags,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}
}
})
)
);
await EESecretService.addSecretVersions({
secretVersions: nonConflictSecrets.map(({ newVersion, secret }) => {
return new SecretVersion({
secret: secret._id,
version: secret.version + 1,
workspace: workspaceId,
type: SECRET_SHARED,
folder: folderId,
environment,
isDeleted: false,
secretBlindIndex: newVersion?.secretBlindIndex ?? secret.secretBlindIndex,
secretKeyCiphertext: newVersion?.secretKeyCiphertext ?? secret.secretKeyCiphertext,
secretKeyIV: newVersion?.secretKeyIV ?? secret.secretKeyCiphertext,
secretKeyTag: newVersion?.secretKeyTag ?? secret.secretKeyTag,
secretValueCiphertext: newVersion?.secretValueCiphertext ?? secret.secretValueCiphertext,
secretValueIV: newVersion?.secretValueIV ?? secret.secretValueIV,
secretValueTag: newVersion?.secretValueTag ?? secret.secretValueTag,
tags: newVersion?.tags ?? secret.tags,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
skipMultilineEncoding: newVersion?.skipMultilineEncoding ?? secret.skipMultilineEncoding
});
})
});
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.UPDATE_SECRETS,
metadata: {
environment,
secretPath: "/",
secrets: nonConflictSecrets.map(({ secret }) => ({
secretId: secret._id.toString(),
secretKey: "",
secretVersion: secret.version + 1
}))
}
},
{
workspaceId
}
);
}
const secretDeletionCommits = secretApprovalRequest.commits.filter(
({ op }) => op === CommitType.DELETE
) as Array<{
op: CommitType.DELETE;
secret: ISecret;
}>;
if (secretDeletionCommits.length) {
await Secret.deleteMany({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secretDeletionCommits.map(({ secret: { secretBlindIndex } }) => ({
secretBlindIndex,
type: { $in: ["shared", "personal"] }
}))
)
.exec();
await EESecretService.markDeletedSecretVersions({
secretIds: secretDeletionCommits.map(({ secret }) => secret._id)
});
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.DELETE_SECRETS,
metadata: {
environment,
secretPath: "/",
secrets: secretDeletionCommits.map(({ secret: { _id, version } }) => ({
secretId: _id.toString(),
secretKey: "",
secretVersion: version
}))
}
},
{
workspaceId
}
);
}
const updatedSecretApproval = await SecretApprovalRequest.findByIdAndUpdate(
id,
{
conflicts,
hasMerged: true,
status: "close"
},
{ new: true }
);
if (postHogClient) {
if (postHogClient) {
postHogClient.capture({
event: "secrets merged",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secretApprovalRequest.commits.length,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
}
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
return updatedSecretApproval;
};
+9 -1
View File
@@ -2,7 +2,15 @@ import { z } from "zod";
export const GetSecretApprovalRuleList = z.object({
query: z.object({
workspaceId: z.string()
workspaceId: z.string().trim()
})
});
export const GetSecretApprovalPolicyOfABoard = z.object({
query: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim()
})
});
@@ -6,7 +6,7 @@ export const getSecretApprovalRequests = z.object({
workspaceId: z.string().trim(),
environment: z.string().trim().optional(),
committer: z.string().trim().optional(),
status: z.string().trim().optional(),
status: z.enum(["open", "close"]).optional(),
limit: z.coerce.number().default(20),
offset: z.coerce.number().default(0)
})
@@ -26,3 +26,9 @@ export const updateSecretApprovalRequestStatus = z.object({
id: z.string().trim()
})
});
export const mergeSecretApprovalRequest = z.object({
body: z.object({
id: z.string().trim()
})
});