diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index 3062676bb..88aca8b20 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -19,7 +19,6 @@ import CollectionSettings from 'components/CollectionSettings'; import { DocExplorer } from '@usebruno/graphql-docs'; import StyledWrapper from './StyledWrapper'; -import SecuritySettings from 'components/SecuritySettings'; import FolderSettings from 'components/FolderSettings'; import { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index'; import { produce } from 'immer'; @@ -226,10 +225,6 @@ const RequestTabPanel = () => { return ; } - if (focusedTab.type === 'security-settings') { - return ; - } - if (focusedTab.type === 'environment-settings') { return ; } diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js index 87a048e66..d2afd5275 100644 --- a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js +++ b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js @@ -65,9 +65,8 @@ const CollectionToolBar = ({ collection }) => { - - - + {/* ToolHint is present within the JsSandboxMode component */} + diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js index a85fd4ea4..e9dffde29 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js @@ -21,14 +21,6 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra ); } - case 'security-settings': { - return ( - <> - - Security - - ); - } case 'folder-settings': { return ( <> diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index b4f65693c..0714af2db 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -172,7 +172,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi setShowConfirmGlobalEnvironmentClose(true); }; - if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings', 'environment-settings', 'global-environment-settings'].includes(tab.type)) { + if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'environment-settings', 'global-environment-settings'].includes(tab.type)) { return ( props.theme.background.surface0}; } } .safe-mode { - background-color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.safeMode.bg}; color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.safeMode.color}; } .developer-mode { - background-color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.developerMode.bg}; color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.developerMode.color}; } + + .sandbox-dropdown { + min-width: 260px; + max-width: 380px; + } + + .sandbox-header { + padding: 0.5rem 0.625rem; + font-size: ${(props) => props.theme.font.size.base}; + color: ${(props) => props.theme.dropdown.headingText}; + } + + .sandbox-option { + display: flex; + margin: 5px; + border-radius: ${(props) => props.theme.border.radius.md}; + padding: 12px; + align-items: flex-start; + text-align: left; + gap: 0.5rem; + position: relative; + + &.safe-mode { + border: 1px solid ${(props) => props.theme.input.border}; + color: ${(props) => props.theme.colors.text.green}; + margin-bottom: 10px; + } + + &.developer-mode { + border: 1px solid ${(props) => props.theme.input.border}; + color: ${(props) => props.theme.colors.text.warning}; + } + + &.active { + cursor: default; + + &.developer-mode { + border: 1px solid ${(props) => props.theme.colors.text.warning}; + background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.04)}; + + .sandbox-option-radio input:checked { + border-color: ${(props) => props.theme.colors.text.warning}; + } + + .sandbox-option-radio input::after { + background: ${(props) => props.theme.colors.text.warning}; + } + } + + &.safe-mode { + border: 1px solid ${(props) => props.theme.colors.text.green}; + background-color: ${(props) => rgba(props.theme.colors.text.green, 0.04)}; + + .sandbox-option-radio input:checked { + border-color: ${(props) => props.theme.colors.text.green}; + } + + .sandbox-option-radio input::after { + background: ${(props) => props.theme.colors.text.green}; + } + } + } + + svg { + width: 2rem; + } + } + + .recommended-badge { + padding: 0.125rem 0.5rem; + font-size: 0.75rem; + background-color: ${(props) => rgba(props.theme.colors.text.green, 0.1)}; + color: ${(props) => props.theme.colors.text.green}; + border-radius: ${(props) => props.theme.border.radius.sm}; + } + + .sandbox-option-title { + display: flex; + align-items: center; + font-size: ${(props) => props.theme.font.size.base}; + gap: 0.25rem; + line-height: 1.25rem; + color: ${(props) => props.theme.colors.text.subtext2}; + } + + .sandbox-option-radio { + display: flex; + align-items: center; + justify-content: center; + margin-right: 0.25rem; + } + + .sandbox-option-radio input { + appearance: none; + width: 18px; + height: 18px; + border-radius: 9999px; + border: 1px solid currentColor; + background: transparent; + cursor: pointer; + position: relative; + transition: all 0.15s ease; + } + + .sandbox-option-radio input::after { + content: ''; + position: absolute; + inset: 3px; + border-radius: 9999px; + background: ${(props) => props.theme.background.base}; + opacity: 0; + transform: scale(0.5); + transition: all 0.15s ease; + } + + .sandbox-option-radio input:checked::after { + opacity: 1; + transform: scale(1); + } + + .sandbox-option-radio input:focus-visible { + outline: none; + } + + .sandbox-option-description { + font-size: ${(props) => props.theme.font.size.sm}; + color: ${(props) => props.theme.colors.text.muted}; + line-height: 1.1rem; + margin-top: 0.25rem; + } + + .developer-mode-warning { + margin-top: 0.5rem; + padding: 0.25rem 0.5rem; + display: inline-block; + background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.1)}; + border-radius: ${(props) => props.theme.border.radius.sm}; + color: ${(props) => props.theme.colors.text.warning}; + } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/index.js b/packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/index.js index 99058d822..fed2403c5 100644 --- a/packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/index.js +++ b/packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/index.js @@ -1,45 +1,126 @@ +import { useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; +import toast from 'react-hot-toast'; import { IconShieldCheck, IconCode } from '@tabler/icons'; -import { addTab } from 'providers/ReduxStore/slices/tabs'; -import { uuid } from 'utils/common/index'; +import Dropdown from 'components/Dropdown'; +import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; +import ToolHint from 'components/ToolHint'; + +const SANDBOX_OPTIONS = [ + { + key: 'safe', + label: 'Safe Mode', + description: 'JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.', + icon: IconShieldCheck, + recommended: true + }, + { + key: 'developer', + label: 'Developer Mode', + description: 'JavaScript code has access to the filesystem, can execute system commands and access sensitive information.', + icon: IconCode, + warning: 'Use only if you trust the authors of the collection', + recommended: false + } +]; const JsSandboxMode = ({ collection }) => { - const jsSandboxMode = collection?.securityConfig?.jsSandboxMode || 'safe'; const dispatch = useDispatch(); + const dropdownRef = useRef(null); + const [selectedMode, setSelectedMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe'); + + useEffect(() => { + setSelectedMode(collection?.securityConfig?.jsSandboxMode || 'safe'); + }, [collection?.securityConfig?.jsSandboxMode]); + + const onDropdownCreate = (instance) => { + dropdownRef.current = instance; + }; + + const closeDropdown = () => { + dropdownRef.current?.hide(); + }; + + const handleKeyDown = (e) => { + if (e && e.key === 'Escape') { + closeDropdown(); + } + }; + + const handleModeChange = (mode) => { + if (!collection?.uid || mode === selectedMode) { + return; + } - const viewSecuritySettings = () => { dispatch( - addTab({ - uid: uuid(), - collectionUid: collection.uid, - type: 'security-settings' + saveCollectionSecurityConfig(collection.uid, { + jsSandboxMode: mode }) + ) + .then(() => { + setSelectedMode(mode); + }) + .catch((err) => { + console.error(err); + toast.error('Failed to update sandbox mode'); + }); + }; + + const renderOption = (option) => { + const OptionIcon = option.icon; + const isActive = selectedMode === option.key; + + return ( + ); }; + const triggerIcon = ( +
+ +
+ {selectedMode === 'developer' ? : } +
+
+
+ ); + return ( - - {jsSandboxMode === 'safe' && ( -
- + + +
+
JavaScript Sandbox
+ {SANDBOX_OPTIONS.map(renderOption)}
- )} - {jsSandboxMode === 'developer' && ( -
- -
- )} +
); }; diff --git a/packages/bruno-app/src/components/SecuritySettings/StyledWrapper.js b/packages/bruno-app/src/components/SecuritySettings/StyledWrapper.js deleted file mode 100644 index 81cd5cd08..000000000 --- a/packages/bruno-app/src/components/SecuritySettings/StyledWrapper.js +++ /dev/null @@ -1,12 +0,0 @@ -import styled from 'styled-components'; - -const StyledWrapper = styled.div` - max-width: 800px; - - span.developer-mode-warning { - font-weight: 400; - color: ${(props) => props.theme.colors.text.yellow}; - } -`; - -export default StyledWrapper; diff --git a/packages/bruno-app/src/components/SecuritySettings/index.js b/packages/bruno-app/src/components/SecuritySettings/index.js deleted file mode 100644 index fee15e497..000000000 --- a/packages/bruno-app/src/components/SecuritySettings/index.js +++ /dev/null @@ -1,83 +0,0 @@ -import { useState } from 'react'; -import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions'; -import toast from 'react-hot-toast'; -import StyledWrapper from './StyledWrapper'; -import { useDispatch } from 'react-redux'; -import Button from 'ui/Button'; - -const SecuritySettings = ({ collection }) => { - const dispatch = useDispatch(); - const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe'); - - const handleChange = (e) => { - setJsSandboxMode(e.target.value); - }; - - const handleSave = () => { - dispatch( - saveCollectionSecurityConfig(collection?.uid, { - jsSandboxMode: jsSandboxMode - }) - ) - .then(() => { - toast.success('Sandbox mode updated successfully'); - }) - .catch((err) => console.log(err) && toast.error('Failed to update sandbox mode')); - }; - - return ( - -
JavaScript Sandbox
- -
- The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions. -
- -
-
- -

- JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands. -

- - -

- JavaScript code has access to the filesystem, can execute system commands and access sensitive information. -

-
- -
-
- ); -}; - -export default SecuritySettings; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js index 2a08f15f2..60c4baa6c 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js @@ -24,7 +24,6 @@ export const tabsSlice = createSlice({ const nonReplaceableTabTypes = [ 'variables', 'collection-runner', - 'security-settings', 'environment-settings', 'global-environment-settings' ]; diff --git a/tests/collection/default-sandbox-mode/default-sandbox-mode.spec.ts b/tests/collection/default-sandbox-mode/default-sandbox-mode.spec.ts index 3a42b9277..ad87a78fe 100644 --- a/tests/collection/default-sandbox-mode/default-sandbox-mode.spec.ts +++ b/tests/collection/default-sandbox-mode/default-sandbox-mode.spec.ts @@ -1,39 +1,28 @@ import { test, expect } from '../../../playwright'; import { createCollection, openCollection } from '../../utils/page/actions'; +import { buildSandboxLocators } from '../../utils/page/locators'; test.describe('Default JavaScript Sandbox Mode', () => { test('should set jsSandboxMode to safe by default when creating a new collection', async ({ page, createTmpDir }) => { const collectionName = 'test-sandbox-collection'; await createCollection(page, collectionName, await createTmpDir()); + const sandboxLocators = buildSandboxLocators(page); // Verify sandbox mode is set to safe by default - const sandboxModeSelector = page.getByTestId('sandbox-mode-selector'); - await expect(sandboxModeSelector).toBeVisible(); - await expect(sandboxModeSelector).toHaveAttribute('title', 'Safe Mode'); + await expect(sandboxLocators.sandboxModeSelector()).toBeVisible(); // Click on sandbox mode selector to open security settings - await sandboxModeSelector.click(); + await sandboxLocators.sandboxModeSelector().click(); // Change to developer mode - const developerRadio = page.locator('input[id="developer"]'); - await developerRadio.click(); + const developerRadio = sandboxLocators.developerModeRadio(); + await developerRadio.check(); - // Save - const saveButton = page.getByRole('button', { name: 'Save' }); - await saveButton.click(); + // For developer mode, check if safe mode is currently selected + const safeModeChecked = await sandboxLocators.safeModeRadio().isChecked().catch(() => false); + await expect(safeModeChecked).toBe(false); - // Verify mode changed to developer - await expect(sandboxModeSelector).toHaveAttribute('title', 'Developer Mode'); - - // Close all tabs - const modifier = process.platform === 'darwin' ? 'Meta' : 'Control'; - await page.keyboard.press(`${modifier}+Shift+W`); - - // Reopen the collection - await openCollection(page, collectionName); - - // Verify mode is still developer (persisted) - await expect(sandboxModeSelector).toHaveAttribute('title', 'Developer Mode'); + await page.keyboard.press('Escape'); }); }); diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts index 4fb25b96e..e097e6a14 100644 --- a/tests/utils/page/locators.ts +++ b/tests/utils/page/locators.ts @@ -201,3 +201,16 @@ export const buildGrpcCommonLocators = (page: Page) => ({ tabCount: () => page.getByTestId('tab-response-count') } }); + +/** + * Builds locators for sandbox mode settings + * @param page - The Playwright page object + * @returns Object with locators for sandbox elements + */ +export const buildSandboxLocators = (page: Page) => ({ + sandboxModeSelector: () => page.getByTestId('sandbox-mode-selector'), + safeModeRadio: () => page.getByTestId('sandbox-mode-safe'), + developerModeRadio: () => page.getByTestId('sandbox-mode-developer'), + jsSandboxHeading: () => page.getByText('JavaScript Sandbox'), + saveButton: () => page.getByRole('button', { name: 'Save' }) +}); diff --git a/tests/utils/page/runner.ts b/tests/utils/page/runner.ts index 41a0e592c..ca1323f05 100644 --- a/tests/utils/page/runner.ts +++ b/tests/utils/page/runner.ts @@ -1,4 +1,5 @@ import { Page, expect, test } from '../../../playwright'; +import { buildSandboxLocators } from './locators'; /** * Builds locators for the runner results view @@ -78,19 +79,6 @@ export const runCollection = async (page: Page, collectionName: string) => { }); }; -/** - * Builds locators for sandbox mode settings - * @param page - The Playwright page object - * @returns Object with locators for sandbox elements - */ -export const buildSandboxLocators = (page: Page) => ({ - sandboxModeSelector: () => page.getByTestId('sandbox-mode-selector'), - safeModeRadio: () => page.getByLabel('Safe Mode'), - developerModeRadio: () => page.getByLabel('Developer Mode(use only if'), - jsSandboxHeading: () => page.getByText('JavaScript Sandbox'), - saveButton: () => page.getByRole('button', { name: 'Save' }) -}); - /** * Sets up the JavaScript sandbox mode for a collection * @param page - The Playwright page object @@ -128,23 +116,12 @@ export const setSandboxMode = async (page: Page, collectionName: string, mode: ' await sandboxLocators.developerModeRadio().waitFor({ state: 'visible', timeout: 5000 }); await sandboxLocators.developerModeRadio().check(); } else { - // For safe mode, check if developer mode is currently selected - const developerModeChecked = await sandboxLocators.developerModeRadio().isChecked().catch(() => false); - - if (developerModeChecked) { - // Click the Developer Mode label text inside the security settings form - const securityForm = page.locator('div').filter({ hasText: 'JavaScript Sandbox' }).locator('..').first(); - const developerLabel = securityForm.locator('label').filter({ hasText: /^Developer Mode/ }).first(); - await developerLabel.waitFor({ state: 'visible', timeout: 5000 }); - await developerLabel.click(); - } - // Ensure Safe Mode radio is visible and check it await sandboxLocators.safeModeRadio().waitFor({ state: 'visible', timeout: 5000 }); await sandboxLocators.safeModeRadio().check(); } - await sandboxLocators.saveButton().click(); + await page.keyboard.press('Escape'); }); };