Skip to content

Commit 9282d1b

Browse files
authored
feat(secrets): allow admins to view and edit workspace secret values (#4040)
* feat(secrets): allow admins to view and edit workspace secret values * fix(secrets): cross-browser masking and grid layout for non-admin users
1 parent 7b81a76 commit 9282d1b

File tree

1 file changed

+48
-32
lines changed

1 file changed

+48
-32
lines changed

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

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,11 @@ interface WorkspaceVariableRowProps {
127127
renamingKey: string | null
128128
pendingKeyValue: string
129129
hasCredential: boolean
130-
isAdmin: boolean
130+
canEdit: boolean
131131
onRenameStart: (key: string) => void
132132
onPendingKeyChange: (value: string) => void
133133
onRenameEnd: (key: string, value: string) => void
134+
onValueChange: (key: string, value: string) => void
134135
onDelete: (key: string) => void
135136
onViewDetails: (envKey: string) => void
136137
}
@@ -141,18 +142,16 @@ function WorkspaceVariableRow({
141142
renamingKey,
142143
pendingKeyValue,
143144
hasCredential,
144-
isAdmin,
145+
canEdit,
145146
onRenameStart,
146147
onPendingKeyChange,
147148
onRenameEnd,
149+
onValueChange,
148150
onDelete,
149151
onViewDetails,
150152
}: WorkspaceVariableRowProps) {
151153
const [valueFocused, setValueFocused] = useState(false)
152154

153-
const maskedValueStyle =
154-
isAdmin && !valueFocused ? ({ WebkitTextSecurity: 'disc' } as React.CSSProperties) : undefined
155-
156155
return (
157156
<div className='contents'>
158157
<EmcnInput
@@ -167,24 +166,31 @@ function WorkspaceVariableRow({
167166
autoCapitalize='off'
168167
spellCheck='false'
169168
readOnly
170-
onFocus={(e) => e.target.removeAttribute('readOnly')}
169+
onFocus={(e) => {
170+
if (canEdit) e.target.removeAttribute('readOnly')
171+
}}
171172
className='h-9'
172173
/>
173174
<div />
174175
<EmcnInput
175-
value={isAdmin ? value : value ? '\u2022'.repeat(value.length) : ''}
176+
value={canEdit ? value : value ? '\u2022'.repeat(value.length) : ''}
177+
type={canEdit && !valueFocused ? 'password' : 'text'}
178+
onChange={(e) => onValueChange(envKey, e.target.value)}
176179
readOnly
177-
onFocus={() => {
178-
if (isAdmin) setValueFocused(true)
180+
onFocus={(e) => {
181+
if (canEdit) {
182+
setValueFocused(true)
183+
e.target.removeAttribute('readOnly')
184+
}
179185
}}
180186
onBlur={() => {
181-
if (isAdmin) setValueFocused(false)
187+
if (canEdit) setValueFocused(false)
182188
}}
189+
name={`workspace_env_value_${envKey}_${Math.random()}`}
183190
autoComplete='off'
184191
autoCorrect='off'
185192
autoCapitalize='off'
186193
spellCheck='false'
187-
style={maskedValueStyle}
188194
className='h-9'
189195
/>
190196
<Button
@@ -195,14 +201,18 @@ function WorkspaceVariableRow({
195201
>
196202
Details
197203
</Button>
198-
<Tooltip.Root>
199-
<Tooltip.Trigger asChild>
200-
<Button variant='ghost' onClick={() => onDelete(envKey)} className='h-9 w-9'>
201-
<Trash />
202-
</Button>
203-
</Tooltip.Trigger>
204-
<Tooltip.Content>Delete secret</Tooltip.Content>
205-
</Tooltip.Root>
204+
{canEdit ? (
205+
<Tooltip.Root>
206+
<Tooltip.Trigger asChild>
207+
<Button variant='ghost' onClick={() => onDelete(envKey)} className='h-9 w-9'>
208+
<Trash />
209+
</Button>
210+
</Tooltip.Trigger>
211+
<Tooltip.Content>Delete secret</Tooltip.Content>
212+
</Tooltip.Root>
213+
) : (
214+
<div />
215+
)}
206216
</div>
207217
)
208218
}
@@ -316,7 +326,7 @@ export function CredentialsManager() {
316326
const { data: workspacePermissions } = useWorkspacePermissionsQuery(workspaceId || null)
317327
const queryClient = useQueryClient()
318328

319-
const isAdmin = useMemo(() => {
329+
const isWorkspaceAdmin = useMemo(() => {
320330
const userId = session?.user?.id
321331
if (!userId || !workspacePermissions?.users) return false
322332
const currentUser = workspacePermissions.users.find((user) => user.userId === userId)
@@ -791,6 +801,10 @@ export function CredentialsManager() {
791801
[pendingKeyValue, renamingKey]
792802
)
793803

804+
const handleWorkspaceValueChange = useCallback((key: string, value: string) => {
805+
setWorkspaceVars((prev) => ({ ...prev, [key]: value }))
806+
}, [])
807+
794808
const handleDeleteWorkspaceVar = useCallback((key: string) => {
795809
setWorkspaceVars((prev) => {
796810
const next = { ...prev }
@@ -1536,25 +1550,27 @@ export function CredentialsManager() {
15361550
renamingKey={renamingKey}
15371551
pendingKeyValue={pendingKeyValue}
15381552
hasCredential={envKeyToCredential.has(key)}
1539-
isAdmin={isAdmin}
1553+
canEdit={isWorkspaceAdmin}
15401554
onRenameStart={setRenamingKey}
15411555
onPendingKeyChange={setPendingKeyValue}
15421556
onRenameEnd={handleWorkspaceKeyRename}
1557+
onValueChange={handleWorkspaceValueChange}
15431558
onDelete={handleDeleteWorkspaceVar}
15441559
onViewDetails={(envKey) => handleViewDetails(envKey, 'env_workspace')}
15451560
/>
15461561
))}
1547-
{(searchTerm.trim()
1548-
? filteredNewWorkspaceRows
1549-
: newWorkspaceRows.map((row, index) => ({ row, originalIndex: index }))
1550-
).map(({ row, originalIndex }) => (
1551-
<NewWorkspaceVariableRow
1552-
key={row.id || originalIndex}
1553-
envVar={row}
1554-
index={originalIndex}
1555-
onUpdate={updateNewWorkspaceRow}
1556-
/>
1557-
))}
1562+
{isWorkspaceAdmin &&
1563+
(searchTerm.trim()
1564+
? filteredNewWorkspaceRows
1565+
: newWorkspaceRows.map((row, index) => ({ row, originalIndex: index }))
1566+
).map(({ row, originalIndex }) => (
1567+
<NewWorkspaceVariableRow
1568+
key={row.id || originalIndex}
1569+
envVar={row}
1570+
index={originalIndex}
1571+
onUpdate={updateNewWorkspaceRow}
1572+
/>
1573+
))}
15581574
<div className={`${COL_SPAN_ALL} h-[8px]`} />
15591575
</>
15601576
)}

0 commit comments

Comments
 (0)