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 7aee91273..7929dc0e6 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 @@ -622,11 +622,15 @@ export const expandSecretReferencesFactory = ({ const stackTrace = { ...dto, key: "root", children: [] } as TSecretReferenceTraceNode; if (!dto.value) return { expandedValue: "", stackTrace }; - const stack = [{ ...dto, depth: 0, trace: stackTrace }]; + + // Track visited secrets to prevent circular references + const createSecretId = (env: string, secretPath: string, key: string) => `${env}:${secretPath}:${key}`; + + const stack = [{ ...dto, depth: 0, trace: stackTrace, visitedSecrets: new Set() }]; let expandedValue = dto.value; while (stack.length) { - const { value, secretPath, environment, depth, trace } = stack.pop()!; + const { value, secretPath, environment, depth, trace, visitedSecrets } = stack.pop()!; // eslint-disable-next-line no-continue if (depth > MAX_SECRET_REFERENCE_DEPTH) continue; @@ -700,17 +704,27 @@ export const expandSecretReferencesFactory = ({ trace }; - const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue); + // Check for circular reference + const referencedSecretId = createSecretId( + referencedSecretEnvironmentSlug, + referencedSecretPath, + referencedSecretKey + ); + const isCircular = visitedSecrets.has(referencedSecretId); + + const newVisitedSecrets = new Set([...visitedSecrets, referencedSecretId]); + + const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue) && !isCircular; if (dto.shouldStackTrace) { const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null }; trace?.children.push(stackTraceNode); // if stack trace this would be child node if (shouldExpandMore) { - stack.push({ ...node, trace: stackTraceNode }); + stack.push({ ...node, trace: stackTraceNode, visitedSecrets: newVisitedSecrets }); } } else if (shouldExpandMore) { // if no stack trace is needed we just keep going with root node - stack.push(node); + stack.push({ ...node, visitedSecrets: newVisitedSecrets }); } if (referencedSecretValue) { diff --git a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx index a9542c90c..df7649109 100644 --- a/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx +++ b/frontend/src/components/secrets/SecretReferenceDetails/SecretReferenceDetails.tsx @@ -29,17 +29,54 @@ const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/; export const hasSecretReference = (value: string | undefined) => value ? INTERPOLATION_SYNTAX_REG.test(value) : false; +const createNodeId = (node: TSecretReferenceTraceNode): string => + `${node.environment}:${node.secretPath}:${node.key}`; + +const isCircularReference = ( + node: TSecretReferenceTraceNode, + visitedPath: Set +): boolean => { + const nodeId = createNodeId(node); + return visitedPath.has(nodeId); +}; + +const hasCircularReferences = ( + node: TSecretReferenceTraceNode, + visitedPath: Set = new Set() +): boolean => { + const nodeId = createNodeId(node); + + if (visitedPath.has(nodeId)) { + return true; + } + + const newVisitedPath = new Set([...visitedPath, nodeId]); + + return node.children.some((child) => hasCircularReferences(child, newVisitedPath)); +}; + export const SecretReferenceNode = ({ node, isRoot, - secretKey + secretKey, + visitedPath = new Set() }: { node: TSecretReferenceTraceNode; isRoot?: boolean; secretKey?: string; + visitedPath?: Set; }) => { const [isOpen, setIsOpen] = useState(false); - const hasChildren = node.children.length > 0; + + const nodeId = createNodeId(node); + const isCircular = !isRoot && isCircularReference(node, visitedPath); + + const newVisitedPath = isCircular ? visitedPath : new Set([...visitedPath, nodeId]); + + const safeChildren = isCircular + ? [] + : node.children.filter((child) => !isCircularReference(child, newVisitedPath)); + const hasChildren = safeChildren.length > 0; return (
  • @@ -77,8 +114,12 @@ export const SecretReferenceNode = ({ {hasChildren && (
      - {node.children.map((el, index) => ( - + {safeChildren.map((el, index) => ( + ))}
    )} @@ -102,6 +143,9 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop const tree = data?.tree; const secretValue = data?.value; + // Check if the tree contains circular references + const hasCirculars = tree ? hasCircularReferences(tree) : false; + useEffect(() => { if (error instanceof AxiosError) { const err = error?.response?.data as TApiErrors; @@ -132,7 +176,15 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop return (
    - +