mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 08:27:36 +00:00
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:
@@ -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}`}>
|
||||||
${<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
${
|
||||||
|
<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user