Skip to content

Commit c656008

Browse files
committed
Add unit tests for PluginSshSessionFactory SSH backend
Tests cover: - Constructor and TransportConfigCallback interface contract - SshTransport configuration with SshdSessionFactory - Non-SSH transports are ignored - Private key provided as default identity via temp file - Temp key file caching (no accumulation across calls) - Temp key file has POSIX 0600 permissions - Null private key delegates to default identities - StrictHostKeyChecking=no returns accept-all ServerKeyDatabase - StrictHostKeyChecking=yes uses default ServerKeyDatabase - Null sshConfig uses default ServerKeyDatabase - Lazy factory creation picks up current sshConfig on each configure() Made-with: Cursor
1 parent 9f847d1 commit c656008

1 file changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package com.rundeck.plugin.util
2+
3+
import org.eclipse.jgit.transport.CredentialsProvider
4+
import org.eclipse.jgit.transport.SshTransport
5+
import org.eclipse.jgit.transport.Transport
6+
import org.eclipse.jgit.transport.sshd.ServerKeyDatabase
7+
import org.eclipse.jgit.transport.sshd.SshdSessionFactory
8+
import spock.lang.Specification
9+
import spock.lang.TempDir
10+
11+
import java.nio.file.Files
12+
import java.nio.file.Path
13+
import java.nio.file.attribute.PosixFilePermissions
14+
import java.security.KeyPairGenerator
15+
import java.security.PublicKey
16+
17+
class PluginSshSessionFactorySpec extends Specification {
18+
19+
private static final byte[] FAKE_KEY = "-----BEGIN RSA PRIVATE KEY-----\nfake-key-data\n-----END RSA PRIVATE KEY-----".bytes
20+
21+
def "constructor accepts private key and implements TransportConfigCallback"() {
22+
when:
23+
def factory = new PluginSshSessionFactory(FAKE_KEY)
24+
25+
then:
26+
factory != null
27+
factory instanceof org.eclipse.jgit.api.TransportConfigCallback
28+
}
29+
30+
def "configure sets SshdSessionFactory on SshTransport"() {
31+
given:
32+
def factory = new PluginSshSessionFactory(FAKE_KEY)
33+
factory.sshConfig = [StrictHostKeyChecking: 'no']
34+
35+
def sshTransport = Mock(SshTransport)
36+
37+
when:
38+
factory.configure(sshTransport)
39+
40+
then:
41+
1 * sshTransport.setSshSessionFactory(_ as SshdSessionFactory)
42+
}
43+
44+
def "configure ignores non-SSH transports"() {
45+
given:
46+
def factory = new PluginSshSessionFactory(FAKE_KEY)
47+
def transport = Mock(Transport)
48+
49+
when:
50+
factory.configure(transport)
51+
52+
then:
53+
0 * transport._(*_)
54+
}
55+
56+
def "sshConfig is available to session factory when set before configure"() {
57+
given:
58+
def factory = new PluginSshSessionFactory(FAKE_KEY)
59+
factory.sshConfig = [StrictHostKeyChecking: 'no']
60+
61+
SshdSessionFactory captured = null
62+
def sshTransport = Mock(SshTransport) {
63+
setSshSessionFactory(_) >> { args -> captured = args[0] }
64+
}
65+
66+
when:
67+
factory.configure(sshTransport)
68+
69+
then:
70+
captured != null
71+
captured instanceof SshdSessionFactory
72+
}
73+
74+
def "session factory provides private key as default identity"() {
75+
given:
76+
def factory = new PluginSshSessionFactory(FAKE_KEY)
77+
factory.sshConfig = [:]
78+
79+
SshdSessionFactory captured = null
80+
def sshTransport = Mock(SshTransport) {
81+
setSshSessionFactory(_) >> { args -> captured = args[0] }
82+
}
83+
factory.configure(sshTransport)
84+
85+
when:
86+
List<Path> identities = captured.getDefaultIdentities(new File(System.getProperty("java.io.tmpdir")))
87+
88+
then:
89+
identities.size() == 1
90+
Files.exists(identities[0])
91+
identities[0].toFile().name.startsWith("rundeck-git-key-")
92+
identities[0].toFile().name.endsWith(".pem")
93+
Files.readAllBytes(identities[0]) == FAKE_KEY
94+
}
95+
96+
def "temp key file is cached across multiple calls"() {
97+
given:
98+
def factory = new PluginSshSessionFactory(FAKE_KEY)
99+
factory.sshConfig = [:]
100+
101+
SshdSessionFactory captured = null
102+
def sshTransport = Mock(SshTransport) {
103+
setSshSessionFactory(_) >> { args -> captured = args[0] }
104+
}
105+
factory.configure(sshTransport)
106+
107+
def tmpDir = new File(System.getProperty("java.io.tmpdir"))
108+
109+
when:
110+
List<Path> firstCall = captured.getDefaultIdentities(tmpDir)
111+
List<Path> secondCall = captured.getDefaultIdentities(tmpDir)
112+
113+
then:
114+
firstCall[0] == secondCall[0]
115+
}
116+
117+
def "temp key file has restricted permissions on POSIX systems"() {
118+
given:
119+
def factory = new PluginSshSessionFactory(FAKE_KEY)
120+
factory.sshConfig = [:]
121+
122+
SshdSessionFactory captured = null
123+
def sshTransport = Mock(SshTransport) {
124+
setSshSessionFactory(_) >> { args -> captured = args[0] }
125+
}
126+
factory.configure(sshTransport)
127+
128+
when:
129+
List<Path> identities = captured.getDefaultIdentities(new File(System.getProperty("java.io.tmpdir")))
130+
def perms = Files.getPosixFilePermissions(identities[0])
131+
132+
then:
133+
perms == PosixFilePermissions.fromString("rw-------")
134+
}
135+
136+
def "session factory without private key delegates to default identities"() {
137+
given:
138+
def factory = new PluginSshSessionFactory(null)
139+
factory.sshConfig = [:]
140+
141+
SshdSessionFactory captured = null
142+
def sshTransport = Mock(SshTransport) {
143+
setSshSessionFactory(_) >> { args -> captured = args[0] }
144+
}
145+
factory.configure(sshTransport)
146+
147+
when:
148+
List<Path> identities = captured.getDefaultIdentities(new File(System.getProperty("java.io.tmpdir")))
149+
150+
then:
151+
notThrown(Exception)
152+
identities != null
153+
}
154+
155+
def "StrictHostKeyChecking=no returns accept-all ServerKeyDatabase"() {
156+
given:
157+
def factory = new PluginSshSessionFactory(FAKE_KEY)
158+
factory.sshConfig = [StrictHostKeyChecking: 'no']
159+
160+
SshdSessionFactory captured = null
161+
def sshTransport = Mock(SshTransport) {
162+
setSshSessionFactory(_) >> { args -> captured = args[0] }
163+
}
164+
factory.configure(sshTransport)
165+
166+
def homeDir = new File(System.getProperty("user.home"))
167+
def sshDir = new File(homeDir, ".ssh")
168+
169+
when:
170+
ServerKeyDatabase db = captured.getServerKeyDatabase(homeDir, sshDir)
171+
172+
then:
173+
db != null
174+
175+
and:
176+
def keyPairGen = KeyPairGenerator.getInstance("RSA")
177+
keyPairGen.initialize(2048)
178+
PublicKey randomKey = keyPairGen.generateKeyPair().getPublic()
179+
def addr = new InetSocketAddress("github.com", 22)
180+
181+
db.accept("github.com:22", addr, randomKey, null, null) == true
182+
db.lookup("github.com:22", addr, null) == []
183+
}
184+
185+
def "StrictHostKeyChecking=yes uses default ServerKeyDatabase"() {
186+
given:
187+
def factory = new PluginSshSessionFactory(FAKE_KEY)
188+
factory.sshConfig = [StrictHostKeyChecking: 'yes']
189+
190+
SshdSessionFactory captured = null
191+
def sshTransport = Mock(SshTransport) {
192+
setSshSessionFactory(_) >> { args -> captured = args[0] }
193+
}
194+
factory.configure(sshTransport)
195+
196+
def homeDir = new File(System.getProperty("user.home"))
197+
def sshDir = new File(homeDir, ".ssh")
198+
199+
when:
200+
ServerKeyDatabase db = captured.getServerKeyDatabase(homeDir, sshDir)
201+
202+
then:
203+
db != null
204+
}
205+
206+
def "null sshConfig uses default ServerKeyDatabase"() {
207+
given:
208+
def factory = new PluginSshSessionFactory(FAKE_KEY)
209+
factory.sshConfig = null
210+
211+
SshdSessionFactory captured = null
212+
def sshTransport = Mock(SshTransport) {
213+
setSshSessionFactory(_) >> { args -> captured = args[0] }
214+
}
215+
factory.configure(sshTransport)
216+
217+
def homeDir = new File(System.getProperty("user.home"))
218+
def sshDir = new File(homeDir, ".ssh")
219+
220+
when:
221+
ServerKeyDatabase db = captured.getServerKeyDatabase(homeDir, sshDir)
222+
223+
then:
224+
db != null
225+
}
226+
227+
def "each call to configure creates a fresh session factory with current sshConfig"() {
228+
given:
229+
def factory = new PluginSshSessionFactory(FAKE_KEY)
230+
231+
SshdSessionFactory first = null
232+
SshdSessionFactory second = null
233+
def sshTransport = Mock(SshTransport) {
234+
setSshSessionFactory(_) >> { args ->
235+
if (first == null) first = args[0]
236+
else second = args[0]
237+
}
238+
}
239+
240+
when:
241+
factory.sshConfig = [StrictHostKeyChecking: 'yes']
242+
factory.configure(sshTransport)
243+
factory.sshConfig = [StrictHostKeyChecking: 'no']
244+
factory.configure(sshTransport)
245+
246+
then:
247+
first != null
248+
second != null
249+
!first.is(second)
250+
}
251+
}

0 commit comments

Comments
 (0)