mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 08:26:29 +00:00
Stop blocking secret references with no matching reference and imporve UI edit secret behavior to better highlight this
This commit is contained in:
@@ -148,7 +148,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
keyStore,
|
keyStore,
|
||||||
reminderService
|
reminderService
|
||||||
}: TSecretV2BridgeServiceFactoryDep) => {
|
}: TSecretV2BridgeServiceFactoryDep) => {
|
||||||
const $validateSecretReferences = async (
|
const validateSecretReferences = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
permission: MongoAbility<ProjectPermissionSet>,
|
permission: MongoAbility<ProjectPermissionSet>,
|
||||||
references: ReturnType<typeof getAllSecretReferences>["nestedReferences"],
|
references: ReturnType<typeof getAllSecretReferences>["nestedReferences"],
|
||||||
@@ -312,12 +312,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
project.secretDetectionIgnoreValues || []
|
project.secretDetectionIgnoreValues || []
|
||||||
);
|
);
|
||||||
|
|
||||||
const { nestedReferences, localReferences } = getAllSecretReferences(inputSecret.secretValue);
|
const { nestedReferences } = getAllSecretReferences(inputSecret.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,
|
||||||
@@ -558,14 +553,6 @@ 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 updatedSecret = await secretDAL.transaction(async (tx) =>
|
const updatedSecret = await secretDAL.transaction(async (tx) =>
|
||||||
fnSecretBulkUpdate({
|
fnSecretBulkUpdate({
|
||||||
folderId,
|
folderId,
|
||||||
@@ -1686,7 +1673,6 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await $validateSecretReferences(projectId, permission, secretReferences);
|
|
||||||
|
|
||||||
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
|
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
|
||||||
await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId });
|
await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId });
|
||||||
@@ -2000,7 +1986,6 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await $validateSecretReferences(projectId, permission, secretReferences, tx);
|
|
||||||
|
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
await scanSecretPolicyViolations(
|
await scanSecretPolicyViolations(
|
||||||
@@ -3150,6 +3135,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
getSecretById,
|
getSecretById,
|
||||||
getAccessibleSecrets,
|
getAccessibleSecrets,
|
||||||
getSecretVersionsByIds,
|
getSecretVersionsByIds,
|
||||||
findSecretIdsByFolderIdAndKeys
|
findSecretIdsByFolderIdAndKeys,
|
||||||
|
validateSecretReferences
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import { useGetProjectFolders, useGetProjectSecrets } from "@app/hooks/api";
|
|||||||
|
|
||||||
import { SecretInput } from "../SecretInput";
|
import { SecretInput } from "../SecretInput";
|
||||||
|
|
||||||
|
// Regex to find all secret references in the format ${reference}
|
||||||
|
const REFERENCE_REGEX = /\${([^}]+)}/g;
|
||||||
|
|
||||||
|
// Extract unique references from a value
|
||||||
|
const extractReferences = (value: string): string[] => {
|
||||||
|
const matches = Array.from(value.matchAll(REFERENCE_REGEX));
|
||||||
|
return [...new Set(matches.map((match) => match[1]))];
|
||||||
|
};
|
||||||
|
|
||||||
const getIndexOfUnclosedRefToTheLeft = (value: string, pos: number) => {
|
const getIndexOfUnclosedRefToTheLeft = (value: string, pos: number) => {
|
||||||
// take substring up to pos in order to consider edits for closed references
|
// take substring up to pos in order to consider edits for closed references
|
||||||
for (let i = pos; i >= 1; i -= 1) {
|
for (let i = pos; i >= 1; i -= 1) {
|
||||||
@@ -83,7 +92,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
|
|
||||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||||
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const popoverContentRef = useRef<HTMLDivElement>(null);
|
const popoverContentRef = useRef<HTMLDivElement>(null);
|
||||||
const [isFocused, setIsFocused] = useToggle(false);
|
const [isFocused, setIsFocused] = useToggle(false);
|
||||||
const currentCursorPosition = inputRef.current?.selectionStart || 0;
|
const currentCursorPosition = inputRef.current?.selectionStart || 0;
|
||||||
@@ -139,6 +148,8 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const allReferences = useMemo(() => extractReferences(value), [value]);
|
||||||
|
|
||||||
const suggestions = useMemo(() => {
|
const suggestions = useMemo(() => {
|
||||||
if (!isPopupOpen) return [];
|
if (!isPopupOpen) return [];
|
||||||
// reset highlight whenever recomputation happens
|
// reset highlight whenever recomputation happens
|
||||||
@@ -178,6 +189,44 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
return suggestionsArr;
|
return suggestionsArr;
|
||||||
}, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.value]);
|
}, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.value]);
|
||||||
|
|
||||||
|
// Mark as invalid when editing and reference doesn't match any suggestion
|
||||||
|
const invalidReferences = useMemo(() => {
|
||||||
|
const invalid = new Set<string>();
|
||||||
|
|
||||||
|
if (!isPopupOpen) {
|
||||||
|
return invalid; // No validation when not editing
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestionsHaveLoaded = Boolean(secrets || folders || currentWorkspace?.environments);
|
||||||
|
|
||||||
|
if (!suggestionsHaveLoaded) {
|
||||||
|
return invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have an active suggestion context but no suggestions, it means the query returned empty
|
||||||
|
const suggestionsAreEmpty = suggestions.length === 0;
|
||||||
|
|
||||||
|
allReferences.forEach((reference) => {
|
||||||
|
const matchesAnySuggestion = suggestions.some((suggestion) => {
|
||||||
|
if (!reference.includes(".")) {
|
||||||
|
return suggestion.slug === reference;
|
||||||
|
}
|
||||||
|
const parts = reference.split(".");
|
||||||
|
const finalPart = parts[parts.length - 1];
|
||||||
|
return suggestion.slug === finalPart;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark as invalid only if:
|
||||||
|
// 1. We have suggestions loaded AND none match, OR
|
||||||
|
// 2. The query returned empty results
|
||||||
|
if (!matchesAnySuggestion && (suggestions.length > 0 || suggestionsAreEmpty)) {
|
||||||
|
invalid.add(reference);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return invalid;
|
||||||
|
}, [isPopupOpen, allReferences, suggestions, secrets, folders, currentWorkspace?.environments]);
|
||||||
|
|
||||||
const handleSuggestionSelect = (selectIndex?: number) => {
|
const handleSuggestionSelect = (selectIndex?: number) => {
|
||||||
const selectedSuggestion =
|
const selectedSuggestion =
|
||||||
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
|
suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
|
||||||
@@ -257,18 +306,20 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// to handle multiple ref for single component
|
// to handle multiple ref for single component
|
||||||
const handleRef = useCallback((el: HTMLTextAreaElement) => {
|
const handleRef = useCallback(
|
||||||
// @ts-expect-error this is for multiple ref single component
|
(el: HTMLTextAreaElement) => {
|
||||||
inputRef.current = el;
|
inputRef.current = el;
|
||||||
if (ref) {
|
if (ref) {
|
||||||
if (typeof ref === "function") {
|
if (typeof ref === "function") {
|
||||||
ref(el);
|
ref(el);
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line no-param-reassign
|
||||||
ref.current = el;
|
ref.current = el;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}, []);
|
[ref]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root open={isPopupOpen} onOpenChange={handlePopUpOpen}>
|
<Popover.Root open={isPopupOpen} onOpenChange={handlePopUpOpen}>
|
||||||
@@ -287,6 +338,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
}}
|
}}
|
||||||
onChange={(e) => onChange?.(e.target.value)}
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
containerClassName={containerClassName}
|
containerClassName={containerClassName}
|
||||||
|
invalidReferences={invalidReferences}
|
||||||
/>
|
/>
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPa
|
|||||||
|
|
||||||
const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g;
|
const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g;
|
||||||
|
|
||||||
const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => {
|
const syntaxHighlight = (
|
||||||
|
content?: string | null,
|
||||||
|
isVisible?: boolean,
|
||||||
|
isImport?: boolean,
|
||||||
|
invalidReferences?: Set<string>
|
||||||
|
) => {
|
||||||
if (isImport && !content) return "IMPORTED";
|
if (isImport && !content) return "IMPORTED";
|
||||||
if (content === "") return "EMPTY";
|
if (content === "") return "EMPTY";
|
||||||
if (!content) return "EMPTY";
|
if (!content) return "EMPTY";
|
||||||
@@ -18,9 +23,18 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
|
|||||||
const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}");
|
const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}");
|
||||||
if (isInterpolationSyntax) {
|
if (isInterpolationSyntax) {
|
||||||
skipNext = true;
|
skipNext = true;
|
||||||
|
const referenceContent = el.slice(2, -1);
|
||||||
|
const isInvalid = invalidReferences?.has(referenceContent) ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
|
<span
|
||||||
${<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
className={`ph-no-capture ${isInvalid ? "text-red-500" : "text-yellow"}`}
|
||||||
|
key={`secret-value-${i + 1}`}
|
||||||
|
>
|
||||||
|
${
|
||||||
|
<span className={`ph-no-capture ${isInvalid ? "text-red-300/80" : "text-yellow-200/80"}`}>
|
||||||
|
{referenceContent}
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -46,6 +60,7 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
|||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
canEditButNotView?: boolean;
|
canEditButNotView?: boolean;
|
||||||
|
invalidReferences?: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all";
|
const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all";
|
||||||
@@ -63,6 +78,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
isReadOnly,
|
isReadOnly,
|
||||||
onFocus,
|
onFocus,
|
||||||
canEditButNotView,
|
canEditButNotView,
|
||||||
|
invalidReferences,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -81,7 +97,8 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
{syntaxHighlight(
|
{syntaxHighlight(
|
||||||
value,
|
value,
|
||||||
isVisible || (isSecretFocused && !valueAlwaysHidden),
|
isVisible || (isSecretFocused && !valueAlwaysHidden),
|
||||||
isImport
|
isImport,
|
||||||
|
invalidReferences
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</code>
|
</code>
|
||||||
|
|||||||
Reference in New Issue
Block a user