Merge pull request #2153 from aheruz/feat/secret-request-bypass-reason

feat: add bypass reason on bypassed secret requests
This commit is contained in:
Maidul Islam
2024-07-19 17:22:18 -04:00
committed by GitHub
19 changed files with 165 additions and 22 deletions
@@ -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 = {
+4 -1
View File
@@ -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.
![Create Access Request Policy Modal](/images/platform/access-controls/create-access-request-policy.png) ![Create Access Request Policy Modal](/images/platform/access-controls/create-access-request-policy.png)
![Access Request Policies](/images/platform/access-controls/access-request-policies.png) ![Access Request Policies](/images/platform/access-controls/access-request-policies.png)
@@ -14,9 +14,14 @@ This functionality works in the following way:
![Access Request Create](/images/platform/access-controls/request-access.png) ![Access Request Create](/images/platform/access-controls/request-access.png)
![Access Request Dashboard](/images/platform/access-controls/access-requests-pending.png) ![Access Request Dashboard](/images/platform/access-controls/access-requests-pending.png)
3. An eligible approver can approve or reject the access request. 4. An eligible approver can approve or reject the access request.
![Access Request Review](/images/platform/access-controls/review-access-request.png) {/* ![Access Request Review](/images/platform/access-controls/review-access-request.png) */}
![Access Request Bypass](/images/platform/access-controls/access-request-bypass.png)
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.
![Access Request Dashboard](/images/platform/access-controls/access-requests-completed.png) ![Access Request Dashboard](/images/platform/access-controls/access-requests-completed.png)
+12 -2
View File
@@ -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).
![create secret update policy](../../images/platform/pr-workflows/secret-update-policy.png) ![create secret update policy](../../images/platform/pr-workflows/secret-update-policy.png)
### 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)**.
![create panel secret update policy](../../images/platform/pr-workflows/create-change-policy.png)
### 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).
![secret update change requests](../../images/platform/pr-workflows/secret-update-request.png) ![secret update change requests](../../images/platform/pr-workflows/secret-update-request.png)
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)).
![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png) ![secrets update pull request](../../images/platform/pr-workflows/secret-update-pr.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

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}