Skip to content

Commit c754181

Browse files
committed
Add support for SSH private key as json.
1 parent 5d959f7 commit c754181

File tree

6 files changed

+354
-1
lines changed

6 files changed

+354
-1
lines changed

docs/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,28 @@ node {
236236
}
237237
```
238238

239+
### SSH User Private Key (JSON format)
240+
241+
An SSH *private key*, with a *username* and optional *passphrase* for the private key.
242+
243+
- Value: Valid JSON describing an object with a `username` field, `privatekey` field and optionally a `passphrase` field.
244+
- Tags:
245+
- `jenkins:credentials:type` = `jsonSshUserPrivateKey`
246+
247+
**Note:** Unlike the non-JSON-format version, the passphrase field is supported. This is because the passphrase is contained within the AWS secret data, not as a (non-secret) tag.
248+
249+
#### Example
250+
251+
AWS CLI:
252+
253+
```bash
254+
ssh-keygen -t rsa -b 4096 -C '[email protected]' -f id_rsa -N mySecretPassPhrase
255+
jq -n --arg key "$(cat id_rsa)" '{ username: "joe", privatekey: $key, passphrase: "mySecretPassPhrase" }' >json
256+
aws secretsmanager create-secret --name 'ssh-key' --secret-string 'file://json' --tags 'Key=jenkins:credentials:type,Value=jsonSshUserPrivateKey' --description 'Acme Corp SSH key'
257+
```
258+
259+
Declarative and Scripted Pipeline behavior is (exactly) the same as non-JSON-format SSH User Private Key.
260+
239261
### Certificate
240262

241263
A client certificate *keystore* in PKCS#12 format, encrypted with a zero-length password.

src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.jenkins.plugins.credentials.secretsmanager.Messages;
1313
import io.jenkins.plugins.credentials.secretsmanager.factory.certificate.AwsCertificateCredentials;
1414
import io.jenkins.plugins.credentials.secretsmanager.factory.file.AwsFileCredentials;
15+
import io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key.AwsJsonSshUserPrivateKey;
1516
import io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key.AwsSshUserPrivateKey;
1617
import io.jenkins.plugins.credentials.secretsmanager.factory.string.AwsStringCredentials;
1718
import io.jenkins.plugins.credentials.secretsmanager.factory.username_password.AwsJsonUsernamePasswordCredentials;
@@ -51,6 +52,8 @@ public static Optional<StandardCredentials> create(String arn, String name, Stri
5152
return Optional.of(new AwsJsonUsernamePasswordCredentials(name, description, new SecretSupplier(client, arn)));
5253
case Type.sshUserPrivateKey:
5354
return Optional.of(new AwsSshUserPrivateKey(name, description, new StringSupplier(client, arn), username));
55+
case Type.jsonSshUserPrivateKey:
56+
return Optional.of(new AwsJsonSshUserPrivateKey(name, description, new SecretSupplier(client, arn)));
5457
case Type.certificate:
5558
return Optional.of(new AwsCertificateCredentials(name, description, new SecretBytesSupplier(client, arn)));
5659
case Type.file:

src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Type.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ public abstract class Type {
99
public static final String usernamePassword = "usernamePassword";
1010
public static final String jsonUsernamePassword = "jsonUsernamePassword";
1111
public static final String sshUserPrivateKey = "sshUserPrivateKey";
12+
public static final String jsonSshUserPrivateKey = "jsonSshUserPrivateKey";
1213
public static final String string = "string";
1314

1415
private Type() {
15-
1616
}
1717
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
import java.util.function.Supplier;
6+
7+
import javax.annotation.Nonnull;
8+
9+
import org.kohsuke.accmod.Restricted;
10+
import org.kohsuke.accmod.restrictions.NoExternalUse;
11+
12+
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
13+
import com.cloudbees.plugins.credentials.CredentialsProvider;
14+
15+
import edu.umd.cs.findbugs.annotations.NonNull;
16+
import hudson.Extension;
17+
import hudson.util.Secret;
18+
import io.jenkins.plugins.credentials.secretsmanager.AwsCredentialsProvider;
19+
import io.jenkins.plugins.credentials.secretsmanager.Messages;
20+
import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentials;
21+
22+
public class AwsJsonSshUserPrivateKey extends BaseAwsJsonCredentials implements SSHUserPrivateKey {
23+
/**
24+
* Name of the JSON field that we expect to be present and to contain the
25+
* credential's username.
26+
*/
27+
@Restricted(NoExternalUse.class)
28+
public static final String JSON_FIELDNAME_FOR_USERNAME = "username";
29+
/**
30+
* Name of the JSON field that we expect to be present and to contain the
31+
* credential's private key.
32+
*/
33+
@Restricted(NoExternalUse.class)
34+
public static final String JSON_FIELDNAME_FOR_PRIVATE_KEY = "privatekey";
35+
/**
36+
* Name of the JSON field that we look for and, if present, expect it to contain
37+
* the credential's password. If it isn't present then we assume no passphrase.
38+
*/
39+
@Restricted(NoExternalUse.class)
40+
public static final String JSON_FIELDNAME_FOR_PASSPHRASE = "passphrase";
41+
42+
/**
43+
* Constructs a new instance.
44+
*
45+
* @param id The value for {@link #getId()}.
46+
* @param description The value for {@link #getDescription()}.
47+
* @param jsonContainingPrivateKey Supplies JSON containing a
48+
* {@value #JSON_FIELDNAME_FOR_USERNAME} field,
49+
* a {@value #JSON_FIELDNAME_FOR_PRIVATE_KEY}
50+
* field and optionally a
51+
* {@value #JSON_FIELDNAME_FOR_PASSPHRASE}
52+
* field.
53+
*/
54+
public AwsJsonSshUserPrivateKey(String id, String description, Supplier<Secret> jsonContainingPrivateKey) {
55+
super(id, description, jsonContainingPrivateKey);
56+
}
57+
58+
/**
59+
* Constructs a snapshot of an existing instance.
60+
*
61+
* @param toBeSnapshotted The instance that contains the live data to be
62+
* snapshotted.
63+
*/
64+
@Restricted(NoExternalUse.class)
65+
AwsJsonSshUserPrivateKey(AwsJsonSshUserPrivateKey toBeSnapshotted) {
66+
super(toBeSnapshotted);
67+
}
68+
69+
@NonNull
70+
@Deprecated
71+
@Override
72+
public String getPrivateKey() {
73+
return getMandatoryField(getSecretJson(), JSON_FIELDNAME_FOR_PRIVATE_KEY);
74+
}
75+
76+
@Override
77+
public Secret getPassphrase() {
78+
return Secret.fromString(getOptionalField(getSecretJson(), JSON_FIELDNAME_FOR_PASSPHRASE));
79+
}
80+
81+
@NonNull
82+
@Override
83+
public List<String> getPrivateKeys() {
84+
return Collections.singletonList(getPrivateKey());
85+
}
86+
87+
@NonNull
88+
@Override
89+
public String getUsername() {
90+
return getMandatoryField(getSecretJson(), JSON_FIELDNAME_FOR_USERNAME);
91+
}
92+
93+
@Override
94+
public boolean isUsernameSecret() {
95+
return true;
96+
}
97+
98+
@Extension
99+
@SuppressWarnings("unused")
100+
public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {
101+
@Override
102+
@Nonnull
103+
public String getDisplayName() {
104+
return Messages.sshUserPrivateKey();
105+
}
106+
107+
@Override
108+
public String getIconClassName() {
109+
return "icon-ssh-credentials-ssh-key";
110+
}
111+
112+
@Override
113+
public boolean isApplicable(CredentialsProvider provider) {
114+
return provider instanceof AwsCredentialsProvider;
115+
}
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key;
2+
3+
import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker;
4+
import hudson.Extension;
5+
import io.jenkins.plugins.credentials.secretsmanager.factory.Snapshot;
6+
7+
@Extension
8+
@SuppressWarnings("unused")
9+
public class AwsJsonSshUserPrivateKeySnapshotTaker extends CredentialsSnapshotTaker<AwsJsonSshUserPrivateKey> {
10+
@Override
11+
public Class<AwsJsonSshUserPrivateKey> type() {
12+
return AwsJsonSshUserPrivateKey.class;
13+
}
14+
15+
@Override
16+
public AwsJsonSshUserPrivateKey snapshot(AwsJsonSshUserPrivateKey credential) {
17+
return new AwsJsonSshUserPrivateKey(credential);
18+
}
19+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key;
2+
3+
import static io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.mkJson;
4+
import static io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.assertThatJsonCredentialsDescriptorIsTheSameAsTheDescriptorForNonJsonCredentials;
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
7+
import java.util.Collections;
8+
import java.util.List;
9+
import java.util.function.Supplier;
10+
11+
import org.junit.Test;
12+
13+
import com.cloudbees.plugins.credentials.CredentialsUnavailableException;
14+
15+
import hudson.util.Secret;
16+
import io.jenkins.plugins.credentials.secretsmanager.Messages;
17+
import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.StubSingleShotSupplier;
18+
import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.StubSupplier;
19+
20+
public class AwsJsonSshUserPrivateKeyTest {
21+
@Test
22+
public void getPassphraseGivenValidJsonThenReturnsSecretPassphrase() {
23+
// Given
24+
final String id = "testId";
25+
final String description = "some test description";
26+
final String expected = "mySecretPassphrase";
27+
final String json = mkUsernameKeyAndPassphraseJson("myUsername", "myKey", expected);
28+
final Secret stubSecret = Secret.fromString(json);
29+
final Supplier<Secret> stubSupplier = new StubSupplier<>(stubSecret);
30+
final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier);
31+
32+
// When
33+
final Secret actualSecret = instance.getPassphrase();
34+
final String actualPassphrase = actualSecret.getPlainText();
35+
36+
// Then
37+
assertThat(actualPassphrase).isEqualTo(expected);
38+
}
39+
40+
@Test
41+
public void getPassphraseGivenValidJsonWithNoPassphraseThenReturnsEmpty() {
42+
// Given
43+
final String id = "testId";
44+
final String description = "some test description";
45+
final String expected = "";
46+
final String json = mkUsernameKeyAndNoPassphraseJson("myUsername", "myKey");
47+
final Secret stubSecret = Secret.fromString(json);
48+
final Supplier<Secret> stubSupplier = new StubSupplier<>(stubSecret);
49+
final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier);
50+
51+
// When
52+
final Secret actualSecret = instance.getPassphrase();
53+
final String actualPassphrase = actualSecret.getPlainText();
54+
55+
// Then
56+
assertThat(actualPassphrase).isEqualTo(expected);
57+
}
58+
59+
@Test
60+
public void getPrivateKeysGivenValidJsonThenReturnsSingleKey() {
61+
// Given
62+
final String id = "testId";
63+
final String description = "some test description";
64+
final List<String> expected = Collections.singletonList("theKeyThatISet");
65+
final String json = mkUsernameKeyAndPassphraseJson("myUsername", expected.get(0), "mySecretPassphrase");
66+
final Secret stubSecret = Secret.fromString(json);
67+
final Supplier<Secret> stubSupplier = new StubSupplier<>(stubSecret);
68+
final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier);
69+
70+
// When
71+
final List<String> actual = instance.getPrivateKeys();
72+
73+
// Then
74+
assertThat(actual).isEqualTo(expected);
75+
}
76+
77+
@Test
78+
public void getUsernameGivenValidJsonThenReturnsUsername() {
79+
// Given
80+
final String id = "testId";
81+
final String description = "some test description";
82+
final String expected = "myUsername";
83+
final String json = mkUsernameKeyAndPassphraseJson(expected, "myKey", "mySecretPassphrase");
84+
final Secret stubSecret = Secret.fromString(json);
85+
final Supplier<Secret> stubSupplier = new StubSupplier<>(stubSecret);
86+
final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier);
87+
88+
// When
89+
final String actual = instance.getUsername();
90+
91+
// Then
92+
assertThat(actual).isEqualTo(expected);
93+
}
94+
95+
@Test
96+
public void isUsernameSecretGivenAnythingThenReturnsTrue() {
97+
// Given
98+
final String id = "testId";
99+
final String description = "some test description";
100+
final boolean expected = true;
101+
final String json = mkUsernameKeyAndPassphraseJson("myUser", "myKey", "mySecretPassphrase");
102+
final Secret stubSecret = Secret.fromString(json);
103+
final Supplier<Secret> stubSupplier = new StubSupplier<>(stubSecret);
104+
final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier);
105+
106+
// When
107+
final boolean actual = instance.isUsernameSecret();
108+
109+
// Then
110+
assertThat(actual).isEqualTo(expected);
111+
}
112+
113+
@Test
114+
public void getUsernameGivenInvalidJsonThenReturnsThrows() {
115+
// Given
116+
final String id = "testId";
117+
final String description = "some test description";
118+
final String unexpectedFieldName = "potentiallySecretFieldName";
119+
final String unexpectedValue = "potentiallySecretValue";
120+
final String json = mkJson(unexpectedFieldName, unexpectedValue, unexpectedFieldName + "2", unexpectedValue);
121+
final Secret stubSecret = Secret.fromString(json);
122+
final Supplier<Secret> stubSupplier = new StubSupplier<>(stubSecret);
123+
final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier);
124+
125+
// When
126+
CredentialsUnavailableException actual = null;
127+
try {
128+
instance.getUsername();
129+
} catch (CredentialsUnavailableException ex) {
130+
actual = ex;
131+
}
132+
133+
// Then
134+
assertThat(actual).isNotNull();
135+
assertThat(actual.getProperty()).isEqualTo("secret");
136+
assertThat(actual.getMessage()).contains(id);
137+
assertThat(actual.getMessage()).contains(AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_USERNAME);
138+
assertThat(actual.getMessage())
139+
.contains(Messages.wrongJsonError(id, AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_USERNAME));
140+
assertThat(actual.getMessage()).doesNotContain(unexpectedFieldName);
141+
assertThat(actual.getMessage()).doesNotContain(unexpectedValue);
142+
}
143+
144+
@Test
145+
public void snapshotConstructorGivenInstanceToSnapshotThenReturnsClone() {
146+
// Given
147+
final String expectedUsername = "myUser";
148+
final String expectedPassphrase = "mySecretPassphrase";
149+
final String expectedId = "someId";
150+
final String expectedDescription = "someDescription";
151+
final String expectedKey = "someKeyThatIMade";
152+
final String json = mkUsernameKeyAndPassphraseJson(expectedUsername, expectedKey, expectedPassphrase);
153+
final Secret secretJson = Secret.fromString(json);
154+
final StubSingleShotSupplier<Secret> stubSupplier = new StubSingleShotSupplier<Secret>(secretJson);
155+
final AwsJsonSshUserPrivateKey original = new AwsJsonSshUserPrivateKey(expectedId, expectedDescription,
156+
stubSupplier);
157+
158+
// When
159+
final AwsJsonSshUserPrivateKey actual = new AwsJsonSshUserPrivateKey(original);
160+
161+
// Then
162+
final String actualId = actual.getId();
163+
assertThat(actualId).isEqualTo(expectedId);
164+
final String actualDescription = actual.getDescription();
165+
assertThat(actualDescription).isEqualTo(expectedDescription);
166+
final String actualUsername = actual.getUsername();
167+
assertThat(actualUsername).isEqualTo(expectedUsername);
168+
final String actualPassphrase = actual.getPassphrase().getPlainText();
169+
assertThat(actualPassphrase).isEqualTo(expectedPassphrase);
170+
final String actualKey = actual.getPrivateKeys().get(0);
171+
assertThat(actualKey).isEqualTo(expectedKey);
172+
}
173+
174+
@Test
175+
public void ourDescriptorIsTheSameAsDescriptorForNonJsonCredentials() {
176+
// Given
177+
final AwsSshUserPrivateKey.DescriptorImpl expected = new AwsSshUserPrivateKey.DescriptorImpl();
178+
final AwsJsonSshUserPrivateKey.DescriptorImpl instance = new AwsJsonSshUserPrivateKey.DescriptorImpl();
179+
assertThatJsonCredentialsDescriptorIsTheSameAsTheDescriptorForNonJsonCredentials(instance, expected);
180+
}
181+
182+
private static String mkUsernameKeyAndPassphraseJson(String username, String key, String passphrase) {
183+
return mkJson(AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_USERNAME, username,
184+
AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_PASSPHRASE, passphrase,
185+
AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_PRIVATE_KEY, key);
186+
}
187+
188+
private static String mkUsernameKeyAndNoPassphraseJson(String username, String key) {
189+
return mkJson(AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_USERNAME, username,
190+
AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_PRIVATE_KEY, key);
191+
}
192+
}

0 commit comments

Comments
 (0)