Skip to content

Commit ba193e3

Browse files
committed
fix(secrets): harden visible value reveal
1 parent 2d93f56 commit ba193e3

5 files changed

Lines changed: 54 additions & 17 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.test.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,12 @@ describe('SecretValueField', () => {
3737
act(() => root.render(<SecretValueField value='visible-secret' canEdit={false} canReveal />))
3838

3939
expect(input().readOnly).toBe(true)
40-
expect(input().style.webkitTextSecurity).toBe('disc')
40+
expect(input().value).toBe('•'.repeat(10))
4141

4242
act(() => input().focus())
4343

4444
expect(input().value).toBe('visible-secret')
4545
expect(input().readOnly).toBe(true)
46-
expect(input().style.webkitTextSecurity).toBe('')
4746
})
4847

4948
it('never places a withheld value in the field', () => {
@@ -53,4 +52,10 @@ describe('SecretValueField', () => {
5352
act(() => input().focus())
5453
expect(input().value).toBe('•'.repeat(10))
5554
})
55+
56+
it('keeps an empty editable value empty while unfocused', () => {
57+
act(() => root.render(<SecretValueField value='' />))
58+
59+
expect(input().value).toBe('')
60+
})
5661
})

apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
'use client'
22

3-
import type { ComponentProps, CSSProperties } from 'react'
3+
import type { ComponentProps } from 'react'
44
import { useState } from 'react'
55
import { ChipInput } from '@sim/emcn'
66

77
const BULLET = '\u2022'
88

9-
/**
10-
* Viewers without reveal access receive a fixed-length mask so the secret's
11-
* length is not disclosed.
12-
*/
9+
/** Fixed-length masks avoid disclosing the secret's length. */
1310
const VIEWER_MASK_LENGTH = 10
1411

1512
type SecretValueFieldProps = Omit<
@@ -58,11 +55,8 @@ export function SecretValueField({
5855
const editable = canEdit && !readOnly
5956
const revealable = canEdit || canReveal
6057
const maskActive = revealable && !unmasked && !focused
61-
const displayValue = revealable ? value : BULLET.repeat(VIEWER_MASK_LENGTH)
62-
63-
const mergedStyle: CSSProperties | undefined = maskActive
64-
? ({ ...style, WebkitTextSecurity: 'disc' } as CSSProperties)
65-
: style
58+
const displayValue =
59+
!revealable || (maskActive && value.length > 0) ? BULLET.repeat(VIEWER_MASK_LENGTH) : value
6660

6761
return (
6862
<ChipInput
@@ -71,7 +65,7 @@ export function SecretValueField({
7165
type='text'
7266
value={displayValue}
7367
readOnly
74-
style={mergedStyle}
68+
style={style}
7569
onChange={(event) => {
7670
if (editable) onChange?.(event.target.value)
7771
}}

apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1038,7 +1038,7 @@ export function SecretsManager() {
10381038
).map(([key, value]) => {
10391039
const cred = workspaceEnvKeyToCredential.get(key)
10401040
const canEditRow = canCreateWorkspaceSecret && cred?.role === 'admin'
1041-
const canRevealRow = canEditRow || Boolean(cred?.unredacted)
1041+
const canRevealRow = cred?.role === 'admin' || Boolean(cred?.unredacted)
10421042
return (
10431043
<WorkspaceVariableRow
10441044
key={key}

apps/sim/hooks/queries/environment.test.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ vi.mock('@/lib/environment/api', () => ({
1515
fetchWorkspaceEnvironment: mockFetchWorkspaceEnvironment,
1616
}))
1717

18-
import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
18+
import { environmentKeys, useWorkspaceEnvironment } from '@/hooks/queries/environment'
1919

2020
function renderWorkspaceEnvironment(workspaceId: string, enabled?: boolean) {
2121
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
@@ -59,4 +59,43 @@ describe('useWorkspaceEnvironment', () => {
5959
expect(mockFetchWorkspaceEnvironment).not.toHaveBeenCalled()
6060
unmount()
6161
})
62+
63+
it('does not retain decrypted values while a different workspace loads', () => {
64+
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
65+
const container = document.createElement('div')
66+
const root = createRoot(container)
67+
const pendingWorkspace = new Promise<never>(() => {})
68+
69+
queryClient.setQueryData(environmentKeys.workspace('workspace-1'), {
70+
workspace: { SHARED_KEY: 'workspace-1-secret' },
71+
personal: {},
72+
conflicts: [],
73+
})
74+
mockFetchWorkspaceEnvironment.mockReturnValueOnce(pendingWorkspace)
75+
76+
function Probe({ workspaceId }: { workspaceId: string }) {
77+
const { data } = useWorkspaceEnvironment(workspaceId)
78+
return <span>{data?.workspace.SHARED_KEY ?? 'loading'}</span>
79+
}
80+
81+
act(() => {
82+
root.render(
83+
<QueryClientProvider client={queryClient}>
84+
<Probe workspaceId='workspace-1' />
85+
</QueryClientProvider>
86+
)
87+
})
88+
expect(container.textContent).toBe('workspace-1-secret')
89+
90+
act(() => {
91+
root.render(
92+
<QueryClientProvider client={queryClient}>
93+
<Probe workspaceId='workspace-2' />
94+
</QueryClientProvider>
95+
)
96+
})
97+
98+
expect(container.textContent).toBe('loading')
99+
act(() => root.unmount())
100+
})
62101
})

apps/sim/hooks/queries/environment.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createLogger } from '@sim/logger'
2-
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
33
import { requestJson } from '@/lib/api/client/request'
44
import {
55
type ContractBodyInput,
@@ -53,7 +53,6 @@ export function useWorkspaceEnvironment<TData = WorkspaceEnvironmentData>(
5353
queryFn: ({ signal }) => fetchWorkspaceEnvironment(workspaceId, signal),
5454
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
5555
staleTime: WORKSPACE_ENVIRONMENT_STALE_TIME,
56-
placeholderData: keepPreviousData,
5756
// See usePersonalEnvironment: seeds an editable form, so a focus refetch
5857
// during a concurrent workspace-env edit must not clobber unsaved rows.
5958
refetchOnWindowFocus: false,

0 commit comments

Comments
 (0)