diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index be460b4b4..eab0f5fb2 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2394,7 +2394,8 @@ export const SecretSyncs = { keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault." }, ONEPASS: { - vaultId: "The ID of the 1Password vault to sync secrets to." + vaultId: "The ID of the 1Password vault to sync secrets to.", + valueLabel: "The label of the entry that holds the secret value." }, HEROKU: { app: "The ID of the Heroku app to sync secrets to.", diff --git a/backend/src/services/secret-sync/1password/1password-sync-fns.ts b/backend/src/services/secret-sync/1password/1password-sync-fns.ts index 9305f2e3c..4bc51b2ad 100644 --- a/backend/src/services/secret-sync/1password/1password-sync-fns.ts +++ b/backend/src/services/secret-sync/1password/1password-sync-fns.ts @@ -6,7 +6,6 @@ import { TOnePassListVariablesResponse, TOnePassSyncWithCredentials, TOnePassVariable, - TOnePassVariableDetails, TPostOnePassVariable, TPutOnePassVariable } from "@app/services/secret-sync/1password/1password-sync-types"; @@ -14,7 +13,10 @@ import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; -const listOnePassItems = async ({ instanceUrl, apiToken, vaultId }: TOnePassListVariables) => { +// This should not be changed or it may break existing logic +const VALUE_LABEL_DEFAULT = "value"; + +const listOnePassItems = async ({ instanceUrl, apiToken, vaultId, valueLabel }: TOnePassListVariables) => { const { data } = await request.get(`${instanceUrl}/v1/vaults/${vaultId}/items`, { headers: { Authorization: `Bearer ${apiToken}`, @@ -22,36 +24,49 @@ const listOnePassItems = async ({ instanceUrl, apiToken, vaultId }: TOnePassList } }); - const result: Record = {}; + const items: Record = {}; + const duplicates: Record = {}; for await (const s of data) { - const { data: secret } = await request.get( - `${instanceUrl}/v1/vaults/${vaultId}/items/${s.id}`, - { - headers: { - Authorization: `Bearer ${apiToken}`, - Accept: "application/json" - } - } - ); + // eslint-disable-next-line no-continue + if (s.category !== "API_CREDENTIAL") continue; - const value = secret.fields.find((f) => f.label === "value")?.value; - const fieldId = secret.fields.find((f) => f.label === "value")?.id; + if (items[s.title]) { + duplicates[s.id] = s.title; + // eslint-disable-next-line no-continue + continue; + } + + const { data: secret } = await request.get(`${instanceUrl}/v1/vaults/${vaultId}/items/${s.id}`, { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + }); + + const valueField = secret.fields.find((f) => f.label === valueLabel); // eslint-disable-next-line no-continue - if (!value || !fieldId) continue; + if (!valueField || !valueField.value || !valueField.id) continue; - result[s.title] = { + items[s.title] = { ...secret, - value, - fieldId + value: valueField.value, + fieldId: valueField.id }; } - return result; + return { items, duplicates }; }; -const createOnePassItem = async ({ instanceUrl, apiToken, vaultId, itemTitle, itemValue }: TPostOnePassVariable) => { +const createOnePassItem = async ({ + instanceUrl, + apiToken, + vaultId, + itemTitle, + itemValue, + valueLabel +}: TPostOnePassVariable) => { return request.post( `${instanceUrl}/v1/vaults/${vaultId}/items`, { @@ -63,7 +78,7 @@ const createOnePassItem = async ({ instanceUrl, apiToken, vaultId, itemTitle, it tags: ["synced-from-infisical"], fields: [ { - label: "value", + label: valueLabel, value: itemValue, type: "CONCEALED" } @@ -85,7 +100,9 @@ const updateOnePassItem = async ({ itemId, fieldId, itemTitle, - itemValue + itemValue, + valueLabel, + otherFields }: TPutOnePassVariable) => { return request.put( `${instanceUrl}/v1/vaults/${vaultId}/items/${itemId}`, @@ -98,9 +115,10 @@ const updateOnePassItem = async ({ }, tags: ["synced-from-infisical"], fields: [ + ...otherFields, { id: fieldId, - label: "value", + label: valueLabel, value: itemValue, type: "CONCEALED" } @@ -128,13 +146,18 @@ export const OnePassSyncFns = { const { connection, environment, - destinationConfig: { vaultId } + destinationConfig: { vaultId, valueLabel } } = secretSync; const instanceUrl = await getOnePassInstanceUrl(connection); const { apiToken } = connection.credentials; - const items = await listOnePassItems({ instanceUrl, apiToken, vaultId }); + const { items, duplicates } = await listOnePassItems({ + instanceUrl, + apiToken, + vaultId, + valueLabel: valueLabel || VALUE_LABEL_DEFAULT + }); for await (const entry of Object.entries(secretMap)) { const [key, { value }] = entry; @@ -148,10 +171,19 @@ export const OnePassSyncFns = { itemTitle: key, itemValue: value, itemId: items[key].id, - fieldId: items[key].fieldId + fieldId: items[key].fieldId, + valueLabel: valueLabel || VALUE_LABEL_DEFAULT, + otherFields: items[key].fields.filter((field) => field.label !== (valueLabel || VALUE_LABEL_DEFAULT)) }); } else { - await createOnePassItem({ instanceUrl, apiToken, vaultId, itemTitle: key, itemValue: value }); + await createOnePassItem({ + instanceUrl, + apiToken, + vaultId, + itemTitle: key, + itemValue: value, + valueLabel: valueLabel || VALUE_LABEL_DEFAULT + }); } } catch (error) { throw new SecretSyncError({ @@ -163,7 +195,28 @@ export const OnePassSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; - for await (const [key, variable] of Object.entries(items)) { + // Delete duplicate item entries + for await (const [itemId, key] of Object.entries(duplicates)) { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue; + + try { + await deleteOnePassItem({ + instanceUrl, + apiToken, + vaultId, + itemId + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + // Delete item entries that are not in secretMap + for await (const [key, item] of Object.entries(items)) { // eslint-disable-next-line no-continue if (!matchesSchema(key, environment?.slug || "", secretSync.syncOptions.keySchema)) continue; @@ -173,7 +226,7 @@ export const OnePassSyncFns = { instanceUrl, apiToken, vaultId, - itemId: variable.id + itemId: item.id }); } catch (error) { throw new SecretSyncError({ @@ -187,13 +240,18 @@ export const OnePassSyncFns = { removeSecrets: async (secretSync: TOnePassSyncWithCredentials, secretMap: TSecretMap) => { const { connection, - destinationConfig: { vaultId } + destinationConfig: { vaultId, valueLabel } } = secretSync; const instanceUrl = await getOnePassInstanceUrl(connection); const { apiToken } = connection.credentials; - const items = await listOnePassItems({ instanceUrl, apiToken, vaultId }); + const { items } = await listOnePassItems({ + instanceUrl, + apiToken, + vaultId, + valueLabel: valueLabel || VALUE_LABEL_DEFAULT + }); for await (const [key, item] of Object.entries(items)) { if (key in secretMap) { @@ -216,12 +274,12 @@ export const OnePassSyncFns = { getSecrets: async (secretSync: TOnePassSyncWithCredentials) => { const { connection, - destinationConfig: { vaultId } + destinationConfig: { vaultId, valueLabel } } = secretSync; const instanceUrl = await getOnePassInstanceUrl(connection); const { apiToken } = connection.credentials; - return listOnePassItems({ instanceUrl, apiToken, vaultId }); + return listOnePassItems({ instanceUrl, apiToken, vaultId, valueLabel: valueLabel || VALUE_LABEL_DEFAULT }); } }; diff --git a/backend/src/services/secret-sync/1password/1password-sync-schemas.ts b/backend/src/services/secret-sync/1password/1password-sync-schemas.ts index 2f77a1dad..9ee4ca496 100644 --- a/backend/src/services/secret-sync/1password/1password-sync-schemas.ts +++ b/backend/src/services/secret-sync/1password/1password-sync-schemas.ts @@ -11,7 +11,8 @@ import { import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; const OnePassSyncDestinationConfigSchema = z.object({ - vaultId: z.string().trim().min(1, "Vault required").describe(SecretSyncs.DESTINATION_CONFIG.ONEPASS.vaultId) + vaultId: z.string().trim().min(1, "Vault required").describe(SecretSyncs.DESTINATION_CONFIG.ONEPASS.vaultId), + valueLabel: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.ONEPASS.valueLabel) }); const OnePassSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; diff --git a/backend/src/services/secret-sync/1password/1password-sync-types.ts b/backend/src/services/secret-sync/1password/1password-sync-types.ts index af4db7369..8e6c1da6a 100644 --- a/backend/src/services/secret-sync/1password/1password-sync-types.ts +++ b/backend/src/services/secret-sync/1password/1password-sync-types.ts @@ -14,29 +14,32 @@ export type TOnePassSyncWithCredentials = TOnePassSync & { connection: TOnePassConnection; }; +type Field = { + id: string; + type: string; // CONCEALED, STRING + label: string; + value: string; +}; + export type TOnePassVariable = { id: string; title: string; category: string; // API_CREDENTIAL, SECURE_NOTE, LOGIN, etc -}; - -export type TOnePassVariableDetails = TOnePassVariable & { - fields: { - id: string; - type: string; // CONCEALED, STRING - label: string; - value: string; - }[]; + fields: Field[]; }; export type TOnePassListVariablesResponse = TOnePassVariable[]; -export type TOnePassListVariables = { +type TOnePassBase = { apiToken: string; instanceUrl: string; vaultId: string; }; +export type TOnePassListVariables = TOnePassBase & { + valueLabel: string; +}; + export type TPostOnePassVariable = TOnePassListVariables & { itemTitle: string; itemValue: string; @@ -47,8 +50,9 @@ export type TPutOnePassVariable = TOnePassListVariables & { fieldId: string; itemTitle: string; itemValue: string; + otherFields: Field[]; }; -export type TDeleteOnePassVariable = TOnePassListVariables & { +export type TDeleteOnePassVariable = TOnePassBase & { itemId: string; }; diff --git a/docs/integrations/secret-syncs/1password.mdx b/docs/integrations/secret-syncs/1password.mdx index 6e2b96b4a..17a65a1ec 100644 --- a/docs/integrations/secret-syncs/1password.mdx +++ b/docs/integrations/secret-syncs/1password.mdx @@ -94,7 +94,8 @@ description: "Learn how to configure a 1Password Sync for Infisical." "initialSyncBehavior": "overwrite-destination" }, "destinationConfig": { - "vaultId": "..." + "vaultId": "...", + "valueLabel": "value" } }' ``` @@ -145,7 +146,8 @@ description: "Learn how to configure a 1Password Sync for Infisical." }, "destination": "1password", "destinationConfig": { - "vaultId": "..." + "vaultId": "...", + "valueLabel": "value" } } } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx index 1202ec875..3408c192f 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx @@ -4,7 +4,7 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; -import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2"; +import { FilterableSelect, FormControl, Input, Tooltip } from "@app/components/v2"; import { TOnePassVault, useOnePassConnectionListVaults @@ -32,6 +32,7 @@ export const OnePassSyncFields = () => { { setValue("destinationConfig.vaultId", ""); + setValue("destinationConfig.valueLabel", ""); }} /> @@ -69,6 +70,22 @@ export const OnePassSyncFields = () => { )} /> + + ( + + + + )} + control={control} + name="destinationConfig.valueLabel" + /> ); }; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/OnePassSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/OnePassSyncReviewFields.tsx index 1c31fb6c3..6b11f7ef5 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/OnePassSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/OnePassSyncReviewFields.tsx @@ -7,6 +7,12 @@ import { SecretSync } from "@app/hooks/api/secretSyncs"; export const OnePassSyncReviewFields = () => { const { watch } = useFormContext(); const vaultId = watch("destinationConfig.vaultId"); + const valueLabel = watch("destinationConfig.valueLabel"); - return {vaultId}; + return ( + <> + {vaultId} + {valueLabel || "value"} + + ); }; diff --git a/frontend/src/components/secret-syncs/forms/schemas/1password-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/1password-sync-destination-schema.ts index 36b144776..a4cddd7b6 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/1password-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/1password-sync-destination-schema.ts @@ -7,7 +7,8 @@ export const OnePassSyncDestinationSchema = BaseSecretSyncSchema().merge( z.object({ destination: z.literal(SecretSync.OnePass), destinationConfig: z.object({ - vaultId: z.string().trim().min(1, "Vault ID required") + vaultId: z.string().trim().min(1, "Vault ID required"), + valueLabel: z.string().trim().optional() }) }) ); diff --git a/frontend/src/hooks/api/secretSyncs/types/1password-sync.ts b/frontend/src/hooks/api/secretSyncs/types/1password-sync.ts index 98556d2af..cf8fcfc29 100644 --- a/frontend/src/hooks/api/secretSyncs/types/1password-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/1password-sync.ts @@ -6,6 +6,7 @@ export type TOnePassSync = TRootSecretSync & { destination: SecretSync.OnePass; destinationConfig: { vaultId: string; + valueLabel?: string; }; connection: { app: AppConnection.OnePass; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/1PasswordSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/1PasswordSyncDestinationSection.tsx index 09d598f31..b83e52596 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/1PasswordSyncDestinationSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/1PasswordSyncDestinationSection.tsx @@ -7,8 +7,13 @@ type Props = { export const OnePassSyncDestinationSection = ({ secretSync }: Props) => { const { - destinationConfig: { vaultId } + destinationConfig: { vaultId, valueLabel } } = secretSync; - return {vaultId}; + return ( + <> + {vaultId} + {valueLabel || "value"} + + ); };