Merge pull request #2153 from aheruz/feat/secret-request-bypass-reason
feat: add bypass reason on bypassed secret requests
@@ -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: "Infisical Secret Change Policy 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,28 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Secret Approval Request Policy Bypassed</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Infisical</h1>
|
||||||
|
<h2>Secret Approval Request Bypassed</h2>
|
||||||
|
<p>A secret approval request has been bypassed in the project "{{projectName}}".</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{requesterFullName}} ({{requesterEmail}}) has merged
|
||||||
|
a secret to environment {{environment}} at secret path {{secretPath}}
|
||||||
|
without obtaining the required approvals.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The following reason was provided for bypassing the policy:
|
||||||
|
<em>{{bypassReason}}</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To review this action, please visit the request panel
|
||||||
|
<a href="{{approvalUrl}}">here</a>.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -6,7 +6,7 @@ description: "Learn how to request access to sensitive resources in Infisical."
|
|||||||
In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality.
|
In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality.
|
||||||
|
|
||||||
This functionality works in the following way:
|
This functionality works in the following way:
|
||||||
1. A project administrator sets up a policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment.
|
1. A project administrator sets up an access policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment.
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
@@ -14,9 +14,14 @@ This functionality works in the following way:
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
3. An eligible approver can approve or reject the access request.
|
4. An eligible approver can approve or reject the access request.
|
||||||

|
{/*  */}
|
||||||
|

|
||||||
|
|
||||||
4. As soon as the request is approved, developer is able to access the sought resources.
|
<Info>
|
||||||
|
If the access request matches with a policy that has a **Soft** enforcement level, the requester may bypass the policy and get access to the resource without full approval.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
5. As soon as the request is approved, developer is able to access the sought resources.
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -18,16 +18,26 @@ In a similar way, to solve the above-mentioned issues, Infisical provides a feat
|
|||||||
|
|
||||||
### Setting a policy
|
### Setting a policy
|
||||||
|
|
||||||
First, you would need to create a set of policies for a certain environment. In the example below, a generic policy for a production environment is shown. In this case, any user who submits a change to `prod` would first have to get an approval by a predefined approver (or multiple approvers).
|
First, you would need to create a set of policies for a certain environment. In the example below, a generic change policy for a production environment is shown. In this case, any user who submits a change to `prod` would first have to get an approval by a predefined approver (or multiple approvers).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### Policy enforcement levels
|
||||||
|
|
||||||
|
The enforcement level determines how strict the policy is. A **Hard** enforcement level means that any change that matches the policy will need full approval prior merging. A **Soft** enforcement level allows for break glass functionality on the request. If a change request is bypassed, the approvers will be notified via email.
|
||||||
|
|
||||||
|
### Example of creating a change policy
|
||||||
|
|
||||||
|
When creating a policy, you can choose the type of policy you want to create. In this case, we will be creating a `Change Policy`. Other types of policies include `Access Policy` that creates policies for **[Access Requests](/documentation/platform/access-controls/access-requests)**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### Example of updating secrets with Approval workflows
|
### Example of updating secrets with Approval workflows
|
||||||
|
|
||||||
When a user submits a change to an enviropnment that is under a particular policy, a corresponsing change request will go to a predefined approver (or multiple approvers).
|
When a user submits a change to an enviropnment that is under a particular policy, a corresponsing change request will go to a predefined approver (or multiple approvers).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
An approver is notified by email and/or Slack as soon as the request is initiated. In the Infisical Dashboard, they will be able to `approve` and `merge` (or `deny`) a request for a change in a particular environment. After that, depending on the workflows setup, the change will be automatically propagated to the right applications (e.g., using [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes)).
|
Approvers are notified by email and/or Slack as soon as the request is initiated. In the Infisical Dashboard, they will be able to `approve` and `merge` (or `deny`) a request for a change in a particular environment. After that, depending on the workflows setup, the change will be automatically propagated to the right applications (e.g., using [Infisical Kubernetes Operator](https://infisical.com/docs/integrations/platforms/kubernetes)).
|
||||||
|
|
||||||

|

|
||||||
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 55 KiB |
@@ -46,8 +46,10 @@ export const usePerformSecretApprovalRequestMerge = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TPerformSecretApprovalRequestMerge>({
|
return useMutation<{}, {}, TPerformSecretApprovalRequestMerge>({
|
||||||
mutationFn: async ({ id }) => {
|
mutationFn: async ({ id, bypassReason }) => {
|
||||||
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/merge`);
|
const { data } = await apiRequest.post(`/api/v1/secret-approval-requests/${id}/merge`, {
|
||||||
|
bypassReason
|
||||||
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { id, workspaceId }) => {
|
onSuccess: (_, { id, workspaceId }) => {
|
||||||
|
|||||||
@@ -133,4 +133,5 @@ export type TUpdateSecretApprovalRequestStatusDTO = {
|
|||||||
export type TPerformSecretApprovalRequestMerge = {
|
export type TPerformSecretApprovalRequestMerge = {
|
||||||
id: string;
|
id: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
bypassReason?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import {
|
|||||||
faLockOpen,
|
faLockOpen,
|
||||||
faSquareCheck,
|
faSquareCheck,
|
||||||
faSquareXmark,
|
faSquareXmark,
|
||||||
|
faTriangleExclamation,
|
||||||
faUserLock} from "@fortawesome/free-solid-svg-icons";
|
faUserLock} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, Checkbox } from "@app/components/v2";
|
import { Button, Checkbox, FormControl, Input } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
usePerformSecretApprovalRequestMerge,
|
usePerformSecretApprovalRequestMerge,
|
||||||
useUpdateSecretApprovalRequestStatus
|
useUpdateSecretApprovalRequestStatus
|
||||||
@@ -48,12 +49,19 @@ export const SecretApprovalRequestAction = ({
|
|||||||
useUpdateSecretApprovalRequestStatus();
|
useUpdateSecretApprovalRequestStatus();
|
||||||
|
|
||||||
const [byPassApproval, setByPassApproval] = useState(false);
|
const [byPassApproval, setByPassApproval] = useState(false);
|
||||||
|
const [bypassReason, setBypassReason] = useState("");
|
||||||
|
|
||||||
|
const isValidBypassReason = (value: string) => {
|
||||||
|
const trimmedValue = value.trim();
|
||||||
|
return trimmedValue.length >= 10;
|
||||||
|
};
|
||||||
|
|
||||||
const handleSecretApprovalRequestMerge = async () => {
|
const handleSecretApprovalRequestMerge = async () => {
|
||||||
try {
|
try {
|
||||||
await performSecretApprovalMerge({
|
await performSecretApprovalMerge({
|
||||||
id: approvalRequestId,
|
id: approvalRequestId,
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
bypassReason: byPassApproval ? bypassReason : undefined
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
@@ -92,7 +100,7 @@ export const SecretApprovalRequestAction = ({
|
|||||||
|
|
||||||
if (!hasMerged && status === "open") {
|
if (!hasMerged && status === "open") {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-start justify-between transition-all">
|
||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={isMergable ? faSquareCheck : faSquareXmark}
|
icon={isMergable ? faSquareCheck : faSquareXmark}
|
||||||
@@ -105,18 +113,33 @@ export const SecretApprovalRequestAction = ({
|
|||||||
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
|
{Boolean(statusChangeByEmail) && `. Reopened by ${statusChangeByEmail}`}
|
||||||
</span>
|
</span>
|
||||||
{!canApprove && isSoftEnforcement && (
|
{!canApprove && isSoftEnforcement && (
|
||||||
<div className="mt-1">
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onCheckedChange={(checked) => setByPassApproval(checked === true)}
|
onCheckedChange={(checked) => setByPassApproval(checked === true)}
|
||||||
isChecked={byPassApproval}
|
isChecked={byPassApproval}
|
||||||
id="byPassApproval"
|
id="byPassApproval"
|
||||||
checkIndicatorBg="text-white"
|
checkIndicatorBg="text-white"
|
||||||
className={byPassApproval ? "bg-red hover:bg-red-600 border-red" : ""}
|
className={twMerge("mr-2", byPassApproval ? "bg-red hover:bg-red-600 border-red" : "")}
|
||||||
>
|
>
|
||||||
<span className="text-red text-sm">
|
<span className="text-red text-xs">
|
||||||
Merge without waiting for approval (bypass secret change policy)
|
Merge without waiting for approval (bypass secret change policy)
|
||||||
</span>
|
</span>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
{byPassApproval && (
|
||||||
|
<FormControl
|
||||||
|
label="Reason for bypass"
|
||||||
|
className="mt-2"
|
||||||
|
isRequired
|
||||||
|
tooltipText="Enter a reason for bypassing the secret change policy"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={bypassReason}
|
||||||
|
onChange={(e) => setBypassReason(e.target.value)}
|
||||||
|
placeholder="Enter reason for bypass (min 10 chars)"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faTriangleExclamation} />}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -137,7 +160,7 @@ export const SecretApprovalRequestAction = ({
|
|||||||
leftIcon={<FontAwesomeIcon icon={!canApprove ? faLandMineOn : faCheck} />}
|
leftIcon={<FontAwesomeIcon icon={!canApprove ? faLandMineOn : faCheck} />}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
(!isMergable && canApprove)
|
(!isMergable && canApprove)
|
||||||
|| (!canApprove && isSoftEnforcement && !byPassApproval)
|
|| (!canApprove && isSoftEnforcement && (!byPassApproval || !isValidBypassReason(bypassReason)))
|
||||||
}
|
}
|
||||||
isLoading={isMerging}
|
isLoading={isMerging}
|
||||||
onClick={handleSecretApprovalRequestMerge}
|
onClick={handleSecretApprovalRequestMerge}
|
||||||
|
|||||||