feat(secret-approval): implemented the base

This commit is contained in:
Akhil Mohan
2023-10-05 20:33:48 +05:30
parent a995627815
commit fc43511f5d
22 changed files with 1467 additions and 49 deletions
+3 -2
View File
@@ -17,7 +17,7 @@ import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController";
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
import * as secretApprovalRequestController from "./secretApprovalRequestsController";
export {
authController,
botController,
@@ -37,5 +37,6 @@ export {
secretScanningController,
webhookController,
secretImpsController,
secretApprovalPolicyController
secretApprovalPolicyController,
secretApprovalRequestController
};
@@ -0,0 +1,128 @@
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 * as reqValidator from "../../validation/secretApprovalRequest";
import { getFolderWithPathFromId } from "../../services/FolderService";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ISecretApprovalPolicy } from "../../models/secretApprovalPolicy";
export const getSecretApprovalRequests = async (req: Request, res: Response) => {
const {
query: { status, committer, workspaceId, environment, limit, offset }
} = await validateRequest(reqValidator.getSecretApprovalRequests, req);
const { membership } = await getUserProjectPermissions(req.user._id, workspaceId);
const query = {
workspace: workspaceId,
environment,
committer,
status,
...(membership.role !== "admin"
? { $or: [{ committer: membership.id }, { "policy.approvers": membership.id }] }
: {})
};
// to strip of undefined in query we use es6 spread to ignore those fields
Object.entries(query).forEach(
([key, value]) => value === undefined && delete query[key as keyof typeof query]
);
const approvalRequests = await SecretApprovalRequest.find(query)
.limit(limit)
.skip(offset)
.populate("policy")
.lean();
if (!approvalRequests.length) return res.send({ requests: [] });
const unqiueEnvs = environment ?? {
$in: [...new Set(approvalRequests.map(({ environment }) => environment))]
};
const approvalRootFolders = await Folder.find({
workspace: workspaceId,
environment: unqiueEnvs
}).lean();
const formatedApprovals = approvalRequests.map((el) => {
let secretPath = "/";
const folders = approvalRootFolders.find(({ environment }) => environment === el.environment);
if (folders) {
secretPath = getFolderWithPathFromId(folders?.nodes, el.folderId)?.folderPath || "/";
}
return { ...el, secretPath };
});
return res.send({
approvals: formatedApprovals
});
};
export const getSecretApprovalRequestDetails = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate("policy")
.populate({
path: "commits.secret",
populate: {
path: "tags"
}
})
.populate("commits.newVersion.tags");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
const { membership } = await getUserProjectPermissions(
req.user._id,
secretApprovalRequest.workspace.toString()
);
// allow to fetch only if its admin or is the committer or approver
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
secretApprovalRequest.reviewers.find(({ member }) => member === membership.id)
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
return res.send({
approval: secretApprovalRequest
});
};
export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => {
const {
body: { status },
params: { id }
} = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, 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 reviewerPos = secretApprovalRequest.reviewers.findIndex(
({ member }) => member.toString() === membership._id.toString()
);
if (reviewerPos !== -1) {
secretApprovalRequest.reviewers[reviewerPos].status = status;
} else {
secretApprovalRequest.reviewers.push({ member: membership._id, status });
}
await secretApprovalRequest.save();
return res.send({ status });
};
+145 -15
View File
@@ -12,6 +12,7 @@ import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import {
Folder,
IMembership,
IServiceTokenData,
IServiceTokenDataV3
} from "../../models";
@@ -49,7 +50,7 @@ const checkSecretsPermission = async ({
environment: string;
secretPath: string;
secretAction: ProjectPermissionActions; // CRUD
}): Promise<(env: string, secPath: string) => boolean> => {
}): Promise<{authVerifier:(env: string, secPath: string) => boolean,membership?:Omit<IMembership,"customRole"> & {customRole: IRole}}> => {
let STV2RequiredPermissions = [];
let STV3RequiredPermissions: Permission[] = [];
@@ -75,19 +76,19 @@ const checkSecretsPermission = async ({
switch (authData.actor.type) {
case ActorType.USER: {
const { permission } = await getUserProjectPermissions(authData.actor.metadata.userId, workspaceId);
const { permission,membership } = await getUserProjectPermissions(authData.actor.metadata.userId, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
secretAction,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
return (env: string, secPath: string) =>
return {authVerifier: (env: string, secPath: string) =>
permission.can(
secretAction,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
),membership};
}
case ActorType.SERVICE: {
await validateServiceTokenDataClientForWorkspace({
@@ -97,7 +98,7 @@ const checkSecretsPermission = async ({
secretPath,
requiredPermissions: STV2RequiredPermissions
});
return () => true;
return {authVerifier:() => true};
}
case ActorType.SERVICE_V3: {
await validateServiceTokenDataV3ClientForWorkspace({
@@ -108,19 +109,25 @@ const checkSecretsPermission = async ({
secretPath,
requiredPermissions: STV3RequiredPermissions
});
return (env: string, secPath: string) =>
return {authVerifier: (env: string, secPath: string) =>
isValidScopeV3({
authPayload: authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
requiredPermissions: STV3RequiredPermissions
});
})};
}
default: {
throw UnauthorizedRequestError();
}
}
}
import {
generateSecretApprovalRequest,
getSecretPolicyOfBoard
} from "../../services/SecretApprovalService";
import { CommitType } from "../../models/secretApprovalRequest";
import { IRole } from "../../ee/models/role";
/**
* Return secrets for workspace with id [workspaceId] and environment
@@ -160,7 +167,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
if (!environment || !workspaceId)
throw BadRequestError({ message: "Missing environment or workspace id" });
const permissionCheckFn = await checkSecretsPermission({
const {authVerifier:permissionCheckFn} = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -475,7 +482,7 @@ export const getSecrets = async (req: Request, res: Response) => {
secretPath = getFolderWithPathFromId(folder.nodes, folderId).folderPath;
}
const permissionCheckFn = await checkSecretsPermission({
const {authVerifier:permissionCheckFn} = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -580,7 +587,7 @@ export const createSecret = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req);
await checkSecretsPermission({
const {membership} = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -588,6 +595,35 @@ export const createSecret = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Create
});
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.CREATE]: [
{
secretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
@@ -656,7 +692,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
throw BadRequestError({ message: "Missing encrypted key" });
}
await checkSecretsPermission({
const {membership} = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -664,6 +700,36 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Edit
});
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.UPDATE]: [
{
secretName,
secretValueCiphertext,
secretValueIV,
secretValueTag,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
const secret = await SecretService.updateSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
@@ -709,7 +775,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
await checkSecretsPermission({
const {membership} = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -717,6 +783,25 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Delete
});
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: [
{
secretName
}
]
}
});
return res.send({ approval: secretApprovalRequest });
}
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
@@ -744,7 +829,7 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
await checkSecretsPermission({
const {membership} = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -752,6 +837,21 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Create
});
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.CREATE]: secrets
}
});
return res.send({ approval: secretApprovalRequest });
}
const createdSecrets = await SecretService.createSecretBatch({
secretPath,
environment,
@@ -770,7 +870,7 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req);
await checkSecretsPermission({
const {membership} = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
@@ -778,6 +878,21 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Edit
});
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.UPDATE]: secrets
}
});
return res.send({ approval: secretApprovalRequest });
}
const updatedSecrets = await SecretService.updateSecretBatch({
secretPath,
environment,
@@ -796,13 +911,28 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req);
await checkSecretsPermission({
const {membership} = await checkSecretsPermission({
authData: req.authData,
workspaceId,
environment,
secretPath,
secretAction: ProjectPermissionActions.Delete
});
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) {
const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId,
environment,
secretPath,
policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(),
data: {
[CommitType.DELETE]: secrets
}
});
return res.send({ approval: secretApprovalRequest });
}
const deletedSecrets = await SecretService.deleteSecretBatch({
secretPath,
+2
View File
@@ -43,6 +43,7 @@ import {
password as v1PasswordRouter,
sso as v1SSORouter,
secretApprovalPolicy as v1SecretApprovalPolicy,
secretApprovalRequest as v1SecretApprovalRequest,
secretImps as v1SecretImpsRouter,
secret as v1SecretRouter,
secretsFolder as v1SecretsFolder,
@@ -183,6 +184,7 @@ const main = async () => {
app.use("/api/v1/roles", v1RoleRouter);
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy);
app.use("/api/v1/sso", v1SSORouter);
app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequest);
// v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter);
+130 -26
View File
@@ -1,58 +1,162 @@
import { Schema, Types, model } from "mongoose";
import { ISecretVersion, SecretVersion } from "../ee/models/secretVersion";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8 } from "../variables";
enum ApprovalStatus {
export enum ApprovalStatus {
PENDING = "pending",
APPROVED = "approved",
REJECTED = "rejected"
}
enum CommitType {
export enum CommitType {
DELETE = "delete",
UPDATE = "update",
CREATE = "create"
}
export interface ISecretApprovalSecChange {
_id: Types.ObjectId;
version: number;
secretBlindIndex?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentCiphertext?: string;
skipMultilineEncoding?: boolean;
algorithm?: "aes-256-gcm";
keyEncoding?: "utf8" | "base64";
tags?: string[];
}
export interface ISecretApprovalRequest {
_id: Types.ObjectId;
committer: Types.ObjectId;
approvers: {
reviewers: {
member: Types.ObjectId;
status: ApprovalStatus;
}[];
approvals: number;
workspace: Types.ObjectId;
environment: string;
folderId: string;
hasMerged: boolean;
status: ApprovalStatus;
commits: {
secretVersion: Types.ObjectId;
newVersion: ISecretVersion;
op: CommitType;
}[];
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;
}
>;
}
const secretApprovalSecretChangeSchema = new Schema<ISecretApprovalSecChange>({
version: {
type: Number,
default: 1,
required: true
},
secretBlindIndex: {
type: String,
select: false
},
secretKeyCiphertext: {
type: String,
required: true
},
secretKeyIV: {
type: String, // symmetric
required: true
},
secretKeyTag: {
type: String, // symmetric
required: true
},
secretValueCiphertext: {
type: String,
required: true
},
secretValueIV: {
type: String, // symmetric
required: true
},
secretValueTag: {
type: String, // symmetric
required: true
},
skipMultilineEncoding: {
type: Boolean,
required: false
},
algorithm: {
// the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
default: ALGORITHM_AES_256_GCM
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true,
default: ENCODING_SCHEME_UTF8
},
tags: {
ref: "Tag",
type: [Schema.Types.ObjectId],
default: []
}
});
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
{
approvers: [
{
member: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: "Membership"
},
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }
}
],
approvals: {
type: Number,
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
environment: {
type: String,
required: true
},
folderId: {
type: String,
required: true,
default: "root"
},
reviewers: {
type: [
{
member: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: "Membership"
},
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }
}
],
default: []
},
policy: { type: Schema.Types.ObjectId, ref: "SecretApprovalPolicy" },
hasMerged: { type: Boolean, default: false },
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING },
status: { type: String, enum: ["close", "open"], default: "open" },
committer: { type: Schema.Types.ObjectId, ref: "Membership" },
commits: [
{
secretVersion: { type: Types.ObjectId, ref: "SecretVersion" },
newVersion: SecretVersion,
secret: { type: Types.ObjectId, ref: "Secret" },
newVersion: secretApprovalSecretChangeSchema,
op: { type: String, enum: [CommitType], required: true }
}
]
+3 -1
View File
@@ -19,6 +19,7 @@ import secretsFolder from "./secretsFolder";
import webhooks from "./webhook";
import secretImps from "./secretImps";
import secretApprovalPolicy from "./secretApprovalPolicy";
import secretApprovalRequest from "./secretApprovalRequest";
export {
signup,
@@ -41,5 +42,6 @@ export {
webhooks,
secretImps,
sso,
secretApprovalPolicy
secretApprovalPolicy,
secretApprovalRequest
};
@@ -0,0 +1,31 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../middleware";
import { secretApprovalRequestController } from "../../controllers/v1";
import { AuthMode } from "../../variables";
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequests
);
router.get(
"/:id",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.getSecretApprovalRequestDetails
);
router.post(
"/:id",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
secretApprovalRequestController.updateSecretApprovalRequestStatus
);
export default router;
@@ -0,0 +1,272 @@
import picomatch from "picomatch";
import { Types } from "mongoose";
import {
containsGlobPatterns,
generateSecretBlindIndexWithSaltHelper,
getSecretBlindIndexSaltHelper
} from "../helpers/secrets";
import { Folder, ISecret, Secret } from "../models";
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../models/secretApprovalPolicy";
import {
CommitType,
ISecretApprovalRequest,
ISecretApprovalSecChange,
SecretApprovalRequest
} from "../models/secretApprovalRequest";
import { BadRequestError } from "../utils/errors";
import { getFolderByPath } from "./FolderService";
import { SECRET_SHARED } from "../variables";
// 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) =>
policy.secretPath ? (containsGlobPatterns(policy.secretPath) ? 1 : 2) : 0;
// this will fetch the policy that gets priority for an environment and secret path
export const getSecretPolicyOfBoard = async (
workspaceId: string,
environment: string,
secretPath: string
) => {
const policies = await SecretApprovalPolicy.find({ workspace: workspaceId, environment });
if (!policies) return;
// this will filter policies either without scoped to secret path or the one that matches with secret path
const policiesFilteredByPath = policies.filter(
({ secretPath: policyPath }) =>
!policyPath || picomatch.isMatch(secretPath, policyPath, { strictSlashes: false })
);
// now sort by priority. exact secret path gets first match followed by glob followed by just env scoped
// if that is tie get by first createdAt
const policiesByPriority = policiesFilteredByPath.sort(
(a, b) => getPolicyScore(a) - getPolicyScore(b)
);
const finalPolicy = policiesByPriority.shift();
return finalPolicy;
};
type TApprovalCreateSecret = Omit<ISecretApprovalSecChange, "_id" | "version"> & {
secretName: string;
};
type TApprovalUpdateSecret = Partial<Omit<ISecretApprovalSecChange, "_id" | "version">> & {
secretName: string;
newSecretName?: string;
};
type TGenerateSecretApprovalRequestArg = {
workspaceId: string;
environment: string;
secretPath: string;
policy: ISecretApprovalPolicy;
data: {
[CommitType.CREATE]?: TApprovalCreateSecret[];
[CommitType.UPDATE]?: TApprovalUpdateSecret[];
[CommitType.DELETE]?: { secretName: string }[];
};
commiterMembershipId: string;
};
export const generateSecretApprovalRequest = async ({
workspaceId,
environment,
secretPath,
policy,
data,
commiterMembershipId
}: TGenerateSecretApprovalRequestArg) => {
// calculate folder id from secret path
let folderId = "root";
const rootFolder = await Folder.findOne({ workspace: workspaceId, environment });
if (!rootFolder && secretPath !== "/") throw BadRequestError({ message: "Folder not found" });
if (rootFolder) {
const folder = getFolderByPath(rootFolder.nodes, secretPath);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
// generate secret blindIndexes
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const commits: ISecretApprovalRequest["commits"] = [];
// -----
// for created secret approval change
const createdSecret = data[CommitType.CREATE];
if (createdSecret && createdSecret?.length) {
// validation checks whether secret exists for creation
const secretBlindIndexes = await Promise.all(
createdSecret.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[createdSecret[i].secretName] = curr;
return prev;
}, {})
);
// check created secret exists
const exists = await Secret.exists({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
createdSecret.map(({ secretName }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: SECRET_SHARED
}))
)
.exec();
if (exists) throw BadRequestError({ message: "Secrets already exist" });
commits.push(
...createdSecret.map((el) => ({
op: CommitType.CREATE as const,
newVersion: {
...el,
version: 0,
_id: new Types.ObjectId(),
secretBlindIndex: secretBlindIndexes[el.secretName]
}
}))
);
}
// ----
// updated secrets approval change
const updatedSecret = data[CommitType.UPDATE];
if (updatedSecret && updatedSecret?.length) {
// validation checks whether secret doesn't exists for update
const secretBlindIndexes = await Promise.all(
updatedSecret.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[updatedSecret[i].secretName] = curr;
return prev;
}, {})
);
// check update secret exists
const secretsToBeUpdated = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.select("+secretBlindIndex")
.or(
updatedSecret.map(({ secretName }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: SECRET_SHARED
}))
)
.lean()
.exec();
if (secretsToBeUpdated.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(
nameUpdatedSecrets.map(({ newSecretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName: newSecretName as string,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[nameUpdatedSecrets[i].secretName] = curr;
return prev;
}, {})
);
const doesAnySecretExistWithNewIndex = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment,
secretBlindIndex: { $in: Object.values(newSecretBlindIndexes) }
});
if (doesAnySecretExistWithNewIndex)
throw BadRequestError({ message: "Secret with new name already exist" });
commits.push(
...updatedSecret.map((el) => {
const oldSecret = secretsToBeUpdated.find(
(sec) => sec?.secretBlindIndex === secretBlindIndexes[el.secretName]
);
if (!oldSecret) throw BadRequestError({ message: "Secret not found" });
return {
op: CommitType.UPDATE as const,
secret: oldSecret._id,
newVersion: {
...el,
secretBlindIndex: newSecretBlindIndexes?.[el.secretName],
_id: new Types.ObjectId(),
version: oldSecret.version || 1
}
};
})
);
}
// -----
// deleted secrets
const deletedSecrets = data[CommitType.DELETE];
if (deletedSecrets && deletedSecrets.length) {
const secretBlindIndexes = await Promise.all(
deletedSecrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[deletedSecrets[i].secretName] = curr;
return prev;
}, {})
);
const secretsToDelete = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
deletedSecrets.map(({ secretName }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: SECRET_SHARED
}))
)
.select({ secretBlindIndexes: 1 })
.lean()
.exec();
if (secretsToDelete.length !== deletedSecrets.length)
throw BadRequestError({ message: "Deleted secrets not found" });
commits.push(
...deletedSecrets.map((el) => ({
op: CommitType.DELETE as const,
secret: (
secretsToDelete.find(
(sec) => sec?.secretBlindIndex === secretBlindIndexes[el.secretName]
) as ISecret
)._id
}))
);
}
const secretApprovalRequest = new SecretApprovalRequest({
workspace: workspaceId,
environment,
folderId,
policy,
commits,
committer: commiterMembershipId
});
await secretApprovalRequest.save();
return secretApprovalRequest;
};
@@ -0,0 +1,28 @@
import { z } from "zod";
import { ApprovalStatus } from "../models/secretApprovalRequest";
export const getSecretApprovalRequests = z.object({
query: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim().optional(),
committer: z.string().trim().optional(),
status: z.string().trim().optional(),
limit: z.coerce.number().default(20),
offset: z.coerce.number().default(0)
})
});
export const getSecretApprovalRequestDetails = z.object({
params: z.object({
id: z.string().trim()
})
});
export const updateSecretApprovalRequestStatus = z.object({
body: z.object({
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
}),
params: z.object({
id: z.string().trim()
})
});