Fix/client cert passphrase issues (#5898)

* fix: added interpolation, warning and syntax highlight for passphrase input

Changes:
1) When users add plain text in passphrase, warning message will be shown.
2) Passphrase will be interpolated from environment
3) Syntax highlighting for variables added.

Closes #2685

* fix: global environment variables interpolation in cert passphrase implemented.

* refactor: indentation refactoring
This commit is contained in:
Abhishek S Lal
2025-10-30 18:14:39 +05:30
committed by GitHub
parent c9a96ee94f
commit 6e8751a27a
8 changed files with 135 additions and 125 deletions

View File

@@ -1,19 +1,20 @@
import React from 'react';
import { IconCertificate, IconTrash, IconWorld } from '@tabler/icons';
import { useFormik } from 'formik';
import { uuid } from 'utils/common';
import * as Yup from 'yup';
import { IconEye, IconEyeOff } from '@tabler/icons';
import { useState } from 'react';
import StyledWrapper from './StyledWrapper';
import { useRef } from 'react';
import path from 'utils/common/path';
import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';
import SingleLineEditor from 'components/SingleLineEditor/index';
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField/index';
import { useTheme } from 'styled-components';
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }) => {
const certFilePathInputRef = useRef();
const keyFilePathInputRef = useRef();
const pfxFilePathInputRef = useRef();
const { storedTheme } = useTheme();
const formik = useFormik({
initialValues: {
@@ -68,10 +69,13 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
}
});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(formik.values.passphrase);
const getFile = (e) => {
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
if (filePath) {
let relativePath = path.relative(root, filePath);
let relativePath = path.relative(collection.pathname, filePath);
formik.setFieldValue(e.name, relativePath);
}
};
@@ -82,8 +86,6 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
pfxFilePathInputRef.current.value = '';
};
const [passwordVisible, setPasswordVisible] = useState(false);
const handleTypeChange = (e) => {
formik.setFieldValue('type', e.target.value);
if (e.target.value === 'cert') {
@@ -314,21 +316,14 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
Passphrase
</label>
<div className="textbox flex flex-row items-center w-[300px] h-[1.70rem] relative">
<input
id="passphrase"
type={passwordVisible ? 'text' : 'password'}
name="passphrase"
className="outline-none w-64 bg-transparent"
onChange={formik.handleChange}
<SingleLineEditor
value={formik.values.passphrase || ''}
theme={storedTheme}
onChange={(val) => formik.setFieldValue('passphrase', val)}
collection={collection}
isSecret={true}
/>
<button
type="button"
className="btn btn-sm absolute right-0 l"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
</button>
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
</div>
{formik.touched.passphrase && formik.errors.passphrase ? (
<div className="ml-1 text-red-500">{formik.errors.passphrase}</div>

View File

@@ -120,7 +120,7 @@ const CollectionSettings = ({ collection }) => {
case 'clientCert': {
return (
<ClientCertSettings
root={collection.pathname}
collection={collection}
clientCertConfig={clientCertConfig}
onUpdate={onClientCertSettingsUpdate}
onRemove={onClientCertSettingsRemove}

View File

@@ -187,7 +187,7 @@ class SingleLineEditor extends Component {
*/
secretEye = (isSecret) => {
return isSecret === true ? (
<button className="mx-2" onClick={() => this.toggleVisibleSecret()}>
<button type="button" className="mx-2" onClick={() => this.toggleVisibleSecret()}>
{this.state.maskInput === true ? (
<IconEyeOff size={18} strokeWidth={2} />
) : (

View File

@@ -974,80 +974,82 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:fetch-oauth2-credentials', async (event, { itemUid, request, collection }) => {
try {
if (request.oauth2) {
let requestCopy = _.cloneDeep(request);
const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection;
const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const partialItem = { uid: itemUid };
const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem);
mergeVars(collection, requestCopy, requestTreePath);
if (request.oauth2) {
let requestCopy = _.cloneDeep(request);
const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection;
const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const partialItem = { uid: itemUid };
const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem);
mergeVars(collection, requestCopy, requestTreePath);
const globalEnvironmentVariables = collection.globalEnvironmentVariables;
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const certsAndProxyConfig = await getCertsAndProxyConfig({
collectionUid,
request: requestCopy,
envVars,
runtimeVariables,
processEnvVars,
collectionPath
});
const { oauth2: { grantType }} = requestCopy || {};
const handleOAuth2Response = (response) => {
if (response.error && !response.debugInfo) {
throw new Error(response.error);
}
return response;
};
switch (grantType) {
case 'authorization_code':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
return await getOAuth2TokenUsingAuthorizationCode({
request: requestCopy,
collectionUid,
forceFetch: true,
certsAndProxyConfig
}).then(handleOAuth2Response);
case 'client_credentials':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
return await getOAuth2TokenUsingClientCredentials({
request: requestCopy,
collectionUid,
forceFetch: true,
certsAndProxyConfig
}).then(handleOAuth2Response);
case 'password':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
return await getOAuth2TokenUsingPasswordCredentials({
request: requestCopy,
collectionUid,
forceFetch: true,
certsAndProxyConfig
}).then(handleOAuth2Response);
case 'implicit':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
return await getOAuth2TokenUsingImplicitGrant({
request: requestCopy,
collectionUid,
forceFetch: true
}).then(handleOAuth2Response);
default:
return {
error: `Unsupported grant type: ${grantType}`,
credentials: null,
url: null,
collectionUid,
credentialsId: null
};
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const certsAndProxyConfig = await getCertsAndProxyConfig({
collectionUid,
request: requestCopy,
envVars,
runtimeVariables,
processEnvVars,
collectionPath,
globalEnvironmentVariables
});
const { oauth2: { grantType } } = requestCopy || {};
const handleOAuth2Response = (response) => {
if (response.error && !response.debugInfo) {
throw new Error(response.error);
}
return response;
};
switch (grantType) {
case 'authorization_code':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
return await getOAuth2TokenUsingAuthorizationCode({
request: requestCopy,
collectionUid,
forceFetch: true,
certsAndProxyConfig
}).then(handleOAuth2Response);
case 'client_credentials':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
return await getOAuth2TokenUsingClientCredentials({
request: requestCopy,
collectionUid,
forceFetch: true,
certsAndProxyConfig
}).then(handleOAuth2Response);
case 'password':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
return await getOAuth2TokenUsingPasswordCredentials({
request: requestCopy,
collectionUid,
forceFetch: true,
certsAndProxyConfig
}).then(handleOAuth2Response);
case 'implicit':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
return await getOAuth2TokenUsingImplicitGrant({
request: requestCopy,
collectionUid,
forceFetch: true
}).then(handleOAuth2Response);
default:
return {
error: `Unsupported grant type: ${grantType}`,
credentials: null,
url: null,
collectionUid,
credentialsId: null
};
}
}
} catch (error) {
return Promise.reject(error);
}
@@ -1105,29 +1107,31 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:refresh-oauth2-credentials', async (event, { itemUid, request, collection }) => {
try {
if (request.oauth2) {
let requestCopy = _.cloneDeep(request);
const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection;
const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const partialItem = { uid: itemUid };
const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem);
mergeVars(collection, requestCopy, requestTreePath);
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
if (request.oauth2) {
let requestCopy = _.cloneDeep(request);
const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection;
const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const partialItem = { uid: itemUid };
const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem);
mergeVars(collection, requestCopy, requestTreePath);
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const globalEnvironmentVariables = collection.globalEnvironmentVariables;
const certsAndProxyConfig = await getCertsAndProxyConfig({
collectionUid,
request: requestCopy,
envVars,
runtimeVariables,
processEnvVars,
collectionPath
});
let { credentials, url, credentialsId, debugInfo } = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig });
return { credentials, url, collectionUid, credentialsId, debugInfo };
}
const certsAndProxyConfig = await getCertsAndProxyConfig({
collectionUid,
request: requestCopy,
envVars,
runtimeVariables,
processEnvVars,
collectionPath,
globalEnvironmentVariables
});
let { credentials, url, credentialsId, debugInfo } = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig });
return { credentials, url, collectionUid, credentialsId, debugInfo };
}
} catch (error) {
return Promise.reject(error);
}

View File

@@ -15,7 +15,8 @@ const getCertsAndProxyConfig = async ({
envVars,
runtimeVariables,
processEnvVars,
collectionPath
collectionPath,
globalEnvironmentVariables
}) => {
/**
* @see https://github.com/usebruno/bruno/issues/211 set keepAlive to true, this should fix socket hang up errors
@@ -41,6 +42,7 @@ const getCertsAndProxyConfig = async ({
const brunoConfig = getBrunoConfig(collectionUid);
const interpolationOptions = {
globalEnvironmentVariables,
envVars,
runtimeVariables,
processEnvVars

View File

@@ -171,7 +171,8 @@ const registerGrpcEventHandlers = (window) => {
envVars: preparedRequest.envVars,
runtimeVariables,
processEnvVars: preparedRequest.processEnvVars,
collectionPath: collection.pathname
collectionPath: collection.pathname,
globalEnvironmentVariables: collection.globalEnvironmentVariables
});
@@ -302,7 +303,8 @@ const registerGrpcEventHandlers = (window) => {
envVars: preparedRequest.envVars,
runtimeVariables,
processEnvVars: preparedRequest.processEnvVars,
collectionPath: collection.pathname
collectionPath: collection.pathname,
globalEnvironmentVariables: collection.globalEnvironmentVariables
});
// Extract certificate information from the config

View File

@@ -75,7 +75,8 @@ const configureRequest = async (
envVars,
runtimeVariables,
processEnvVars,
collectionPath
collectionPath,
globalEnvironmentVariables
) => {
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
if (!protocolRegex.test(request.url)) {
@@ -88,7 +89,8 @@ const configureRequest = async (
envVars,
runtimeVariables,
processEnvVars,
collectionPath
collectionPath,
globalEnvironmentVariables
});
// Get followRedirects setting, default to true for backward compatibility
@@ -316,7 +318,8 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
envVars,
collection.runtimeVariables,
processEnvVars,
collectionPath
collectionPath,
collection.globalEnvironmentVariables
);
const response = await axiosInstance(request);
@@ -642,7 +645,8 @@ const registerNetworkIpc = (mainWindow) => {
envVars,
runtimeVariables,
processEnvVars,
collectionPath
collectionPath,
collection.globalEnvironmentVariables
);
const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request);
@@ -1165,7 +1169,8 @@ const registerNetworkIpc = (mainWindow) => {
envVars,
runtimeVariables,
processEnvVars,
collectionPath
collectionPath,
collection.globalEnvironmentVariables
);
if (request?.oauth2Credentials) {

View File

@@ -1,13 +1,14 @@
const { forOwn, cloneDeep } = require('lodash');
const { interpolate } = require('@usebruno/common');
const interpolateString = (str, { envVars, runtimeVariables, processEnvVars }) => {
const interpolateString = (str, { globalEnvironmentVariables, envVars, runtimeVariables, processEnvVars }) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
processEnvVars = processEnvVars || {};
runtimeVariables = runtimeVariables || {};
globalEnvironmentVariables = globalEnvironmentVariables || {};
// we clone envVars because we don't want to modify the original object
envVars = envVars ? cloneDeep(envVars) : {};
@@ -26,6 +27,7 @@ const interpolateString = (str, { envVars, runtimeVariables, processEnvVars }) =
// runtimeVariables take precedence over envVars
const combinedVars = {
...globalEnvironmentVariables,
...envVars,
...runtimeVariables,
process: {