diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index da443fe68..6dba0150f 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -597,19 +597,27 @@ export const expandSecretReferencesFactory = ({ return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; } - const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); - if (!folder) return { value: "", tags: [] }; - const secrets = await secretDAL.findByFolderId({ folderId: folder.id }); + try { + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return { value: "", tags: [] }; + const secrets = await secretDAL.findByFolderId({ folderId: folder.id }); - const decryptedSecret = secrets.reduce>((prev, secret) => { - // eslint-disable-next-line no-param-reassign - prev[secret.key] = { value: decryptSecret(secret.encryptedValue) || "", tags: secret.tags?.map((el) => el.slug) }; - return prev; - }, {}); + const decryptedSecret = secrets.reduce>((prev, secret) => { + // eslint-disable-next-line no-param-reassign + prev[secret.key] = { + value: decryptSecret(secret.encryptedValue) || "", + tags: secret.tags?.map((el) => el.slug) + }; + return prev; + }, {}); - secretCache[cacheKey] = decryptedSecret; + secretCache[cacheKey] = decryptedSecret; - return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; + return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; + } catch (error) { + secretCache[cacheKey] = {}; + return { value: "", tags: [] }; + } }; const recursivelyExpandSecret = async (dto: { @@ -669,6 +677,7 @@ export const expandSecretReferencesFactory = ({ }); const cacheKey = getCacheUniqueKey(environment, secretPath); + if (!secretCache[cacheKey]) secretCache[cacheKey] = {}; secretCache[cacheKey][secretKey] = referredValue; referencedSecretValue = referredValue.value; @@ -688,6 +697,7 @@ export const expandSecretReferencesFactory = ({ }); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); + if (!secretCache[cacheKey]) secretCache[cacheKey] = {}; secretCache[cacheKey][secretReferenceKey] = referedValue; referencedSecretValue = referedValue.value; diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index f9ab8eb92..de8f930ed 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -159,17 +159,14 @@ export const secretV2BridgeServiceFactory = ({ const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment))); const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx); - if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length) - throw new BadRequestError({ - message: `Referenced environment not found. Missing ${diff( - uniqueReferenceEnvironmentSlugs, - referencesEnvironments.map((el) => el.slug) - ).join(",")}` - }); - + // Filter out references to non-existent environments const referencesEnvironmentGroupBySlug = groupBy(referencesEnvironments, (i) => i.slug); + const validEnvironmentReferences = references.filter((el) => referencesEnvironmentGroupBySlug[el.environment]); + + if (validEnvironmentReferences.length === 0) return; + const referredFolders = await folderDAL.findByManySecretPath( - references.map((el) => ({ + validEnvironmentReferences.map((el) => ({ secretPath: el.secretPath, envId: referencesEnvironmentGroupBySlug[el.environment][0].id })), @@ -177,58 +174,71 @@ export const secretV2BridgeServiceFactory = ({ ); const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`); + + // Find only references that have valid folders (don't throw for missing paths) + const validReferences = validEnvironmentReferences.filter((el) => { + const folderId = + referencesFolderGroupByPath[`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`]?.[0] + ?.id; + return folderId; + }); + + if (validReferences.length === 0) return; + const referredSecrets = await secretDAL.find( { $complex: { operator: "or", - value: references.map((el) => { - const folderId = - referencesFolderGroupByPath[ - `${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}` - ][0]?.id; - if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` }); + value: validReferences + .map((el) => { + const folderGroup = + referencesFolderGroupByPath[ + `${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}` + ]; + if (!folderGroup || !folderGroup[0]) return null; - return { - operator: "and", - value: [ - { - operator: "eq", - field: "folderId", - value: folderId - }, - { - operator: "eq", - field: `${TableName.SecretV2}.key` as "key", - value: el.secretKey - } - ] - }; - }) + const folderId = folderGroup[0].id; + + return { + operator: "and", + value: [ + { + operator: "eq", + field: "folderId", + value: folderId + }, + { + operator: "eq", + field: `${TableName.SecretV2}.key` as "key", + value: el.secretKey + } + ] + }; + }) + .filter((query) => query !== null) as Array<{ + operator: "and"; + value: Array<{ + operator: "eq"; + field: "folderId" | "key"; + value: string; + }>; + }> } }, { tx } ); - if ( - referredSecrets.length !== - new Set(references.map(({ secretKey, secretPath, environment }) => `${secretKey}.${secretPath}.${environment}`)) - .size // only count unique references - ) - throw new BadRequestError({ - message: `Referenced secret(s) not found: ${diff( - references.map((el) => el.secretKey), - referredSecrets.map((el) => el.key) - ).join(",")}` - }); - - const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key); - references.forEach((el) => { - throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { - environment: el.environment, - secretPath: el.secretPath, - secretName: el.secretKey, - secretTags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug) - }); + // Only check permissions for secrets that actually exist + referredSecrets.forEach((secret) => { + const reference = validReferences.find((ref) => ref.secretKey === secret.key); + if (reference) { + throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, { + environment: reference.environment, + secretPath: reference.secretPath, + secretName: reference.secretKey, + secretTags: secret.tags?.map((i) => i.slug) + }); + } }); return referredSecrets; @@ -548,6 +558,14 @@ export const secretV2BridgeServiceFactory = ({ ); } + if (secretValue) { + const { nestedReferences, localReferences } = getAllSecretReferences(secretValue); + const allSecretReferences = nestedReferences.concat( + localReferences.map((el) => ({ secretKey: el, secretPath, environment })) + ); + await $validateSecretReferences(projectId, permission, allSecretReferences); + } + const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId @@ -3161,6 +3179,7 @@ export const secretV2BridgeServiceFactory = ({ getSecretById, getAccessibleSecrets, getSecretVersionsByIds, - findSecretIdsByFolderIdAndKeys + findSecretIdsByFolderIdAndKeys, + $validateSecretReferences }; }; diff --git a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx index 347be1b4f..7e3d38f4f 100644 --- a/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx +++ b/frontend/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx @@ -1,5 +1,5 @@ import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react"; -import { faFolder, faKey, faLayerGroup } from "@fortawesome/free-solid-svg-icons"; +import { faFolder, faKey, faLayerGroup, faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as Popover from "@radix-ui/react-popover"; @@ -177,13 +177,29 @@ export const InfisicalSecretInput = forwardRef( type: ReferenceType.SECRET }); }); + + if (suggestionsArr.length === 0 && suggestionSource.predicate.trim()) { + suggestionsArr.push({ + label: "No matches found", + slug: "__no_match__", + type: ReferenceType.SECRET + }); + } + return suggestionsArr; - }, [secrets, folders, currentProject?.environments, isPopupOpen, suggestionSource.value]); + }, [ + secrets, + folders, + currentProject?.environments, + isPopupOpen, + suggestionSource.value, + suggestionSource.predicate + ]); const handleSuggestionSelect = (selectIndex?: number) => { const selectedSuggestion = suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex]; - if (!selectedSuggestion) { + if (!selectedSuggestion || selectedSuggestion.slug === "__no_match__") { return; } @@ -228,21 +244,40 @@ export const InfisicalSecretInput = forwardRef( if (isPopupOpen) { if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) { setHighlightedIndex((prevIndex) => { - const pos = mod(prevIndex + 1, suggestions.length); - popoverContentRef.current?.children?.[pos]?.scrollIntoView({ + let nextIndex = mod(prevIndex + 1, suggestions.length); + // Skip "no match" messages + while ( + nextIndex < suggestions.length && + suggestions[nextIndex].slug === "__no_match__" + ) { + nextIndex = mod(nextIndex + 1, suggestions.length); + } + // If we only have no-match messages, don't highlight anything + if (suggestions[nextIndex]?.slug === "__no_match__") { + return -1; + } + popoverContentRef.current?.children?.[nextIndex]?.scrollIntoView({ block: "nearest", behavior: "smooth" }); - return pos; + return nextIndex; }); } else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) { setHighlightedIndex((prevIndex) => { - const pos = mod(prevIndex - 1, suggestions.length); - popoverContentRef.current?.children?.[pos]?.scrollIntoView({ + let prevIdx = mod(prevIndex - 1, suggestions.length); + // Skip "no match" messages + while (prevIdx >= 0 && suggestions[prevIdx].slug === "__no_match__") { + prevIdx = mod(prevIdx - 1, suggestions.length); + } + // If we only have no-match messages, don't highlight anything + if (suggestions[prevIdx]?.slug === "__no_match__") { + return -1; + } + popoverContentRef.current?.children?.[prevIdx]?.scrollIntoView({ block: "nearest", behavior: "smooth" }); - return pos; + return prevIdx; }); } else if (e.key === "Enter" && highlightedIndex >= 0) { e.preventDefault(); @@ -311,7 +346,12 @@ export const InfisicalSecretInput = forwardRef( {suggestions.map((item, i) => { let entryIcon; let subText; - if (item.type === ReferenceType.SECRET) { + const isNoMatchMessage = item.slug === "__no_match__"; + + if (isNoMatchMessage) { + entryIcon = ; + subText = "No results"; + } else if (item.type === ReferenceType.SECRET) { entryIcon = ; subText = "Secret"; } else if (item.type === ReferenceType.ENVIRONMENT) { @@ -322,10 +362,28 @@ export const InfisicalSecretInput = forwardRef( subText = "Folder"; } - return ( + return isNoMatchMessage ? (
+
+
+
{entryIcon}
+
+ {item.label} +
+ {subText} +
+
+
+
+
+ ) : ( + ); })} diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 60ab5848f..69a6bf49f 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -29,7 +29,8 @@ const syntaxHighlight = ( skipNext = true; return ( - ${{el.slice(2, -1)} + ${ + {el.slice(2, -1)} } );