From c3c907788aad527d72c694fb67ca5be4983b2221 Mon Sep 17 00:00:00 2001 From: x032205 Date: Mon, 12 May 2025 16:42:48 -0400 Subject: [PATCH] review fixes --- backend/src/db/schemas/identity-oci-auths.ts | 26 +++++++ .../app-connection/oci/oci-connection-fns.ts | 15 ++-- .../oci-vault/oci-vault-sync-fns.ts | 72 ++++++++++++++----- docs/integrations/secret-syncs/oci-vault.mdx | 18 +++++ .../pages/organization/SettingsPage/route.tsx | 38 +++++----- 5 files changed, 123 insertions(+), 46 deletions(-) create mode 100644 backend/src/db/schemas/identity-oci-auths.ts diff --git a/backend/src/db/schemas/identity-oci-auths.ts b/backend/src/db/schemas/identity-oci-auths.ts new file mode 100644 index 000000000..e0be86b78 --- /dev/null +++ b/backend/src/db/schemas/identity-oci-auths.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const IdentityOciAuthsSchema = z.object({ + id: z.string().uuid(), + accessTokenTTL: z.coerce.number().default(7200), + accessTokenMaxTTL: z.coerce.number().default(7200), + accessTokenNumUsesLimit: z.coerce.number().default(0), + accessTokenTrustedIps: z.unknown(), + createdAt: z.date(), + updatedAt: z.date(), + identityId: z.string().uuid(), + type: z.string(), + tenancyOcid: z.string(), + allowedUsernames: z.string().nullable().optional() +}); + +export type TIdentityOciAuths = z.infer; +export type TIdentityOciAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityOciAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/services/app-connection/oci/oci-connection-fns.ts b/backend/src/services/app-connection/oci/oci-connection-fns.ts index c93a668d4..5dcf6ee7a 100644 --- a/backend/src/services/app-connection/oci/oci-connection-fns.ts +++ b/backend/src/services/app-connection/oci/oci-connection-fns.ts @@ -117,18 +117,19 @@ export const listOCIVaults = async (appConnection: TOCIConnection, compartmentOc export const listOCIVaultKeys = async (appConnection: TOCIConnection, compartmentOcid: string, vaultOcid: string) => { const provider = await getOCIProvider(appConnection); - const vaultIdMatch = vaultOcid.match(/ocid1\.vault\.[^.]+\.[^.]+\.([^.]+)/); - if (!vaultIdMatch || !vaultIdMatch[1]) { - throw new BadRequestError({ - message: "Invalid vault OCID format" - }); - } + const kmsVaultClient = new keymanagement.KmsVaultClient({ + authenticationDetailsProvider: provider + }); + + const vault = await kmsVaultClient.getVault({ + vaultId: vaultOcid + }); const keyManagementClient = new keymanagement.KmsManagementClient({ authenticationDetailsProvider: provider }); - keyManagementClient.endpoint = `https://${vaultIdMatch[1].replace(/[^a-zA-Z0-9]/g, "")}-management.kms.${appConnection.credentials.region}.oraclecloud.com`; + keyManagementClient.endpoint = vault.vault.managementEndpoint; const keys = await keyManagementClient.listKeys({ compartmentId: compartmentOcid diff --git a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts index a352a23c8..3137013d0 100644 --- a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts @@ -1,5 +1,6 @@ import { secrets, vault } from "oci-sdk"; +import { delay } from "@app/lib/delay"; import { getOCIProvider } from "@app/services/app-connection/oci"; import { TCreateOCIVaultVariable, @@ -121,11 +122,28 @@ export const OCIVaultSyncFns = { const provider = await getOCIProvider(connection); const variables = await listOCIVaultVariables({ provider, compartmentId: compartmentOcid, vaultId: vaultOcid }); + // Throw an error if any keys are updating in OCI vault to prevent skipped updates + if ( + Object.entries(variables).some( + ([, secret]) => + secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Updating || + secret.lifecycleState === vault.models.SecretSummary.LifecycleState.CancellingDeletion || + secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Creating || + secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Deleting || + secret.lifecycleState === vault.models.SecretSummary.LifecycleState.SchedulingDeletion + ) + ) { + throw new SecretSyncError({ + error: "Cannot sync while keys are updating in OCI Vault." + }); + } + // Create secrets for await (const entry of Object.entries(secretMap)) { const [key, { value }] = entry; + const existingVariable = Object.values(variables).find((v) => v.secretName === key); - if (!Object.values(variables).some((v) => v.secretName === key)) { + if (!existingVariable) { try { await createOCIVaultVariable({ compartmentId: compartmentOcid, @@ -141,27 +159,45 @@ export const OCIVaultSyncFns = { secretKey: key }); } - } else { + } else if (existingVariable.lifecycleState === vault.models.SecretSummary.LifecycleState.PendingDeletion) { // If a secret exists but is pending deletion, cancel the deletion and update the secret - const secretPendingDeletion = Object.values(variables).find( - (s) => s.secretName === key && s.lifecycleState === vault.models.SecretSummary.LifecycleState.PendingDeletion - ); + await unmarkOCIVaultVariableFromDeletion({ + provider, + compartmentId: compartmentOcid, + vaultId: vaultOcid, + secretId: existingVariable.id + }); - if (secretPendingDeletion) { - await unmarkOCIVaultVariableFromDeletion({ - provider, - compartmentId: compartmentOcid, - vaultId: vaultOcid, - secretId: secretPendingDeletion.id + const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider }); + const MAX_RETRIES = 10; + + for (let i = 0; i < MAX_RETRIES; i += 1) { + // eslint-disable-next-line no-await-in-loop + await delay(5000); + + // eslint-disable-next-line no-await-in-loop + const secret = await vaultsClient.getSecret({ + secretId: existingVariable.id }); - await updateOCIVaultVariable({ - provider, - compartmentId: compartmentOcid, - vaultId: vaultOcid, - secretId: secretPendingDeletion.id, - value - }); + if (secret.secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) { + // eslint-disable-next-line no-await-in-loop + await updateOCIVaultVariable({ + provider, + compartmentId: compartmentOcid, + vaultId: vaultOcid, + secretId: existingVariable.id, + value + }); + break; + } + + if (i === MAX_RETRIES - 1) { + throw new SecretSyncError({ + error: "Failed to update secret after cancelling deletion.", + secretKey: key + }); + } } } } diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx index 5b5aa4eb4..7f3c5fd19 100644 --- a/docs/integrations/secret-syncs/oci-vault.mdx +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -157,3 +157,21 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f ``` + +## FAQ + + + + When Infisical attempts to sync secrets, the sync will fail and attempt to re-sync if **any secret** has one of the following lifecycle states: + - SchedulingDeletion + - CancellingDeletion + - Deleting + - Creating + - Updating + + We do this to prevent any desync issues. + + + In the case that a variable is created or updated while it's scheduled for deletion in OCI Vault, we cancel the deletion and update the variable. This action may take up to a minute since Infisical must wait for OCI to completely cancel the deletion and then update the variable. + + diff --git a/frontend/src/pages/organization/SettingsPage/route.tsx b/frontend/src/pages/organization/SettingsPage/route.tsx index e7fdb706a..3f7beb4e8 100644 --- a/frontend/src/pages/organization/SettingsPage/route.tsx +++ b/frontend/src/pages/organization/SettingsPage/route.tsx @@ -1,37 +1,33 @@ -import { faHome } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { - createFileRoute, - linkOptions, - stripSearchParams, -} from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' -import { z } from 'zod' +import { faHome } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; -import { SettingsPage } from './SettingsPage' +import { SettingsPage } from "./SettingsPage"; const SettingsPageQueryParams = z.object({ - selectedTab: z.string().catch(''), -}) + selectedTab: z.string().catch("") +}); export const Route = createFileRoute( - '/_authenticate/_inject-org-details/_org-layout/organization/settings/', + "/_authenticate/_inject-org-details/_org-layout/organization/settings/" )({ component: SettingsPage, validateSearch: zodValidator(SettingsPageQueryParams), search: { - middlewares: [stripSearchParams({ selectedTab: '' })], + middlewares: [stripSearchParams({ selectedTab: "" })] }, context: () => ({ breadcrumbs: [ { - label: 'Home', + label: "Home", icon: () => , - link: linkOptions({ to: '/' }), + link: linkOptions({ to: "/" }) }, { - label: 'Settings', - }, - ], - }), -}) + label: "Settings" + } + ] + }) +});