Skip to content

Commit 698b334

Browse files
committed
preserve existing mode on config file if present
1 parent 685101a commit 698b334

File tree

2 files changed

+35
-13
lines changed

2 files changed

+35
-13
lines changed

src/sshConfig.test.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { SSHConfig } from "./sshConfig"
55
const sshFilePath = "~/.config/ssh"
66

77
const mockFileSystem = {
8-
readFile: vi.fn(),
98
mkdir: vi.fn(),
10-
writeFile: vi.fn(),
9+
readFile: vi.fn(),
1110
rename: vi.fn(),
11+
stat: vi.fn(),
12+
writeFile: vi.fn(),
1213
}
1314

1415
afterEach(() => {
@@ -17,6 +18,7 @@ afterEach(() => {
1718

1819
it("creates a new file and adds config with empty label", async () => {
1920
mockFileSystem.readFile.mockRejectedValueOnce("No file found")
21+
mockFileSystem.stat.mockRejectedValueOnce({ code: "ENOENT" })
2022

2123
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)
2224
await sshConfig.load()
@@ -49,6 +51,7 @@ Host coder-vscode--*
4951

5052
it("creates a new file and adds the config", async () => {
5153
mockFileSystem.readFile.mockRejectedValueOnce("No file found")
54+
mockFileSystem.stat.mockRejectedValueOnce({ code: "ENOENT" })
5255

5356
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)
5457
await sshConfig.load()
@@ -88,6 +91,7 @@ it("adds a new coder config in an existent SSH configuration", async () => {
8891
StrictHostKeyChecking=no
8992
UserKnownHostsFile=/dev/null`
9093
mockFileSystem.readFile.mockResolvedValueOnce(existentSSHConfig)
94+
mockFileSystem.stat.mockResolvedValueOnce({ mode: 0o644 })
9195

9296
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)
9397
await sshConfig.load()
@@ -113,7 +117,7 @@ Host coder-vscode.dev.coder.com--*
113117

114118
expect(mockFileSystem.writeFile).toBeCalledWith(expect.stringContaining(sshFilePath), expectedOutput, {
115119
encoding: "utf-8",
116-
mode: 384,
120+
mode: 0o644,
117121
})
118122
expect(mockFileSystem.rename).toBeCalledWith(expect.stringContaining(sshFilePath + "."), sshFilePath)
119123
})
@@ -150,6 +154,7 @@ Host coder-vscode.dev.coder.com--*
150154
Host *
151155
SetEnv TEST=1`
152156
mockFileSystem.readFile.mockResolvedValueOnce(existentSSHConfig)
157+
mockFileSystem.stat.mockResolvedValueOnce({ mode: 0o644 })
153158

154159
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)
155160
await sshConfig.load()
@@ -178,7 +183,7 @@ Host *
178183

179184
expect(mockFileSystem.writeFile).toBeCalledWith(expect.stringContaining(sshFilePath), expectedOutput, {
180185
encoding: "utf-8",
181-
mode: 384,
186+
mode: 0o644,
182187
})
183188
expect(mockFileSystem.rename).toBeCalledWith(expect.stringContaining(sshFilePath + "."), sshFilePath)
184189
})
@@ -199,6 +204,7 @@ Host coder-vscode--*
199204
UserKnownHostsFile=/dev/null
200205
# --- END CODER VSCODE ---`
201206
mockFileSystem.readFile.mockResolvedValueOnce(existentSSHConfig)
207+
mockFileSystem.stat.mockResolvedValueOnce({ mode: 0o644 })
202208

203209
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)
204210
await sshConfig.load()
@@ -224,7 +230,7 @@ Host coder-vscode.dev.coder.com--*
224230

225231
expect(mockFileSystem.writeFile).toBeCalledWith(expect.stringContaining(sshFilePath), expectedOutput, {
226232
encoding: "utf-8",
227-
mode: 384,
233+
mode: 0o644,
228234
})
229235
expect(mockFileSystem.rename).toBeCalledWith(expect.stringContaining(sshFilePath + "."), sshFilePath)
230236
})
@@ -233,6 +239,7 @@ it("it does not remove a user-added block that only matches the host of an old c
233239
const existentSSHConfig = `Host coder-vscode--*
234240
ForwardAgent=yes`
235241
mockFileSystem.readFile.mockResolvedValueOnce(existentSSHConfig)
242+
mockFileSystem.stat.mockResolvedValueOnce({ mode: 0o644 })
236243

237244
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)
238245
await sshConfig.load()
@@ -259,7 +266,7 @@ Host coder-vscode.dev.coder.com--*
259266

260267
expect(mockFileSystem.writeFile).toBeCalledWith(expect.stringContaining(sshFilePath), expectedOutput, {
261268
encoding: "utf-8",
262-
mode: 384,
269+
mode: 0o644,
263270
})
264271
expect(mockFileSystem.rename).toBeCalledWith(expect.stringContaining(sshFilePath + "."), sshFilePath)
265272
})
@@ -451,6 +458,7 @@ Host afterconfig
451458

452459
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)
453460
mockFileSystem.readFile.mockResolvedValueOnce(existentSSHConfig)
461+
mockFileSystem.stat.mockResolvedValueOnce({ mode: 0o644 })
454462
await sshConfig.load()
455463

456464
const expectedOutput = `Host beforeconfig
@@ -494,13 +502,15 @@ Host afterconfig
494502

495503
expect(mockFileSystem.writeFile).toBeCalledWith(expect.stringContaining(sshFilePath), expectedOutput, {
496504
encoding: "utf-8",
497-
mode: 384,
505+
mode: 0o644,
498506
})
499507
expect(mockFileSystem.rename).toBeCalledWith(expect.stringContaining(sshFilePath + "."), sshFilePath)
500508
})
501509

502510
it("override values", async () => {
503511
mockFileSystem.readFile.mockRejectedValueOnce("No file found")
512+
mockFileSystem.stat.mockRejectedValueOnce({ code: "ENOENT" })
513+
504514
const sshConfig = new SSHConfig(sshFilePath, mockFileSystem)
505515
await sshConfig.load()
506516
await sshConfig.update(

src/sshConfig.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdir, readFile, writeFile, rename } from "fs/promises"
1+
import { mkdir, readFile, rename, stat, writeFile } from "fs/promises"
22
import path from "path"
33
import { countSubstring } from "./util"
44

@@ -20,17 +20,19 @@ export interface SSHValues {
2020

2121
// Interface for the file system to make it easier to test
2222
export interface FileSystem {
23-
readFile: typeof readFile
2423
mkdir: typeof mkdir
25-
writeFile: typeof writeFile
24+
readFile: typeof readFile
2625
rename: typeof rename
26+
stat: typeof stat
27+
writeFile: typeof writeFile
2728
}
2829

2930
const defaultFileSystem: FileSystem = {
30-
readFile,
3131
mkdir,
32-
writeFile,
32+
readFile,
3333
rename,
34+
stat,
35+
writeFile,
3436
}
3537

3638
// mergeSSHConfigValues will take a given ssh config and merge it with the overrides
@@ -221,14 +223,24 @@ export class SSHConfig {
221223
}
222224

223225
private async save() {
226+
// We want to preserve the original file mode.
227+
const existingMode = await this.fileSystem
228+
.stat(this.filePath)
229+
.then((stat) => stat.mode)
230+
.catch((ex) => {
231+
if (ex.code && ex.code === "ENOENT") {
232+
return 0o600 // default to 0600 if file does not exist
233+
}
234+
throw ex // Any other error is unexpected
235+
})
224236
await this.fileSystem.mkdir(path.dirname(this.filePath), {
225237
mode: 0o700, // only owner has rwx permission, not group or everyone.
226238
recursive: true,
227239
})
228240
const randSuffix = Math.random().toString(36).substring(8)
229241
const tempFilePath = `${this.filePath}.${randSuffix}`
230242
await this.fileSystem.writeFile(tempFilePath, this.getRaw(), {
231-
mode: 0o600, // owner rw
243+
mode: existingMode,
232244
encoding: "utf-8",
233245
})
234246
await this.fileSystem.rename(tempFilePath, this.filePath)

0 commit comments

Comments
 (0)