diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 70e3435a2..e693a7fea 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -257,14 +257,27 @@ const syncSecretsGCPSecretManager = async ({ const syncSecretsAzureKeyVault = async ({ integration, secrets, - accessToken + accessToken, + createManySecretsRawFn, + updateManySecretsRawFn }: { - integration: TIntegrations; + integration: TIntegrations & { + projectId: string; + environment: { + id: string; + name: string; + slug: string; + }; + secretPath: string; + }; secrets: Record; accessToken: string; + createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise>; + updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise>; }) => { interface GetAzureKeyVaultSecret { id: string; // secret URI + value: string; attributes: { enabled: true; created: number; @@ -361,6 +374,83 @@ const syncSecretsAzureKeyVault = async ({ } }); + const secretsToAdd: { [key: string]: string } = {}; + const secretsToUpdate: { [key: string]: string } = {}; + const secretKeysToRemoveFromDelete = new Set(); + + const metadata = IntegrationMetadataSchema.parse(integration.metadata); + if (!integration.lastUsed) { + Object.keys(res).forEach((key) => { + // first time using integration + const underscoredKey = key.replace(/-/g, "_"); + + // -> apply initial sync behavior + switch (metadata.initialSyncBehavior) { + case IntegrationInitialSyncBehavior.PREFER_TARGET: { + if (!(underscoredKey in secrets)) { + secretsToAdd[underscoredKey] = res[key].value; + setSecrets.push({ + key, + value: res[key].value + }); + } else if (secrets[underscoredKey]?.value !== res[key].value) { + secretsToUpdate[underscoredKey] = res[key].value; + const toEditSecretIndex = setSecrets.findIndex((secret) => secret.key === key); + if (toEditSecretIndex >= 0) { + setSecrets[toEditSecretIndex].value = res[key].value; + } + } + + secretKeysToRemoveFromDelete.add(key); + + break; + } + case IntegrationInitialSyncBehavior.PREFER_SOURCE: { + if (!(underscoredKey in secrets)) { + secretsToAdd[underscoredKey] = res[key].value; + setSecrets.push({ + key, + value: res[key].value + }); + } + + secretKeysToRemoveFromDelete.add(key); + break; + } + default: + break; + } + }); + } + + if (Object.keys(secretsToUpdate).length) { + await updateManySecretsRawFn({ + projectId: integration.projectId, + environment: integration.environment.slug, + path: integration.secretPath, + secrets: Object.keys(secretsToUpdate).map((key) => ({ + secretName: key, + secretValue: secretsToUpdate[key], + type: SecretType.Shared, + secretComment: "" + })) + }); + } + + if (Object.keys(secretsToAdd).length) { + await createManySecretsRawFn({ + projectId: integration.projectId, + environment: integration.environment.slug, + path: integration.secretPath, + secrets: Object.keys(secretsToAdd).map((key) => ({ + secretName: key, + secretValue: secretsToAdd[key], + type: SecretType.Shared, + secretComment: "" + })) + }); + } + const setSecretAzureKeyVault = async ({ key, value, @@ -428,7 +518,7 @@ const syncSecretsAzureKeyVault = async ({ }); } - for await (const deleteSecret of deleteSecrets) { + for await (const deleteSecret of deleteSecrets.filter((secret) => !secretKeysToRemoveFromDelete.has(secret.key))) { const { key } = deleteSecret; await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { headers: { @@ -3512,7 +3602,9 @@ export const syncIntegrationSecrets = async ({ await syncSecretsAzureKeyVault({ integration, secrets, - accessToken + accessToken, + createManySecretsRawFn, + updateManySecretsRawFn }); break; case Integrations.AWS_PARAMETER_STORE: diff --git a/frontend/src/pages/integrations/azure-key-vault/create.tsx b/frontend/src/pages/integrations/azure-key-vault/create.tsx index acf2c61bd..e6ed57302 100644 --- a/frontend/src/pages/integrations/azure-key-vault/create.tsx +++ b/frontend/src/pages/integrations/azure-key-vault/create.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import queryString from "query-string"; import { useCreateIntegration } from "@app/hooks/api"; +import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; import { Button, @@ -16,6 +17,18 @@ import { import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; +const initialSyncBehaviors = [ + { + label: "No Import - Overwrite all values in Azure Vault", + value: IntegrationSyncBehavior.OVERWRITE_TARGET + }, + { + label: "Import - Prefer values from Azure Vault", + value: IntegrationSyncBehavior.PREFER_TARGET + }, + { label: "Import - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE } +]; + export default function AzureKeyVaultCreateIntegrationPage() { const router = useRouter(); const { mutateAsync } = useCreateIntegration(); @@ -30,6 +43,9 @@ export default function AzureKeyVaultCreateIntegrationPage() { const [vaultBaseUrl, setVaultBaseUrl] = useState(""); const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState(""); + const [initialSyncBehavior, setInitialSyncBehavior] = useState( + IntegrationSyncBehavior.PREFER_SOURCE + ); const [isLoading, setIsLoading] = useState(false); @@ -59,7 +75,10 @@ export default function AzureKeyVaultCreateIntegrationPage() { isActive: true, app: vaultBaseUrl, sourceEnvironment: selectedSourceEnvironment, - secretPath + secretPath, + metadata: { + initialSyncBehavior + } }); setIsLoading(false); @@ -107,6 +126,21 @@ export default function AzureKeyVaultCreateIntegrationPage() { onChange={(e) => setVaultBaseUrl(e.target.value)} /> + + +