From bff8f55ea297660423f2600a8c7af450d08d2b61 Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Fri, 19 Jul 2024 21:31:48 +0200 Subject: [PATCH] feat(backend): save bypass reason on secret_approval_requests --- ...d-bypass-reason-secret-approval-requets.ts | 21 ++++++++ .../db/schemas/secret-approval-requests.ts | 1 + .../v1/secret-approval-request-router.ts | 6 ++- .../secret-approval-request-dal.ts | 4 +- .../secret-approval-request-service.ts | 49 +++++++++++++++++-- .../secret-approval-request-types.ts | 1 + backend/src/server/routes/index.ts | 5 +- backend/src/services/smtp/smtp-service.ts | 1 + .../accessSecretRequestBypassed.handlebars | 35 +++++++++++++ 9 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts create mode 100644 backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars diff --git a/backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts b/backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts new file mode 100644 index 0000000000..6b688dbc39 --- /dev/null +++ b/backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason"); + if (!hasColumn) { + await knex.schema.table(TableName.SecretApprovalRequest, (table) => { + table.string("bypassReason").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason"); + if (hasColumn) { + await knex.schema.table(TableName.SecretApprovalRequest, (table) => { + table.dropColumn("bypassReason"); + }); + } +} diff --git a/backend/src/db/schemas/secret-approval-requests.ts b/backend/src/db/schemas/secret-approval-requests.ts index aa6896d106..7ca0b71d9f 100644 --- a/backend/src/db/schemas/secret-approval-requests.ts +++ b/backend/src/db/schemas/secret-approval-requests.ts @@ -15,6 +15,7 @@ export const SecretApprovalRequestsSchema = z.object({ conflicts: z.unknown().nullable().optional(), slug: z.string(), folderId: z.string().uuid(), + bypassReason: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), isReplicated: z.boolean().nullable().optional(), diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 8e0b8a6823..0ba7099a3b 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -117,6 +117,9 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv params: z.object({ id: z.string() }), + body: z.object({ + bypassReason: z.string().optional() + }), response: { 200: z.object({ approval: SecretApprovalRequestsSchema @@ -130,7 +133,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - approvalId: req.params.id + approvalId: req.params.id, + bypassReason: req.body.bypassReason }); return { approval }; } diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 4ce174d269..855442e6ff 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -94,6 +94,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("projectId").withSchema(TableName.Environment), tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), + tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"), tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals") ); @@ -130,7 +131,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { name: el.policyName, approvals: el.policyApprovals, secretPath: el.policySecretPath, - enforcementLevel: el.policyEnforcementLevel + enforcementLevel: el.policyEnforcementLevel, + envId: el.policyEnvId } }), childrenMapper: [ diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 9f824b2b15..56a61d4293 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -7,6 +7,7 @@ import { SecretType, TSecretApprovalRequestsSecretsInsert } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { groupBy, pick, unique } from "@app/lib/fn"; @@ -15,6 +16,7 @@ import { EnforcementLevel } from "@app/lib/types"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; +import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { fnSecretBlindIndexCheck, @@ -31,6 +33,8 @@ import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; @@ -63,8 +67,11 @@ type TSecretApprovalRequestServiceFactoryDep = { snapshotService: Pick; secretVersionDAL: Pick; secretVersionTagDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; secretQueueService: Pick; + smtpService: Pick; + userDAL: Pick; + projectEnvDAL: Pick; }; export type TSecretApprovalRequestServiceFactory = ReturnType; @@ -83,7 +90,10 @@ export const secretApprovalRequestServiceFactory = ({ snapshotService, secretVersionDAL, secretQueueService, - projectBotService + projectBotService, + smtpService, + userDAL, + projectEnvDAL }: TSecretApprovalRequestServiceFactoryDep) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); @@ -258,7 +268,8 @@ export const secretApprovalRequestServiceFactory = ({ actor, actorId, actorOrgId, - actorAuthMethod + actorAuthMethod, + bypassReason }: TMergeSecretApprovalRequestDTO) => { const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId); if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); @@ -470,7 +481,8 @@ export const secretApprovalRequestServiceFactory = ({ conflicts: JSON.stringify(conflicts), hasMerged: true, status: RequestState.Closed, - statusChangedByUserId: actorId + statusChangedByUserId: actorId, + bypassReason }, tx ); @@ -489,6 +501,35 @@ export const secretApprovalRequestServiceFactory = ({ actorId, actor }); + + if (isSoftEnforcement) { + const cfg = getConfig(); + const project = await projectDAL.findProjectById(projectId); + const env = await projectEnvDAL.findOne({ id: policy.envId }); + const requestedByUser = await userDAL.findOne({ id: actorId }); + const approverUsers = await userDAL.find({ + $in: { + id: policy.approvers.map((approver: { userId: string }) => approver.userId) + } + }); + + await smtpService.sendMail({ + recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!), + subjectLine: "Secret Request Bypassed", + + substitutions: { + projectName: project.name, + requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`, + requesterEmail: requestedByUser.email, + bypassReason, + secretPath: policy.secretPath, + environment: env.name, + approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval` + }, + template: SmtpTemplates.AccessSecretRequestBypassed + }); + } + return mergeStatus; }; diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts index 1fbb754184..db3dc176b4 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts @@ -39,6 +39,7 @@ export type TGenerateSecretApprovalRequestDTO = { export type TMergeSecretApprovalRequestDTO = { approvalId: string; + bypassReason?: string; } & Omit; export type TStatusChangeDTO = { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 3424e7134a..59aa8d3014 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -755,7 +755,10 @@ export const registerRoutes = async ( secretApprovalRequestDAL, snapshotService, secretVersionTagDAL, - secretQueueService + secretQueueService, + smtpService, + userDAL, + projectEnvDAL }); const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 1fb89c5537..1511274841 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -23,6 +23,7 @@ export enum SmtpTemplates { EmailMfa = "emailMfa.handlebars", UnlockAccount = "unlockAccount.handlebars", AccessApprovalRequest = "accessApprovalRequest.handlebars", + AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars", HistoricalSecretList = "historicalSecretLeakIncident.handlebars", NewDeviceJoin = "newDevice.handlebars", OrgInvite = "organizationInvitation.handlebars", diff --git a/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars b/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars new file mode 100644 index 0000000000..1986d8ef0c --- /dev/null +++ b/backend/src/services/smtp/templates/accessSecretRequestBypassed.handlebars @@ -0,0 +1,35 @@ + + + + + + + Secret Approval Request Bypassed + + + +

Infisical

+

A secret approval request has been bypassed

+

A secret approval request has been merged without approval in project "{{projectName}}".

+ +

+ {{requesterFullName}} + ({{requesterEmail}}) has merged + a secret to + {{secretPath}} + in the + {{environment}} + environment. +

+

+ The following reason was provided: + {{bypassReason}} +

+ +

+ Go to the request panel + here. +

+ + + \ No newline at end of file