Merge pull request #1754 from Infisical/parameter-store-kms-key

added kms key selector for parameter store
This commit is contained in:
vmatsiiako
2024-04-28 23:06:09 -07:00
committed by GitHub
21 changed files with 225 additions and 53 deletions
@@ -572,14 +572,14 @@ export const integrationAuthServiceFactory = ({
const response = keys const response = keys
.Keys!.map((key) => { .Keys!.map((key) => {
const keyAlias = aliases.Aliases!.find((alias) => key.KeyId === alias.TargetKeyId); const keyAlias = aliases.Aliases!.find((alias) => key.KeyId === alias.TargetKeyId);
if (!keyAlias?.AliasName?.includes("alias/aws/") || keyAlias?.AliasName?.includes("alias/aws/secretsmanager")) { if (!keyAlias?.AliasName?.includes("alias/aws/")) {
return { id: String(key.KeyId), alias: String(keyAlias?.AliasName || key.KeyId) }; return { id: String(key.KeyId), alias: String(keyAlias?.AliasName || key.KeyId) };
} }
return { id: "null", alias: "null" }; return { id: "null", alias: "null" };
}) })
.filter((elem) => elem.id !== "null"); .filter((elem) => elem.id !== "null");
return response; return [...response, { id: "null", alias: "default" }];
}; };
const getQoveryProjects = async ({ const getQoveryProjects = async ({
@@ -477,24 +477,29 @@ const syncSecretsAWSParameterStore = async ({
}), }),
{} as Record<string, AWS.SSM.Parameter> {} as Record<string, AWS.SSM.Parameter>
); );
// Identify secrets to create // Identify secrets to create
await Promise.all( await Promise.all(
Object.keys(secrets).map(async (key) => { Object.keys(secrets).map(async (key) => {
if (!(key in awsParameterStoreSecretsObj)) { if (!(key in awsParameterStoreSecretsObj)) {
// case: secret does not exist in AWS parameter store // case: secret does not exist in AWS parameter store
// -> create secret // -> create secret
await ssm if (secrets[key].value) {
.putParameter({ await ssm
Name: `${integration.path}${key}`, .putParameter({
Type: "SecureString", Name: `${integration.path}${key}`,
Value: secrets[key].value, Type: "SecureString",
// Overwrite: true, Value: secrets[key].value,
Tags: metadata.secretAWSTag KeyId: metadata.kmsKeyId ? metadata.kmsKeyId : undefined,
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value })) // Overwrite: true,
: [] Tags: metadata.secretAWSTag
}) ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
.promise(); Key: tag.key,
Value: tag.value
}))
: []
})
.promise();
}
// case: secret exists in AWS parameter store // case: secret exists in AWS parameter store
} else if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) { } else if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
// case: secret value doesn't match one in AWS parameter store // case: secret value doesn't match one in AWS parameter store
Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 160 KiB

@@ -30,13 +30,18 @@ Prerequisites:
"ssm:DeleteParameter", "ssm:DeleteParameter",
"ssm:GetParametersByPath", "ssm:GetParametersByPath",
"ssm:DeleteParameters", "ssm:DeleteParameters",
"ssm:AddTagsToResource" // if you need to add tags to secrets "ssm:AddTagsToResource", // if you need to add tags to secrets
"kms:ListKeys", // if you need to specify the KMS key
"kms:ListAliases", // if you need to specify the KMS key
"kms:Encrypt", // if you need to specify the KMS key
"kms:Decrypt" // if you need to specify the KMS key
], ],
"Resource": "*" "Resource": "*"
} }
] ]
} }
``` ```
</Step> </Step>
<Step title="Authorize Infisical for AWS Parameter store"> <Step title="Authorize Infisical for AWS Parameter store">
Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys
@@ -44,7 +49,7 @@ Prerequisites:
![access key 1](../../images/integrations/aws/integrations-aws-access-key-1.png) ![access key 1](../../images/integrations/aws/integrations-aws-access-key-1.png)
![access key 2](../../images/integrations/aws/integrations-aws-access-key-2.png) ![access key 2](../../images/integrations/aws/integrations-aws-access-key-2.png)
![access key 3](../../images/integrations/aws/integrations-aws-access-key-3.png) ![access key 3](../../images/integrations/aws/integrations-aws-access-key-3.png)
Navigate to your project's integrations tab in Infisical. Navigate to your project's integrations tab in Infisical.
![integrations](../../images/integrations.png) ![integrations](../../images/integrations.png)
@@ -59,6 +64,7 @@ Prerequisites:
breaks E2EE, it's necessary for Infisical to sync the environment variables to breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform. the cloud platform.
</Info> </Info>
</Step> </Step>
<Step title="Start integration"> <Step title="Start integration">
Select which Infisical environment secrets you want to sync to which AWS Parameter Store region and indicate the path for your secrets. Then, press create integration to start syncing secrets to AWS Parameter Store. Select which Infisical environment secrets you want to sync to which AWS Parameter Store region and indicate the path for your secrets. Then, press create integration to start syncing secrets to AWS Parameter Store.
@@ -72,6 +78,6 @@ Prerequisites:
secret like `TEST` to be stored as `/[project_name]/[environment]/TEST` in AWS secret like `TEST` to be stored as `/[project_name]/[environment]/TEST` in AWS
Parameter Store. Parameter Store.
</Tip> </Tip>
</Step> </Step>
</Steps> </Steps>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -1,28 +1,4 @@
[ [
{
"name": "Docker",
"slug": "docker",
"image": "Docker",
"docsLink": "https://infisical.com/docs/integrations/platforms/docker"
},
{
"name": "Docker Compose",
"slug": "docker-compose",
"image": "Docker Compose",
"docsLink": "https://infisical.com/docs/integrations/platforms/docker-compose"
},
{
"name": "Kubernetes",
"slug": "kubernetes",
"image": "Kubernetes",
"docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes"
},
{
"name": "Terraform",
"slug": "terraform",
"image": "Terraform",
"docsLink": "https://infisical.com/docs/integrations/frameworks/terraform"
},
{ {
"name": "React", "name": "React",
"slug": "react", "slug": "react",
@@ -0,0 +1,50 @@
[
{
"name": "Docker",
"slug": "docker",
"image": "Docker",
"docsLink": "https://infisical.com/docs/integrations/platforms/docker"
},
{
"name": "Docker Compose",
"slug": "docker-compose",
"image": "Docker Compose",
"docsLink": "https://infisical.com/docs/integrations/platforms/docker-compose"
},
{
"name": "Kubernetes",
"slug": "kubernetes",
"image": "Kubernetes",
"docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes"
},
{
"name": "Terraform",
"slug": "terraform",
"image": "Terraform",
"docsLink": "https://infisical.com/docs/integrations/frameworks/terraform"
},
{
"name": "Jenkins",
"slug": "jenkins",
"image": "Jenkins",
"docsLink": "https://infisical.com/docs/integrations/cicd/jenkins"
},
{
"name": "Infisical Agent",
"slug": "agent",
"image": "Agent",
"docsLink": "https://infisical.com/docs/integrations/platforms/infisical-agent"
},
{
"name": "Amazon ECS",
"slug": "ecs",
"image": "ECS",
"docsLink": "https://infisical.com/docs/integrations/platforms/ecs-with-agent"
},
{
"name": "Ansible",
"slug": "ansible",
"image": "Ansible",
"docsLink": "https://infisical.com/docs/integrations/platforms/ansible"
}
]
+1 -1
View File
@@ -120,7 +120,7 @@
"available": "Platform & Cloud Integrations", "available": "Platform & Cloud Integrations",
"available-text1": "Click on the integration you want to connect. This will let your environment variables flow automatically into selected third-party services.", "available-text1": "Click on the integration you want to connect. This will let your environment variables flow automatically into selected third-party services.",
"available-text2": "Note: during an integration with Heroku, for security reasons, it is impossible to maintain end-to-end encryption. In theory, this lets Infisical decrypt yor environment variables. In practice, we can assure you that this will never be done, and it allows us to protect your secrets from bad actors online. The core Infisical service will always stay end-to-end encrypted. With any questions, reach out [email protected].", "available-text2": "Note: during an integration with Heroku, for security reasons, it is impossible to maintain end-to-end encryption. In theory, this lets Infisical decrypt yor environment variables. In practice, we can assure you that this will never be done, and it allows us to protect your secrets from bad actors online. The core Infisical service will always stay end-to-end encrypted. With any questions, reach out [email protected].",
"cloud-integrations": "Cloud Integrations", "cloud-integrations": "Native Integrations",
"framework-integrations": "Framework Integrations", "framework-integrations": "Framework Integrations",
"click-to-start": "Click on an integration to begin syncing secrets to it.", "click-to-start": "Click on an integration to begin syncing secrets to it.",
"click-to-setup": "Click on a framework to get the setup instructions.", "click-to-setup": "Click on a framework to get the setup instructions.",
+6 -3
View File
@@ -1,14 +1,16 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Head from "next/head"; import Head from "next/head";
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json"; import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
import infrastructureIntegrationOptions from "public/json/infrastructureIntegrations.json";
import { IntegrationsPage } from "@app/views/IntegrationsPage"; import { IntegrationsPage } from "@app/views/IntegrationsPage";
type Props = { type Props = {
frameworkIntegrations: typeof frameworkIntegrationOptions; frameworkIntegrations: typeof frameworkIntegrationOptions;
infrastructureIntegrations: typeof infrastructureIntegrationOptions;
}; };
const Integration = ({ frameworkIntegrations }: Props) => { const Integration = ({ frameworkIntegrations, infrastructureIntegrations }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -20,7 +22,7 @@ const Integration = ({ frameworkIntegrations }: Props) => {
<meta property="og:title" content="Manage your .env files in seconds" /> <meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} /> <meta name="og:description" content={t("integrations.description") as string} />
</Head> </Head>
<IntegrationsPage frameworkIntegrations={frameworkIntegrations} /> <IntegrationsPage frameworkIntegrations={frameworkIntegrations} infrastructureIntegrations={infrastructureIntegrations} />
</> </>
); );
}; };
@@ -28,7 +30,8 @@ const Integration = ({ frameworkIntegrations }: Props) => {
export const getStaticProps = () => { export const getStaticProps = () => {
return { return {
props: { props: {
frameworkIntegrations: frameworkIntegrationOptions frameworkIntegrations: frameworkIntegrationOptions,
infrastructureIntegrations: infrastructureIntegrationOptions
} }
}; };
}; };
@@ -14,6 +14,7 @@ import { motion } from "framer-motion";
import queryString from "query-string"; import queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api"; import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
import { import {
Button, Button,
@@ -90,6 +91,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
const [shouldTag, setShouldTag] = useState(false); const [shouldTag, setShouldTag] = useState(false);
const [tagKey, setTagKey] = useState(""); const [tagKey, setTagKey] = useState("");
const [tagValue, setTagValue] = useState(""); const [tagValue, setTagValue] = useState("");
const [kmsKeyId, setKmsKeyId] = useState("");
useEffect(() => { useEffect(() => {
if (workspace) { if (workspace) {
@@ -98,6 +100,19 @@ export default function AWSParameterStoreCreateIntegrationPage() {
} }
}, [workspace]); }, [workspace]);
const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } =
useGetIntegrationAuthAwsKmsKeys({
integrationAuthId: String(integrationAuthId),
region: selectedAWSRegion
});
useEffect(() => {
if (integrationAuthAwsKmsKeys) {
setKmsKeyId(String(integrationAuthAwsKmsKeys?.filter(key => key.alias === "default")[0]?.id))
}
}, [integrationAuthAwsKmsKeys])
const isValidAWSParameterStorePath = (awsStorePath: string) => { const isValidAWSParameterStorePath = (awsStorePath: string) => {
const pattern = /^\/([\w-]+\/)*[\w-]+\/$/; const pattern = /^\/([\w-]+\/)*[\w-]+\/$/;
return pattern.test(awsStorePath) && awsStorePath.length <= 2048; return pattern.test(awsStorePath) && awsStorePath.length <= 2048;
@@ -133,7 +148,11 @@ export default function AWSParameterStoreCreateIntegrationPage() {
value: tagValue value: tagValue
}] }]
} }
: {}) : {}),
...((kmsKeyId && integrationAuthAwsKmsKeys?.filter(key => key.id === kmsKeyId)[0]?.alias !== "default") ?
{
kmsKeyId
}: {})
} }
}); });
@@ -146,7 +165,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
} }
}; };
return integrationAuth && workspace && selectedSourceEnvironment ? ( return (integrationAuth && workspace && selectedSourceEnvironment && !isIntegrationAuthAwsKmsKeysLoading) ? (
<div className="flex h-full w-full flex-col items-center justify-center"> <div className="flex h-full w-full flex-col items-center justify-center">
<Head> <Head>
<title>Set Up AWS Parameter Integration</title> <title>Set Up AWS Parameter Integration</title>
@@ -286,6 +305,31 @@ export default function AWSParameterStoreCreateIntegrationPage() {
</FormControl> </FormControl>
</div> </div>
)} )}
<FormControl label="Encryption Key" className="mt-4">
<Select
value={kmsKeyId}
onValueChange={(e) => {
setKmsKeyId(e)
}}
className="w-full border border-mineshaft-500"
>
{integrationAuthAwsKmsKeys?.length ? (
integrationAuthAwsKmsKeys.map((key) => {
return (
<SelectItem
value={key.id as string}
key={`repo-id-${key.id}`}
className="w-[28.4rem] text-sm"
>
{key.alias}
</SelectItem>
);
})
) : (
<div />
)}
</Select>
</FormControl>
</motion.div> </motion.div>
</TabPanel> </TabPanel>
</Tabs> </Tabs>
@@ -318,7 +362,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
<title>Set Up AWS Parameter Store Integration</title> <title>Set Up AWS Parameter Store Integration</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Head> </Head>
{isintegrationAuthLoading ? ( {(isintegrationAuthLoading || isIntegrationAuthAwsKmsKeysLoading) ? (
<img <img
src="/images/loading/loading.gif" src="/images/loading/loading.gif"
height={70} height={70}
@@ -148,7 +148,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
}] }]
} }
: {}), : {}),
...((kmsKeyId && integrationAuthAwsKmsKeys?.filter(key => key.id === kmsKeyId)[0]?.alias !== "alias/aws/secretsmanager") ? ...((kmsKeyId && integrationAuthAwsKmsKeys?.filter(key => key.id === kmsKeyId)[0]?.alias !== "default") ?
{ {
kmsKeyId kmsKeyId
}: {}) }: {})
@@ -21,15 +21,17 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { CloudIntegrationSection } from "./components/CloudIntegrationSection"; import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection"; import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
import { InfrastructureIntegrationSection } from "./components/InfrastructureIntegrationSection/InfrastructureIntegrationSection";
import { IntegrationsSection } from "./components/IntegrationsSection"; import { IntegrationsSection } from "./components/IntegrationsSection";
import { generateBotKey, redirectForProviderAuth } from "./IntegrationPage.utils"; import { generateBotKey, redirectForProviderAuth } from "./IntegrationPage.utils";
type Props = { type Props = {
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>; frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
infrastructureIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
}; };
export const IntegrationsPage = withProjectPermission( export const IntegrationsPage = withProjectPermission(
({ frameworkIntegrations }: Props) => { ({ frameworkIntegrations, infrastructureIntegrations }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -228,6 +230,7 @@ export const IntegrationsPage = withProjectPermission(
</ModalContent> </ModalContent>
</Modal> </Modal>
<FrameworkIntegrationSection frameworks={frameworkIntegrations} /> <FrameworkIntegrationSection frameworks={frameworkIntegrations} />
<InfrastructureIntegrationSection integrations={infrastructureIntegrations} />
</div> </div>
); );
}, },
@@ -109,7 +109,7 @@ export const CloudIntegrationSection = ({
</div> </div>
{cloudIntegration.isAvailable && {cloudIntegration.isAvailable &&
Boolean(integrationAuths?.[cloudIntegration.slug]) && ( Boolean(integrationAuths?.[cloudIntegration.slug]) && (
<div className="absolute top-0 right-0 z-40 h-full"> <div className="absolute top-0 right-0 z-30 h-full">
<div className="relative h-full"> <div className="relative h-full">
<div className="absolute top-0 right-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-tr-md rounded-bl-md bg-primary py-0.5 px-2 text-xs text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0"> <div className="absolute top-0 right-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-tr-md rounded-bl-md bg-primary py-0.5 px-2 text-xs text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0">
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" /> <FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
@@ -1,4 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faKeyboard } from "@fortawesome/free-regular-svg-icons";
import { faComputer } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type Props = { type Props = {
frameworks: Array<{ frameworks: Array<{
@@ -50,6 +53,36 @@ export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
</div> </div>
</a> </a>
))} ))}
<a
key="framework-integration-more"
href="https://infisical.com/docs/cli/commands/run"
rel="noopener noreferrer"
target="_blank"
className="relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
>
<div
className="flex h-full w-full cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 font-semibold text-gray-300 duration-200 hover:bg-mineshaft-700 group-hover:text-gray-200 px-1 text-xl w-full max-w-xs text-center"
>
<FontAwesomeIcon className="text-5xl mb-2 text-white/90" icon={faKeyboard} />
<div className="h-2" />
CLI
</div>
</a>
<a
key="framework-integration-more"
href="https://infisical.com/docs/sdks/overview"
rel="noopener noreferrer"
target="_blank"
className="relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
>
<div
className="flex h-full w-full cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 font-semibold text-gray-300 duration-200 hover:bg-mineshaft-700 group-hover:text-gray-200 px-1 text-xl w-full max-w-xs text-center"
>
<FontAwesomeIcon className="text-5xl mb-1 text-white/90" icon={faComputer} />
<div className="h-2" />
SDKs
</div>
</a>
</div> </div>
</> </>
); );
@@ -0,0 +1,50 @@
type Props = {
integrations: Array<{
name: string;
image: string;
slug: string;
docsLink: string;
}>;
};
export const InfrastructureIntegrationSection = ({ integrations }: Props) => {
const sortedIntegrations = integrations.sort((a, b) => a.name.localeCompare(b.name));
return (
<>
<div className="mx-4 mt-12 mb-4 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Infrastructure Integrations</h1>
<p className="text-base text-gray-400">Click on of the integration to read the documentation.</p>
</div>
<div className="mx-6 grid grid-cols-2 gap-4 lg:grid-cols-3 2xl:grid-cols-4">
{sortedIntegrations.map((integration) => (
<a
key={`framework-integration-${integration.slug}`}
href={integration.docsLink}
rel="noopener noreferrer"
target="_blank"
className="relative w-full flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
className="group relative w-full cursor-pointer duration-200 hover:bg-mineshaft-700 flex h-32 flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
key={integration?.name}
>
<img
src={`/images/integrations/${integration.image}.png`}
height={integration?.name ? 60 : 90}
width={integration?.name ? 60 : 90}
alt="integration logo"
/>
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{integration?.name && integration.name}
</div>
</div>
</a>
))}
</div>
</>
);
};
@@ -0,0 +1 @@
export { InfrastructureIntegrationSection } from "./InfrastructureIntegrationSection";
@@ -137,7 +137,7 @@ export const IntegrationsSection = ({
"App" "App"
} }
/> />
<div className="min-w-[8rem] max-w-[12rem] overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200"> <div className="min-w-[8rem] max-w-[12rem] overflow-scroll no-scrollbar no-scrollbar::-webkit-scrollbar whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{(integration.integration === "hashicorp-vault" && {(integration.integration === "hashicorp-vault" &&
`${integration.app} - path: ${integration.path}`) || `${integration.app} - path: ${integration.path}`) ||
(integration.scope === "github-org" && `${integration.owner}`) || (integration.scope === "github-org" && `${integration.owner}`) ||
@@ -217,11 +217,12 @@ export const IntegrationsSection = ({
isOpen={popUp.deleteConfirmation.isOpen} isOpen={popUp.deleteConfirmation.isOpen}
title={`Are you sure want to remove ${ title={`Are you sure want to remove ${
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " " (popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || " "}?`} } integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || "this project"}?`}
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={ deleteKey={
(popUp?.deleteConfirmation?.data as TIntegration)?.app || (popUp?.deleteConfirmation?.data as TIntegration)?.app ||
(popUp?.deleteConfirmation?.data as TIntegration)?.owner || (popUp?.deleteConfirmation?.data as TIntegration)?.owner ||
(popUp?.deleteConfirmation?.data as TIntegration)?.path ||
"" ""
} }
onDeleteApproved={async () => onDeleteApproved={async () =>