mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +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: [] };
|
||||
}
|
||||
|
||||
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<Record<string, { value: string; tags: string[] }>>((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<Record<string, { value: string; tags: string[] }>>((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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<HTMLTextAreaElement, Props>(
|
||||
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<HTMLTextAreaElement, Props>(
|
||||
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<HTMLTextAreaElement, Props>(
|
||||
{suggestions.map((item, i) => {
|
||||
let entryIcon;
|
||||
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" />;
|
||||
subText = "Secret";
|
||||
} else if (item.type === ReferenceType.ENVIRONMENT) {
|
||||
@@ -322,10 +362,28 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
subText = "Folder";
|
||||
}
|
||||
|
||||
return (
|
||||
return isNoMatchMessage ? (
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
role="status"
|
||||
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) => {
|
||||
if (e.key === "Enter") handleSuggestionSelect(i);
|
||||
}}
|
||||
@@ -337,8 +395,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
handleSuggestionSelect(i);
|
||||
}}
|
||||
onMouseEnter={() => setHighlightedIndex(i)}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
|
||||
className="flex w-full items-center justify-between border-none border-mineshaft-600 bg-transparent p-0 text-left"
|
||||
key={`secret-reference-secret-${i + 1}`}
|
||||
>
|
||||
<div
|
||||
@@ -349,14 +406,14 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
||||
<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">
|
||||
{item.label}
|
||||
<span>{item.label}</span>
|
||||
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
|
||||
{subText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,8 @@ const syntaxHighlight = (
|
||||
skipNext = true;
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user