Merge pull request #4505 from Infisical/ENG-3639

Stop blocking secret references with no matching reference and improve UI edit secret behavior to better highlight this
This commit is contained in:
carlosmonastyrski
2025-09-19 22:49:52 -03:00
committed by GitHub
4 changed files with 167 additions and 80 deletions
@@ -597,19 +597,27 @@ export const expandSecretReferencesFactory = ({
return secretCache[cacheKey][secretKey] || { value: "", tags: [] }; return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
} }
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); try {
if (!folder) return { value: "", tags: [] }; const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
const secrets = await secretDAL.findByFolderId({ folderId: folder.id }); if (!folder) return { value: "", tags: [] };
const secrets = await secretDAL.findByFolderId({ folderId: folder.id });
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => { const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
prev[secret.key] = { value: decryptSecret(secret.encryptedValue) || "", tags: secret.tags?.map((el) => el.slug) }; prev[secret.key] = {
return prev; 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: { const recursivelyExpandSecret = async (dto: {
@@ -669,6 +677,7 @@ export const expandSecretReferencesFactory = ({
}); });
const cacheKey = getCacheUniqueKey(environment, secretPath); const cacheKey = getCacheUniqueKey(environment, secretPath);
if (!secretCache[cacheKey]) secretCache[cacheKey] = {};
secretCache[cacheKey][secretKey] = referredValue; secretCache[cacheKey][secretKey] = referredValue;
referencedSecretValue = referredValue.value; referencedSecretValue = referredValue.value;
@@ -688,6 +697,7 @@ export const expandSecretReferencesFactory = ({
}); });
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
if (!secretCache[cacheKey]) secretCache[cacheKey] = {};
secretCache[cacheKey][secretReferenceKey] = referedValue; secretCache[cacheKey][secretReferenceKey] = referedValue;
referencedSecretValue = referedValue.value; referencedSecretValue = referedValue.value;
@@ -159,17 +159,14 @@ export const secretV2BridgeServiceFactory = ({
const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment))); const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment)));
const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx); const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx);
if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length) // Filter out references to non-existent environments
throw new BadRequestError({
message: `Referenced environment not found. Missing ${diff(
uniqueReferenceEnvironmentSlugs,
referencesEnvironments.map((el) => el.slug)
).join(",")}`
});
const referencesEnvironmentGroupBySlug = groupBy(referencesEnvironments, (i) => i.slug); 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( const referredFolders = await folderDAL.findByManySecretPath(
references.map((el) => ({ validEnvironmentReferences.map((el) => ({
secretPath: el.secretPath, secretPath: el.secretPath,
envId: referencesEnvironmentGroupBySlug[el.environment][0].id envId: referencesEnvironmentGroupBySlug[el.environment][0].id
})), })),
@@ -177,58 +174,71 @@ export const secretV2BridgeServiceFactory = ({
); );
const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`); 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( const referredSecrets = await secretDAL.find(
{ {
$complex: { $complex: {
operator: "or", operator: "or",
value: references.map((el) => { value: validReferences
const folderId = .map((el) => {
referencesFolderGroupByPath[ const folderGroup =
`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}` referencesFolderGroupByPath[
][0]?.id; `${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`
if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` }); ];
if (!folderGroup || !folderGroup[0]) return null;
return { const folderId = folderGroup[0].id;
operator: "and",
value: [ return {
{ operator: "and",
operator: "eq", value: [
field: "folderId", {
value: folderId operator: "eq",
}, field: "folderId",
{ value: folderId
operator: "eq", },
field: `${TableName.SecretV2}.key` as "key", {
value: el.secretKey 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 } { tx }
); );
if ( // Only check permissions for secrets that actually exist
referredSecrets.length !== referredSecrets.forEach((secret) => {
new Set(references.map(({ secretKey, secretPath, environment }) => `${secretKey}.${secretPath}.${environment}`)) const reference = validReferences.find((ref) => ref.secretKey === secret.key);
.size // only count unique references if (reference) {
) throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
throw new BadRequestError({ environment: reference.environment,
message: `Referenced secret(s) not found: ${diff( secretPath: reference.secretPath,
references.map((el) => el.secretKey), secretName: reference.secretKey,
referredSecrets.map((el) => el.key) secretTags: secret.tags?.map((i) => i.slug)
).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)
});
}); });
return referredSecrets; 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({ const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId projectId
@@ -3161,6 +3179,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretById, getSecretById,
getAccessibleSecrets, getAccessibleSecrets,
getSecretVersionsByIds, getSecretVersionsByIds,
findSecretIdsByFolderIdAndKeys findSecretIdsByFolderIdAndKeys,
$validateSecretReferences
}; };
}; };
@@ -1,5 +1,5 @@
import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
@@ -177,13 +177,29 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
type: ReferenceType.SECRET type: ReferenceType.SECRET
}); });
}); });
if (suggestionsArr.length === 0 && suggestionSource.predicate.trim()) {
suggestionsArr.push({
label: "No matches found",
slug: "__no_match__",
type: ReferenceType.SECRET
});
}
return suggestionsArr; return suggestionsArr;
}, [secrets, folders, currentProject?.environments, isPopupOpen, suggestionSource.value]); }, [
secrets,
folders,
currentProject?.environments,
isPopupOpen,
suggestionSource.value,
suggestionSource.predicate
]);
const handleSuggestionSelect = (selectIndex?: number) => { const handleSuggestionSelect = (selectIndex?: number) => {
const selectedSuggestion = const selectedSuggestion =
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex]; suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
if (!selectedSuggestion) { if (!selectedSuggestion || selectedSuggestion.slug === "__no_match__") {
return; return;
} }
@@ -228,21 +244,40 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
if (isPopupOpen) { if (isPopupOpen) {
if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) { if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
setHighlightedIndex((prevIndex) => { setHighlightedIndex((prevIndex) => {
const pos = mod(prevIndex + 1, suggestions.length); let nextIndex = mod(prevIndex + 1, suggestions.length);
popoverContentRef.current?.children?.[pos]?.scrollIntoView({ // 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", block: "nearest",
behavior: "smooth" behavior: "smooth"
}); });
return pos; return nextIndex;
}); });
} else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) { } else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) {
setHighlightedIndex((prevIndex) => { setHighlightedIndex((prevIndex) => {
const pos = mod(prevIndex - 1, suggestions.length); let prevIdx = mod(prevIndex - 1, suggestions.length);
popoverContentRef.current?.children?.[pos]?.scrollIntoView({ // 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", block: "nearest",
behavior: "smooth" behavior: "smooth"
}); });
return pos; return prevIdx;
}); });
} else if (e.key === "Enter" && highlightedIndex >= 0) { } else if (e.key === "Enter" && highlightedIndex >= 0) {
e.preventDefault(); e.preventDefault();
@@ -311,7 +346,12 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
{suggestions.map((item, i) => { {suggestions.map((item, i) => {
let entryIcon; let entryIcon;
let subText; let subText;
if (item.type === ReferenceType.SECRET) { const isNoMatchMessage = item.slug === "__no_match__";
if (isNoMatchMessage) {
entryIcon = <FontAwesomeIcon icon={faSearch} className="text-gray-400" />;
subText = "No results";
} else if (item.type === ReferenceType.SECRET) {
entryIcon = <FontAwesomeIcon icon={faKey} className="text-bunker-300" />; entryIcon = <FontAwesomeIcon icon={faKey} className="text-bunker-300" />;
subText = "Secret"; subText = "Secret";
} else if (item.type === ReferenceType.ENVIRONMENT) { } else if (item.type === ReferenceType.ENVIRONMENT) {
@@ -322,10 +362,28 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
subText = "Folder"; subText = "Folder";
} }
return ( return isNoMatchMessage ? (
<div <div
tabIndex={0} role="status"
role="button" aria-label="no-match-message"
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
key={`secret-reference-secret-${i + 1}`}
>
<div className="text-md relative flex w-full cursor-default select-none items-center justify-between px-2 py-2 opacity-75 outline-none transition-all">
<div className="flex w-full items-start gap-2">
<div className="mt-1 flex items-center">{entryIcon}</div>
<div className="text-md w-10/12 truncate text-left">
<span className="text-gray-400">{item.label}</span>
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
{subText}
</div>
</div>
</div>
</div>
</div>
) : (
<button
type="button"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") handleSuggestionSelect(i); if (e.key === "Enter") handleSuggestionSelect(i);
}} }}
@@ -337,8 +395,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
handleSuggestionSelect(i); handleSuggestionSelect(i);
}} }}
onMouseEnter={() => setHighlightedIndex(i)} onMouseEnter={() => setHighlightedIndex(i)}
style={{ pointerEvents: "auto" }} className="flex w-full items-center justify-between border-none border-mineshaft-600 bg-transparent p-0 text-left"
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
key={`secret-reference-secret-${i + 1}`} key={`secret-reference-secret-${i + 1}`}
> >
<div <div
@@ -349,14 +406,14 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
<div className="flex w-full items-start gap-2"> <div className="flex w-full items-start gap-2">
<div className="mt-1 flex items-center">{entryIcon}</div> <div className="mt-1 flex items-center">{entryIcon}</div>
<div className="text-md w-10/12 truncate text-left"> <div className="text-md w-10/12 truncate text-left">
{item.label} <span>{item.label}</span>
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400"> <div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
{subText} {subText}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </button>
); );
})} })}
</div> </div>
@@ -29,7 +29,8 @@ const syntaxHighlight = (
skipNext = true; skipNext = true;
return ( return (
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}> <span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
&#36;&#123;<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span> &#36;&#123;
<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
&#125; &#125;
</span> </span>
); );