Skip to content

Commit 84a936c

Browse files
authored
fix(engine+ui): Fix unable to update secrets (TracecatHQ#372)
1 parent 3333922 commit 84a936c

File tree

4 files changed

+147
-119
lines changed

4 files changed

+147
-119
lines changed

frontend/src/components/workspaces/edit-workspace-secret.tsx

+132-111
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"use client"
22

3-
import React, { PropsWithChildren } from "react"
3+
import React, { PropsWithChildren, useCallback } from "react"
44
import { SecretResponse, UpdateSecretParams } from "@/client"
55
import { zodResolver } from "@hookform/resolvers/zod"
66
import { DialogProps } from "@radix-ui/react-dialog"
7-
import { KeyRoundIcon, PlusCircle, Trash2Icon } from "lucide-react"
8-
import { ArrayPath, FieldPath, useFieldArray, useForm } from "react-hook-form"
7+
import { PlusCircle, SaveIcon, Trash2Icon } from "lucide-react"
8+
import { useFieldArray, useForm } from "react-hook-form"
9+
import { z } from "zod"
910

10-
import { createSecretSchema } from "@/types/schemas"
1111
import { useSecrets } from "@/lib/hooks"
1212
import { Button } from "@/components/ui/button"
1313
import {
@@ -23,7 +23,6 @@ import {
2323
import {
2424
Form,
2525
FormControl,
26-
FormDescription,
2726
FormField,
2827
FormItem,
2928
FormLabel,
@@ -40,79 +39,88 @@ interface EditCredentialsDialogProps
4039
setSelectedSecret: (selectedSecret: SecretResponse | null) => void
4140
}
4241

42+
export const updateSecretSchema = z.object({
43+
name: z.string().optional(),
44+
description: z.string().max(255).optional(),
45+
keys: z.array(
46+
z.object({
47+
key: z.string(),
48+
value: z.string(),
49+
})
50+
),
51+
})
52+
4353
export function EditCredentialsDialog({
4454
selectedSecret,
4555
setSelectedSecret,
4656
children,
4757
className,
58+
...props
4859
}: EditCredentialsDialogProps) {
49-
const [showDialog, setShowDialog] = React.useState(false)
5060
const { updateSecretById } = useSecrets()
51-
console.log("EDIT SECRET DIALOG", selectedSecret)
5261

5362
const methods = useForm<UpdateSecretParams>({
54-
resolver: zodResolver(createSecretSchema),
55-
values: {
56-
name: selectedSecret?.name ?? undefined,
57-
description: selectedSecret?.description ?? undefined,
58-
type: "custom",
59-
keys: selectedSecret?.keys.map((key) => ({
60-
key,
61-
value: "",
62-
})) || [{ key: "", value: "" }],
63+
resolver: zodResolver(updateSecretSchema),
64+
defaultValues: {
65+
name: "",
66+
description: "",
67+
keys: [],
6368
},
6469
})
6570
const { control, register } = methods
6671

67-
const onSubmit = async (values: UpdateSecretParams) => {
68-
if (!selectedSecret) {
69-
console.error("No secret selected")
70-
return
71-
}
72-
console.log("Submitting edit secret")
73-
try {
74-
await updateSecretById({
75-
secretId: selectedSecret.id,
76-
params: values,
77-
})
78-
} catch (error) {
79-
console.error(error)
80-
}
81-
methods.reset()
82-
}
72+
const onSubmit = useCallback(
73+
async (values: UpdateSecretParams) => {
74+
if (!selectedSecret) {
75+
console.error("No secret selected")
76+
return
77+
}
78+
// Remove unset values from the params object
79+
// We consider empty strings as unset values
80+
const params = {
81+
name: values.name || undefined,
82+
description: values.description || undefined,
83+
keys: values.keys || undefined,
84+
}
85+
console.log("Submitting edit secret", params)
86+
try {
87+
await updateSecretById({
88+
secretId: selectedSecret.id,
89+
params,
90+
})
91+
} catch (error) {
92+
console.error(error)
93+
}
94+
methods.reset()
95+
setSelectedSecret(null) // Only unset the selected secret after the form has been submitted
96+
},
97+
[selectedSecret, setSelectedSecret]
98+
)
8399

84-
const onValidationFailed = () => {
85-
console.error("Form validation failed")
100+
const onValidationFailed = (errors: unknown) => {
101+
console.error("Form validation failed", errors)
86102
toast({
87103
title: "Form validation failed",
88104
description: "A validation error occurred while editing the secret.",
89105
})
90106
}
91107

92-
const inputKey = "keys"
93-
const typedKey = inputKey as FieldPath<UpdateSecretParams>
94108
const { fields, append, remove } = useFieldArray<UpdateSecretParams>({
95109
control,
96-
name: inputKey as ArrayPath<UpdateSecretParams>,
110+
name: "keys",
97111
})
98112

99113
return (
100-
<Dialog
101-
open={showDialog}
102-
onOpenChange={(open) => {
103-
if (!open) {
104-
setSelectedSecret(null)
105-
}
106-
setShowDialog(open)
107-
}}
108-
>
114+
<Dialog {...props}>
109115
{children}
110116
<DialogContent className={className}>
111117
<DialogHeader>
112118
<DialogTitle>Edit secret</DialogTitle>
113-
<DialogDescription>
114-
<b className="inline-block">NOTE</b>: This feature is a work in
115-
progress.
119+
<DialogDescription className="flex flex-col">
120+
<span>
121+
Leave a field blank to keep its existing value. You must update
122+
all keys at once.
123+
</span>
116124
</DialogDescription>
117125
</DialogHeader>
118126
<Form {...methods}>
@@ -125,14 +133,18 @@ export function EditCredentialsDialog({
125133
render={() => (
126134
<FormItem>
127135
<FormLabel className="text-sm">Name</FormLabel>
136+
128137
<FormControl>
129138
<Input
130139
className="text-sm"
131-
placeholder="Name (snake case)"
140+
placeholder={selectedSecret?.name || "Name"}
132141
{...register("name")}
133142
/>
134143
</FormControl>
135144
<FormMessage />
145+
<span className="text-xs text-foreground/50">
146+
{!methods.watch("name") && "Name will be left unchanged."}
147+
</span>
136148
</FormItem>
137149
)}
138150
/>
@@ -143,92 +155,101 @@ export function EditCredentialsDialog({
143155
render={() => (
144156
<FormItem>
145157
<FormLabel className="text-sm">Description</FormLabel>
146-
<FormDescription className="text-sm">
147-
A description for this secret.
148-
</FormDescription>
149158
<FormControl>
150159
<Input
151160
className="text-sm"
152-
placeholder="Description"
161+
placeholder={
162+
selectedSecret?.description || "Description"
163+
}
153164
{...register("description")}
154165
/>
155166
</FormControl>
156167
<FormMessage />
168+
<span className="text-xs text-foreground/50">
169+
{!methods.watch("description") &&
170+
"Description will be left unchanged."}
171+
</span>
157172
</FormItem>
158173
)}
159174
/>
160-
<FormField
161-
key={inputKey}
162-
control={control}
163-
name={typedKey}
164-
render={() => (
165-
<FormItem>
166-
<FormLabel className="text-sm">Keys</FormLabel>
167-
<div className="flex flex-col space-y-2">
168-
{fields.map((field, index) => {
169-
return (
170-
<div
171-
key={`${field.id}.${index}`}
172-
className="flex w-full items-center gap-2"
173-
>
175+
<FormItem>
176+
<FormLabel className="text-sm">Keys</FormLabel>
177+
178+
{fields.length > 0 &&
179+
fields.map((keysItem, index) => (
180+
<div
181+
key={keysItem.id}
182+
className="flex items-center justify-between"
183+
>
184+
<FormField
185+
key={`keys.${index}.key`}
186+
control={control}
187+
name={`keys.${index}.key`}
188+
render={({ field }) => (
189+
<FormItem>
174190
<FormControl>
175191
<Input
176192
id={`key-${index}`}
177193
className="text-sm"
178-
{...register(
179-
`${inputKey}.${index}.key` as const,
180-
{
181-
required: true,
182-
}
183-
)}
184-
placeholder="Key"
185-
/>
186-
</FormControl>
187-
<FormControl>
188-
<Input
189-
id={`value-${index}`}
190-
className="text-sm"
191-
{...register(
192-
`${inputKey}.${index}.value` as const,
193-
{
194-
required: true,
195-
}
196-
)}
197-
placeholder="••••••••••••••••"
198-
type="password"
194+
placeholder={"Key"}
195+
{...field}
199196
/>
200197
</FormControl>
198+
<FormMessage />
199+
</FormItem>
200+
)}
201+
/>
201202

202-
<Button
203-
type="button"
204-
variant="ghost"
205-
onClick={() => remove(index)}
206-
disabled={fields.length === 1}
207-
>
208-
<Trash2Icon className="size-3.5" />
209-
</Button>
210-
</div>
211-
)
212-
})}
203+
<FormField
204+
key={`keys.${index}.value`}
205+
control={control}
206+
name={`keys.${index}.value`}
207+
render={({ field }) => (
208+
<FormItem>
209+
<div className="flex flex-col space-y-2">
210+
<FormControl>
211+
<Input
212+
id={`value-${index}`}
213+
className="text-sm"
214+
placeholder="••••••••••••••••"
215+
type="password"
216+
{...field}
217+
/>
218+
</FormControl>
219+
</div>
220+
<FormMessage />
221+
</FormItem>
222+
)}
223+
/>
213224
<Button
214225
type="button"
215-
variant="outline"
216-
onClick={() => append({ key: "", value: "" })}
217-
className="space-x-2 text-xs"
226+
variant="ghost"
227+
onClick={() => remove(index)}
218228
>
219-
<PlusCircle className="mr-2 size-4" />
220-
Add Item
229+
<Trash2Icon className="size-3.5" />
221230
</Button>
222231
</div>
223-
<FormMessage />
224-
</FormItem>
225-
)}
226-
/>
232+
))}
233+
</FormItem>
234+
<Button
235+
type="button"
236+
variant="outline"
237+
onClick={() => append({ key: "", value: "" })}
238+
className="w-full space-x-2 text-xs text-foreground/80"
239+
>
240+
<PlusCircle className="mr-2 size-4" />
241+
Add Item
242+
</Button>
243+
{fields.length === 0 && (
244+
<span className="text-xs text-foreground/50">
245+
Secrets will be left unchanged.
246+
</span>
247+
)}
227248
<DialogFooter>
228249
<DialogClose asChild>
229250
<Button className="ml-auto space-x-2" type="submit">
230-
<KeyRoundIcon className="mr-2 size-4" />
231-
Create Secret
251+
<SaveIcon className="mr-2 size-4" />
252+
Save
232253
</Button>
233254
</DialogClose>
234255
</DialogFooter>

tracecat/auth/sandbox.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
from typing import Literal, Self
99

1010
import httpx
11-
from loguru import logger
1211

1312
from tracecat.clients import AuthenticatedAPIClient
1413
from tracecat.concurrency import GatheringTaskGroup
1514
from tracecat.contexts import ctx_role
1615
from tracecat.db.schemas import Secret
16+
from tracecat.logging import logger
1717
from tracecat.secrets.encryption import decrypt_keyvalues
1818
from tracecat.secrets.models import SecretKeyValue
1919
from tracecat.secrets.service import SecretsService
@@ -88,7 +88,9 @@ def _set_secrets(self):
8888
objs=self._secret_objs,
8989
)
9090
for name, kv in self._iter_secrets():
91-
self._context[name] = {kv.key: kv.value.get_secret_value()}
91+
if name not in self._context:
92+
self._context[name] = {}
93+
self._context[name][kv.key] = kv.value.get_secret_value()
9294
else:
9395
logger.info("Setting secrets in the environment", paths=self._secret_paths)
9496
for _, kv in self._iter_secrets():
@@ -186,9 +188,11 @@ async def _get_secrets_from_service(self) -> list[Secret]:
186188

187189
async with SecretsService.with_session(role=self._role) as service:
188190
secrets: dict[str, Secret | None] = {}
191+
logger.info("Retrieving secrets", secret_names=self._secret_paths)
189192
for path in self._secret_paths:
190193
name = path.split(".")[0]
191194
secrets[name] = await service.get_secret_by_name(name)
195+
logger.info("Retrieved secrets", secrets=secrets)
192196
missing_secret_names = [name for name, secret in secrets.items() if not secret]
193197
if missing_secret_names:
194198
raise TracecatCredentialsError(

0 commit comments

Comments
 (0)