Repository navigation
fix(connections): keep secrets in the keychain by default and write config files 0600 #980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5a488fd
946c76a
bb0b80a
34a4bca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,7 @@ import { SshConnectionsModal } from "./SshConnectionsModal"; | |
| import { K8sConnectionsModal } from "./K8sConnectionsModal"; | ||
| import { Select } from "../ui/Select"; | ||
| import { SlotAnchor } from "../ui/SlotAnchor"; | ||
| import { useAlert } from "../../hooks/useAlert"; | ||
| import { useDrivers } from "../../hooks/useDrivers"; | ||
| import { useSettings } from "../../hooks/useSettings"; | ||
| import { usePluginSlotRegistry } from "../../hooks/usePluginSlotRegistry"; | ||
|
|
@@ -266,6 +267,7 @@ export const NewConnectionModal = ({ | |
| initialConnection, | ||
| }: NewConnectionModalProps) => { | ||
| const { t } = useTranslation(); | ||
| const { showAlert } = useAlert(); | ||
| const { drivers, refresh: refreshDrivers } = useDrivers(); | ||
| const { settings, updateSetting } = useSettings(); | ||
| const { invalidate: invalidateK8sAsync, run: runK8sAsync } = useLatestAsync(); | ||
|
|
@@ -366,6 +368,11 @@ export const NewConnectionModal = ({ | |
| ssh_enabled: false, | ||
| ssh_port: 22, | ||
| k8s_enabled: false, | ||
| // New connections store secrets in the keychain. An edit starts from the | ||
| // saved flag so a slow credential load cannot flip an existing connection. | ||
| save_in_keychain: initialConnection | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SUGGESTION: Explicit default for existing connection's
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done, switched to ?? false |
||
| ? (initialConnection.params.save_in_keychain ?? false) | ||
| : true, | ||
| }); | ||
| const [selectedDatabasesState, setSelectedDatabasesState] = useState< | ||
| string[] | ||
|
|
@@ -1934,6 +1941,7 @@ export const NewConnectionModal = ({ | |
| ssh_port: 22, | ||
| k8s_enabled: false, | ||
| ssm_enabled: false, | ||
| save_in_keychain: true, | ||
| }); | ||
| setSelectedDatabasesState([]); | ||
| setLoadAllDatabases(true); | ||
|
|
@@ -1977,7 +1985,7 @@ export const NewConnectionModal = ({ | |
| ssh_password: undefined, | ||
| ssh_key_file: undefined, | ||
| ssh_key_passphrase: undefined, | ||
| save_in_keychain: false, | ||
| save_in_keychain: true, | ||
| k8s_enabled: false, | ||
| k8s_connection_id: undefined, | ||
| k8s_context: undefined, | ||
|
|
@@ -2361,6 +2369,7 @@ export const NewConnectionModal = ({ | |
| setTestResult(null); | ||
| try { | ||
| let savedConnectionId: string; | ||
| let persistedInKeychain = params.save_in_keychain; | ||
| if (initialConnection) { | ||
| // An omitted password means "keep the stored one". While a plugin | ||
| // hides the login inputs the empty password is deliberate: it is | ||
|
|
@@ -2369,25 +2378,32 @@ export const NewConnectionModal = ({ | |
| delete params.password; | ||
| } | ||
| if (!params.ssh_password?.trim()) delete params.ssh_password; | ||
| await invoke("update_connection", { | ||
| const updated = await invoke<{ | ||
| params?: { save_in_keychain?: boolean }; | ||
| }>("update_connection", { | ||
| id: initialConnection.id, | ||
| name, | ||
| params, | ||
| detectJsonInTextColumns: detectJsonInTextColumns ? true : null, | ||
| environment: environment || null, | ||
| }); | ||
| persistedInKeychain = updated?.params?.save_in_keychain; | ||
| savedConnectionId = initialConnection.id; | ||
| await invoke("set_connection_appearance", { | ||
| id: initialConnection.id, | ||
| appearance: appearancePayload ?? null, | ||
| }); | ||
| } else { | ||
| const saved = await invoke<{ id: string }>("save_connection", { | ||
| const saved = await invoke<{ | ||
| id: string; | ||
| params?: { save_in_keychain?: boolean }; | ||
| }>("save_connection", { | ||
| name, | ||
| params, | ||
| detectJsonInTextColumns: detectJsonInTextColumns ? true : null, | ||
| environment: environment || null, | ||
| }); | ||
| persistedInKeychain = saved.params?.save_in_keychain; | ||
| savedConnectionId = saved.id; | ||
| if (appearancePayload) { | ||
| await invoke("set_connection_appearance", { | ||
|
|
@@ -2415,6 +2431,12 @@ export const NewConnectionModal = ({ | |
| return; | ||
| } | ||
|
|
||
| if (params.save_in_keychain && persistedInKeychain === false) { | ||
| showAlert(t("newConnection.secretsStoredInPlainText"), { | ||
| kind: "warning", | ||
| }); | ||
| } | ||
|
|
||
| if (onSave) onSave(); | ||
| wasSavedRef.current = true; | ||
|
|
||
|
|
@@ -2905,19 +2927,34 @@ export const NewConnectionModal = ({ | |
| )} | ||
|
|
||
| {/* Keychain */} | ||
| <label className="flex items-center gap-2 cursor-pointer select-none w-fit"> | ||
| <input | ||
| type="checkbox" | ||
| checked={!!formData.save_in_keychain} | ||
| onChange={(e) => { | ||
| updateField("save_in_keychain", e.target.checked); | ||
| }} | ||
| className="accent-accent-primary w-3.5 h-3.5 rounded" | ||
| /> | ||
| <span className="text-xs text-secondary"> | ||
| {t("newConnection.saveKeychain")} | ||
| </span> | ||
| </label> | ||
| <div className="flex flex-col gap-1"> | ||
| <label className="flex items-center gap-2 cursor-pointer select-none w-fit"> | ||
| <input | ||
| type="checkbox" | ||
| checked={!!formData.save_in_keychain} | ||
| onChange={(e) => { | ||
| updateField("save_in_keychain", e.target.checked); | ||
| }} | ||
| aria-describedby={ | ||
| formData.save_in_keychain | ||
| ? undefined | ||
| : "connection-secrets-plaintext-warning" | ||
| } | ||
| className="accent-accent-primary w-3.5 h-3.5 rounded" | ||
| /> | ||
| <span className="text-xs text-secondary"> | ||
| {t("newConnection.saveKeychain")} | ||
| </span> | ||
| </label> | ||
| {!formData.save_in_keychain && ( | ||
| <p | ||
| id="connection-secrets-plaintext-warning" | ||
| className="text-xs text-accent-warning" | ||
| > | ||
| {t("newConnection.secretsStoredInPlainText")} | ||
| </p> | ||
| )} | ||
| </div> | ||
| </> | ||
| )} | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SUGGESTION: Redundant permission setting in
write_private_fileOn Unix, this function sets permissions three times:
set_permissionson existing filemode(0o600)inOpenOptions(creates file with 0600)set_permissionsagain after writeThe
mode(0o600)at open already ensures the new file has correct permissions. The firstset_permissionsis only needed for existing files, and the final one is redundant. Consider simplifying to: set permissions on existing file if needed, then open withmode(0o600)and write.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep the last set_permissions was redundant, dropped it. kept the one before open though since mode() only kicks in when the file is created, so an existing 0644 file still needs tightening before the secrets get written