mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(backend): save bypass reason on secret_approval_requests
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason");
|
||||||
|
if (hasColumn) {
|
||||||
|
await knex.schema.table(TableName.SecretApprovalRequest, (table) => {
|
||||||
|
table.dropColumn("bypassReason");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ export const SecretApprovalRequestsSchema = z.object({
|
|||||||
conflicts: z.unknown().nullable().optional(),
|
conflicts: z.unknown().nullable().optional(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
|
bypassReason: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
isReplicated: z.boolean().nullable().optional(),
|
isReplicated: z.boolean().nullable().optional(),
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string()
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
|
body: z.object({
|
||||||
|
bypassReason: z.string().optional()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
approval: SecretApprovalRequestsSchema
|
approval: SecretApprovalRequestsSchema
|
||||||
@@ -130,7 +133,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
approvalId: req.params.id
|
approvalId: req.params.id,
|
||||||
|
bypassReason: req.body.bypassReason
|
||||||
});
|
});
|
||||||
return { approval };
|
return { approval };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("projectId").withSchema(TableName.Environment),
|
tx.ref("projectId").withSchema(TableName.Environment),
|
||||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
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("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
|
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
|
||||||
);
|
);
|
||||||
@@ -130,7 +131,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
name: el.policyName,
|
name: el.policyName,
|
||||||
approvals: el.policyApprovals,
|
approvals: el.policyApprovals,
|
||||||
secretPath: el.policySecretPath,
|
secretPath: el.policySecretPath,
|
||||||
enforcementLevel: el.policyEnforcementLevel
|
enforcementLevel: el.policyEnforcementLevel,
|
||||||
|
envId: el.policyEnvId
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
SecretType,
|
SecretType,
|
||||||
TSecretApprovalRequestsSecretsInsert
|
TSecretApprovalRequestsSecretsInsert
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { groupBy, pick, unique } from "@app/lib/fn";
|
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 { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
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 { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
import {
|
import {
|
||||||
fnSecretBlindIndexCheck,
|
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 { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-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 { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
@@ -63,8 +67,11 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectById">;
|
||||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
||||||
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
|
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
|
||||||
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||||
@@ -83,7 +90,10 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
snapshotService,
|
snapshotService,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
projectBotService
|
projectBotService,
|
||||||
|
smtpService,
|
||||||
|
userDAL,
|
||||||
|
projectEnvDAL
|
||||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
@@ -258,7 +268,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod,
|
||||||
|
bypassReason
|
||||||
}: TMergeSecretApprovalRequestDTO) => {
|
}: TMergeSecretApprovalRequestDTO) => {
|
||||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||||
@@ -470,7 +481,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
conflicts: JSON.stringify(conflicts),
|
conflicts: JSON.stringify(conflicts),
|
||||||
hasMerged: true,
|
hasMerged: true,
|
||||||
status: RequestState.Closed,
|
status: RequestState.Closed,
|
||||||
statusChangedByUserId: actorId
|
statusChangedByUserId: actorId,
|
||||||
|
bypassReason
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -489,6 +501,35 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actor
|
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;
|
return mergeStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type TGenerateSecretApprovalRequestDTO = {
|
|||||||
|
|
||||||
export type TMergeSecretApprovalRequestDTO = {
|
export type TMergeSecretApprovalRequestDTO = {
|
||||||
approvalId: string;
|
approvalId: string;
|
||||||
|
bypassReason?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TStatusChangeDTO = {
|
export type TStatusChangeDTO = {
|
||||||
|
|||||||
@@ -755,7 +755,10 @@ export const registerRoutes = async (
|
|||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
secretQueueService
|
secretQueueService,
|
||||||
|
smtpService,
|
||||||
|
userDAL,
|
||||||
|
projectEnvDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export enum SmtpTemplates {
|
|||||||
EmailMfa = "emailMfa.handlebars",
|
EmailMfa = "emailMfa.handlebars",
|
||||||
UnlockAccount = "unlockAccount.handlebars",
|
UnlockAccount = "unlockAccount.handlebars",
|
||||||
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
||||||
|
AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars",
|
||||||
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
||||||
NewDeviceJoin = "newDevice.handlebars",
|
NewDeviceJoin = "newDevice.handlebars",
|
||||||
OrgInvite = "organizationInvitation.handlebars",
|
OrgInvite = "organizationInvitation.handlebars",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Secret Approval Request Bypassed</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<h2>A secret approval request has been bypassed</h2>
|
||||||
|
<p>A secret approval request has been merged without approval in project "{{projectName}}".</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{requesterFullName}}
|
||||||
|
({{requesterEmail}}) has merged
|
||||||
|
a secret to
|
||||||
|
{{secretPath}}
|
||||||
|
in the
|
||||||
|
{{environment}}
|
||||||
|
environment.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The following reason was provided:
|
||||||
|
<em>{{bypassReason}}</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Go to the request panel
|
||||||
|
<a href="{{approvalUrl}}">here</a>.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user