Skip to content

Commit 6ee2cd2

Browse files
committed
fix: address PR review feedback for git SSH signature
- removeSignatureHelper now preserves user-owned keys in [gpg "ssh"] sections (e.g., allowedSignersFile). Only strips devpod-managed program entry. Drops the section header only if no user keys remain. - Shell-escape signingKey in setupGitSSHSignature to handle keys with spaces or special characters. - Convert git_ssh_signature_test.go to testify suite, add positional arg assertion and dedicated buffer file test. - Add two new helper tests: PreservesUserOwnedGpgSSHKeys and DropsEmptyGpgSSHSection.
1 parent 2df87b4 commit 6ee2cd2

4 files changed

Lines changed: 131 additions & 35 deletions

File tree

cmd/agent/git_ssh_signature_test.go

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,63 @@ import (
44
"testing"
55

66
"github.com/skevetter/devpod/cmd/flags"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/suite"
79
)
810

9-
func TestGitSSHSignatureCmd_AcceptsUnknownFlags(t *testing.T) {
11+
type GitSSHSignatureTestSuite struct {
12+
suite.Suite
13+
}
14+
15+
func TestGitSSHSignatureSuite(t *testing.T) {
16+
suite.Run(t, new(GitSSHSignatureTestSuite))
17+
}
18+
19+
func (s *GitSSHSignatureTestSuite) TestAcceptsUnknownFlags() {
1020
cmd := NewGitSSHSignatureCmd(&flags.GlobalFlags{})
1121

12-
// Simulate what git passes: -Y sign -n git -f /path/to/key -U /tmp/buffer
13-
// We expect flag parsing to succeed (no "unknown shorthand flag" error).
22+
// Git may pass: -Y sign -n git -f /path/to/key -U /tmp/buffer
23+
// With FParseErrWhitelist, -U is treated as an unknown flag consuming
24+
// /tmp/buffer as its value. This is fine because git always puts the
25+
// buffer file as the last argument. We test with the buffer as a
26+
// separate positional arg (no unknown flag consuming it).
1427
err := cmd.ParseFlags(
1528
[]string{"-Y", "sign", "-n", "git", "-f", "/path/to/key", "-U", "/tmp/buffer"},
1629
)
17-
if err != nil {
18-
t.Fatalf("expected flag parsing to succeed with unknown flag -U, got: %v", err)
19-
}
30+
assert.NoError(s.T(), err, "flag parsing should succeed with unknown flag -U")
31+
}
32+
33+
func (s *GitSSHSignatureTestSuite) TestBufferFileAsPositionalArg() {
34+
cmd := NewGitSSHSignatureCmd(&flags.GlobalFlags{})
35+
36+
// Standard git invocation: -Y sign -n git -f /path/to/key /tmp/buffer
37+
// The buffer file is the last positional argument.
38+
err := cmd.ParseFlags(
39+
[]string{"-Y", "sign", "-n", "git", "-f", "/path/to/key", "/tmp/buffer"},
40+
)
41+
assert.NoError(s.T(), err)
42+
43+
args := cmd.Flags().Args()
44+
assert.NotEmpty(s.T(), args, "should have positional args")
45+
assert.Equal(s.T(), "/tmp/buffer", args[len(args)-1],
46+
"last positional arg should be the buffer file")
2047
}
2148

22-
func TestGitSSHSignatureCmd_KnownFlagsParsed(t *testing.T) {
49+
func (s *GitSSHSignatureTestSuite) TestKnownFlagsParsed() {
2350
cmd := NewGitSSHSignatureCmd(&flags.GlobalFlags{})
2451

25-
err := cmd.ParseFlags([]string{"-Y", "sign", "-n", "git", "-f", "/path/to/key", "/tmp/buffer"})
26-
if err != nil {
27-
t.Fatalf("expected flag parsing to succeed, got: %v", err)
28-
}
52+
err := cmd.ParseFlags(
53+
[]string{"-Y", "sign", "-n", "git", "-f", "/path/to/key", "/tmp/buffer"},
54+
)
55+
assert.NoError(s.T(), err, "flag parsing should succeed")
2956

3057
val, err := cmd.Flags().GetString("command")
31-
if err != nil {
32-
t.Fatalf("expected to get 'command' flag, got: %v", err)
33-
}
34-
if val != "sign" {
35-
t.Fatalf("expected command flag to be 'sign', got: %q", val)
36-
}
58+
assert.NoError(s.T(), err)
59+
assert.Equal(s.T(), "sign", val, "command flag should be 'sign'")
60+
61+
// The buffer file should be the last positional argument
62+
args := cmd.Flags().Args()
63+
assert.NotEmpty(s.T(), args, "should have positional args")
64+
assert.Equal(s.T(), "/tmp/buffer", args[len(args)-1],
65+
"last positional arg should be the buffer file")
3766
}

cmd/up.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"strings"
1616
"syscall"
1717

18+
"al.essio.dev/pkg/shellescape"
1819
"github.com/blang/semver/v4"
1920
"github.com/sirupsen/logrus"
2021
"github.com/skevetter/devpod/cmd/flags"
@@ -1565,7 +1566,10 @@ func setupGitSSHSignature(
15651566
"--context",
15661567
client.Context(),
15671568
client.Workspace(),
1568-
"--command", fmt.Sprintf("devpod agent git-ssh-signature-helper %s", signingKey),
1569+
"--command",
1570+
shellescape.QuoteCommand(
1571+
[]string{"devpod", "agent", "git-ssh-signature-helper", signingKey},
1572+
),
15691573
).CombinedOutput()
15701574
if err != nil {
15711575
return fmt.Errorf("setup git ssh signature helper: %w, output: %s", err, string(out))

pkg/gitsshsigning/helper.go

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/skevetter/devpod/pkg/command"
1111
"github.com/skevetter/devpod/pkg/file"
1212
"github.com/skevetter/log"
13-
"github.com/skevetter/log/scanner"
1413
)
1514

1615
const (
@@ -150,41 +149,72 @@ func removeGitConfigHelper(gitConfigPath, userName string) error {
150149
}
151150

152151
func removeSignatureHelper(content string) string {
153-
scan := scanner.NewScanner(strings.NewReader(content))
154152
inGpgSSHSection := false
155153
inGpgSection := false
156-
out := []string{}
154+
var gpgSSHBuffer []string
155+
var out []string
157156

158-
for scan.Scan() {
159-
line := scan.Text()
157+
for line := range strings.Lines(content) {
158+
line = strings.TrimRight(line, "\n")
160159
trimmed := strings.TrimSpace(line)
161160

162-
// Track section transitions
163-
if len(trimmed) > 0 && trimmed[0] == '[' {
161+
if isSectionHeader(trimmed) {
162+
if inGpgSSHSection {
163+
out = append(out, filterGpgSSHSection(gpgSSHBuffer)...)
164+
gpgSSHBuffer = nil
165+
}
164166
inGpgSSHSection = trimmed == `[gpg "ssh"]`
165167
inGpgSection = trimmed == "[gpg]"
166-
167-
// Skip the entire [gpg "ssh"] section header (devpod-managed)
168168
if inGpgSSHSection {
169+
gpgSSHBuffer = append(gpgSSHBuffer, line)
169170
continue
170171
}
171172
}
172173

173-
// Skip all lines inside [gpg "ssh"] section
174174
if inGpgSSHSection {
175+
gpgSSHBuffer = append(gpgSSHBuffer, line)
175176
continue
176177
}
177178

178-
// Inside [gpg] section, only skip devpod-managed keys
179-
if inGpgSection && len(trimmed) > 0 && trimmed[0] != '[' {
180-
if strings.HasPrefix(trimmed, "format = ssh") ||
181-
strings.HasPrefix(trimmed, "program = devpod-ssh-signature") {
182-
continue
183-
}
179+
if !isDevpodManagedGpgKey(inGpgSection, trimmed) {
180+
out = append(out, line)
184181
}
182+
}
185183

186-
out = append(out, line)
184+
if inGpgSSHSection {
185+
out = append(out, filterGpgSSHSection(gpgSSHBuffer)...)
187186
}
188187

189188
return strings.Join(out, "\n")
190189
}
190+
191+
func isSectionHeader(trimmed string) bool {
192+
return len(trimmed) > 0 && trimmed[0] == '['
193+
}
194+
195+
func isDevpodManagedGpgKey(inGpgSection bool, trimmed string) bool {
196+
if !inGpgSection || len(trimmed) == 0 || trimmed[0] == '[' {
197+
return false
198+
}
199+
return strings.HasPrefix(trimmed, "format = ssh") ||
200+
strings.HasPrefix(trimmed, "program = devpod-ssh-signature")
201+
}
202+
203+
// filterGpgSSHSection removes devpod-managed keys from a buffered [gpg "ssh"]
204+
// section. Returns the header + remaining user keys, or nil if no user keys remain.
205+
func filterGpgSSHSection(lines []string) []string {
206+
if len(lines) == 0 {
207+
return nil
208+
}
209+
var kept []string
210+
for _, line := range lines[1:] {
211+
trimmed := strings.TrimSpace(line)
212+
if !strings.HasPrefix(trimmed, "program = devpod-ssh-signature") {
213+
kept = append(kept, line)
214+
}
215+
}
216+
if len(kept) == 0 {
217+
return nil
218+
}
219+
return append([]string{lines[0]}, kept...)
220+
}

pkg/gitsshsigning/helper_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,36 @@ func (s *HelperTestSuite) TestRemoveSignatureHelper_NoGpgSections() {
5656

5757
assert.Equal(s.T(), input, result)
5858
}
59+
60+
func (s *HelperTestSuite) TestRemoveSignatureHelper_PreservesUserOwnedGpgSSHKeys() {
61+
input := strings.Join([]string{
62+
"[user]", "\tname = Test User",
63+
`[gpg "ssh"]`, "\tprogram = devpod-ssh-signature",
64+
"\tallowedSignersFile = ~/.ssh/allowed_signers",
65+
"[commit]", "\tgpgsign = true",
66+
}, "\n")
67+
68+
result := removeSignatureHelper(input)
69+
70+
assert.NotContains(s.T(), result, "devpod-ssh-signature")
71+
assert.Contains(s.T(), result, `[gpg "ssh"]`,
72+
"section header should be preserved when user keys remain")
73+
assert.Contains(s.T(), result, "allowedSignersFile",
74+
"user-owned key should be preserved")
75+
assert.Contains(s.T(), result, "[commit]")
76+
}
77+
78+
func (s *HelperTestSuite) TestRemoveSignatureHelper_DropsEmptyGpgSSHSection() {
79+
input := strings.Join([]string{
80+
"[user]", "\tname = Test User",
81+
`[gpg "ssh"]`, "\tprogram = devpod-ssh-signature",
82+
"[commit]", "\tgpgsign = true",
83+
}, "\n")
84+
85+
result := removeSignatureHelper(input)
86+
87+
assert.NotContains(s.T(), result, "devpod-ssh-signature")
88+
assert.NotContains(s.T(), result, `[gpg "ssh"]`,
89+
"empty section should be dropped entirely")
90+
assert.Contains(s.T(), result, "[commit]")
91+
}

0 commit comments

Comments
 (0)