Skip to content

Commit cfa9c39

Browse files
pjdartonchriskilding
authored andcommitted
Base class for JSON-format secret data
1 parent 3dcdb98 commit cfa9c39

File tree

15 files changed

+1361
-1
lines changed

15 files changed

+1361
-1
lines changed

docs/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,24 @@ node {
167167
}
168168
```
169169

170+
### Username with Password (JSON format)
171+
172+
A *username* and *password* pair.
173+
174+
- Value: Valid JSON describing an object with a `username` field and and `password` field.
175+
- Tags:
176+
- `jenkins:credentials:type` = `jsonUsernamePassword`
177+
178+
#### Example
179+
180+
AWS CLI:
181+
182+
```bash
183+
aws secretsmanager create-secret --name 'artifactory' --secret-string '{ username: "joe", password: "supersecret" }' --tags 'Key=jenkins:credentials:type,Value=jsonUsernamePassword' --description 'Acme Corp Artifactory login'
184+
```
185+
186+
Declarative and Scripted Pipeline behavior is (exactly) the same as non-JSON-format Username and Password.
187+
170188
### SSH User Private Key
171189

172190
An SSH *private key*, with a *username*.
@@ -218,6 +236,28 @@ node {
218236
}
219237
```
220238

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+
221261
### Certificate
222262

223263
A client certificate *keystore* in PKCS#12 format, encrypted with a zero-length password.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package io.jenkins.plugins.credentials.secretsmanager.factory;
2+
3+
import java.util.function.Supplier;
4+
5+
import org.kohsuke.accmod.Restricted;
6+
import org.kohsuke.accmod.restrictions.NoExternalUse;
7+
8+
import com.cloudbees.plugins.credentials.CredentialsUnavailableException;
9+
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
10+
11+
import edu.umd.cs.findbugs.annotations.NonNull;
12+
import hudson.util.Secret;
13+
import io.jenkins.plugins.credentials.secretsmanager.Messages;
14+
import net.sf.json.JSON;
15+
import net.sf.json.JSONException;
16+
import net.sf.json.JSONObject;
17+
import net.sf.json.JSONSerializer;
18+
19+
/**
20+
* Base class for credentials where the AWS secret data is JSON format instead
21+
* of raw data.
22+
*/
23+
@Restricted(NoExternalUse.class)
24+
public abstract class BaseAwsJsonCredentials extends BaseStandardCredentials {
25+
/** How to access the JSON */
26+
private final Supplier<Secret> json;
27+
28+
/**
29+
* Constructs a new instance with new data.
30+
*
31+
* @param id The value for {@link #getId()}.
32+
* @param description The value for {@link #getDescription()}.
33+
* @param json Supplies the data that {@link #getSecretJson()} will
34+
* decode.
35+
*/
36+
protected BaseAwsJsonCredentials(String id, String description, Supplier<Secret> json) {
37+
super(id, description);
38+
this.json = json;
39+
}
40+
41+
/**
42+
* Constructs an instance that is an unchanging snapshot of another instance.
43+
*
44+
* @param toSnapshot The instance to be copied.
45+
*/
46+
protected BaseAwsJsonCredentials(BaseAwsJsonCredentials toSnapshot) {
47+
super(toSnapshot.getId(), toSnapshot.getDescription());
48+
final Secret secretDataToSnapshot = toSnapshot.json.get();
49+
this.json = new Snapshot<Secret>(secretDataToSnapshot);
50+
}
51+
52+
// Note:
53+
// We MUST NOT tell anyone what the JSON is, or give any hints as to its
54+
// contents, as that could then leak sensitive data so, if anything goes wrong,
55+
// we have to suppress the informative exception(s) and just tell the user that
56+
// it didn't work.
57+
58+
/**
59+
* Reads the secret JSON and returns the field requested.
60+
*
61+
* @param secretJson The {@link JSONObject} we're going to look in, which likely
62+
* came from {@link #getSecretJson()}.
63+
* @param fieldname The (top-level) field that we want (which must be a
64+
* {@link String}).
65+
* @return The contents of that JSON field.
66+
* @throws CredentialsUnavailableException if the JSON is missing the field, or
67+
* the field is not a {@link String}.
68+
*/
69+
protected String getMandatoryField(@NonNull JSONObject secretJson, @NonNull String fieldname) {
70+
final String fieldValue;
71+
try {
72+
fieldValue = secretJson.getString(fieldname);
73+
} catch (JSONException | NullPointerException ex) {
74+
throw new CredentialsUnavailableException("secret", Messages.wrongJsonError(getId(), fieldname));
75+
}
76+
return fieldValue;
77+
}
78+
79+
/**
80+
* Reads the secret JSON and returns the field requested.
81+
*
82+
* @param secretJson The {@link JSONObject} we're going to look in, which likely
83+
* came from {@link #getSecretJson()}.
84+
* @param fieldname The (top-level) field that we want (which must be a
85+
* {@link String}).
86+
* @return The contents of that JSON field.
87+
*/
88+
protected String getOptionalField(@NonNull JSONObject secretJson, @NonNull String fieldname) {
89+
final String fieldValue = secretJson.optString(fieldname);
90+
return fieldValue;
91+
}
92+
93+
/**
94+
* Reads the secret JSON and returns it.
95+
*
96+
* @return The contents of that JSON field.
97+
* @throws CredentialsUnavailableException if there is no JSON, or it is not
98+
* valid JSON.
99+
*/
100+
@NonNull
101+
protected JSONObject getSecretJson() {
102+
final Secret secret = json.get();
103+
final String rawSecretJson = secret == null ? "" : secret.getPlainText();
104+
if (rawSecretJson.isEmpty()) {
105+
throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId()));
106+
}
107+
final JSON parsedJson;
108+
try {
109+
parsedJson = JSONSerializer.toJSON(rawSecretJson);
110+
} catch (JSONException ex) {
111+
throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId()));
112+
}
113+
// if we got this far then we have some syntactically-valid JSON
114+
// ... but it might not be a JSON object containing the field we wanted.
115+
final JSONObject jsonObject;
116+
try {
117+
jsonObject = (JSONObject) parsedJson;
118+
} catch (ClassCastException ex) {
119+
throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId()));
120+
}
121+
return jsonObject;
122+
}
123+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
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;
18+
import io.jenkins.plugins.credentials.secretsmanager.factory.username_password.AwsJsonUsernamePasswordCredentials;
1719
import io.jenkins.plugins.credentials.secretsmanager.factory.username_password.AwsUsernamePasswordCredentials;
1820

1921
import java.util.Map;
@@ -46,8 +48,12 @@ public static Optional<StandardCredentials> create(String arn, String name, Stri
4648
return Optional.of(new AwsStringCredentials(name, description, new SecretSupplier(client, arn)));
4749
case Type.usernamePassword:
4850
return Optional.of(new AwsUsernamePasswordCredentials(name, description, new SecretSupplier(client, arn), username));
51+
case Type.jsonUsernamePassword:
52+
return Optional.of(new AwsJsonUsernamePasswordCredentials(name, description, new SecretSupplier(client, arn)));
4953
case Type.sshUserPrivateKey:
5054
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)));
5157
case Type.certificate:
5258
return Optional.of(new AwsCertificateCredentials(name, description, new SecretBytesSupplier(client, arn)));
5359
case Type.file:

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ public abstract class Type {
77
public static final String certificate = "certificate";
88
public static final String file = "file";
99
public static final String usernamePassword = "usernamePassword";
10+
public static final String jsonUsernamePassword = "jsonUsernamePassword";
1011
public static final String sshUserPrivateKey = "sshUserPrivateKey";
12+
public static final String jsonSshUserPrivateKey = "jsonSshUserPrivateKey";
1113
public static final String string = "string";
1214

1315
private Type() {
14-
1516
}
1617
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
/**
23+
* Similar to {@link AwsSshUserPrivateKey} but expects the AWS secret data to be
24+
* JSON containing two or three fields, {@value #JSON_FIELDNAME_FOR_USERNAME},
25+
* {@value #JSON_FIELDNAME_FOR_PRIVATE_KEY} and optionally a
26+
* {@value #JSON_FIELDNAME_FOR_PASSPHRASE} that provide the username, key and
27+
* (optional) passphrase. The secret JSON may contain other fields too, but
28+
* we'll ignore them.
29+
*/
30+
public class AwsJsonSshUserPrivateKey extends BaseAwsJsonCredentials implements SSHUserPrivateKey {
31+
/**
32+
* Name of the JSON field that we expect to be present and to contain the
33+
* credential's username.
34+
*/
35+
@Restricted(NoExternalUse.class)
36+
public static final String JSON_FIELDNAME_FOR_USERNAME = "username";
37+
/**
38+
* Name of the JSON field that we expect to be present and to contain the
39+
* credential's private key.
40+
*/
41+
@Restricted(NoExternalUse.class)
42+
public static final String JSON_FIELDNAME_FOR_PRIVATE_KEY = "privatekey";
43+
/**
44+
* Name of the JSON field that we look for and, if present, expect it to contain
45+
* the credential's password. If it isn't present then we assume no passphrase.
46+
*/
47+
@Restricted(NoExternalUse.class)
48+
public static final String JSON_FIELDNAME_FOR_PASSPHRASE = "passphrase";
49+
50+
/**
51+
* Constructs a new instance.
52+
*
53+
* @param id The value for {@link #getId()}.
54+
* @param description The value for {@link #getDescription()}.
55+
* @param json Supplies JSON containing a
56+
* {@value #JSON_FIELDNAME_FOR_USERNAME} field, a
57+
* {@value #JSON_FIELDNAME_FOR_PRIVATE_KEY} field and
58+
* optionally a {@value #JSON_FIELDNAME_FOR_PASSPHRASE}
59+
* field.
60+
*/
61+
public AwsJsonSshUserPrivateKey(String id, String description, Supplier<Secret> json) {
62+
super(id, description, json);
63+
}
64+
65+
/**
66+
* Constructs a snapshot of an existing instance.
67+
*
68+
* @param toBeSnapshotted The instance that contains the live data to be
69+
* snapshotted.
70+
*/
71+
@Restricted(NoExternalUse.class)
72+
AwsJsonSshUserPrivateKey(AwsJsonSshUserPrivateKey toBeSnapshotted) {
73+
super(toBeSnapshotted);
74+
}
75+
76+
@NonNull
77+
@Deprecated
78+
@Override
79+
public String getPrivateKey() {
80+
return getMandatoryField(getSecretJson(), JSON_FIELDNAME_FOR_PRIVATE_KEY);
81+
}
82+
83+
@Override
84+
public Secret getPassphrase() {
85+
return Secret.fromString(getOptionalField(getSecretJson(), JSON_FIELDNAME_FOR_PASSPHRASE));
86+
}
87+
88+
@NonNull
89+
@Override
90+
public List<String> getPrivateKeys() {
91+
return Collections.singletonList(getPrivateKey());
92+
}
93+
94+
@NonNull
95+
@Override
96+
public String getUsername() {
97+
return getMandatoryField(getSecretJson(), JSON_FIELDNAME_FOR_USERNAME);
98+
}
99+
100+
@Override
101+
public boolean isUsernameSecret() {
102+
return true;
103+
}
104+
105+
@Extension
106+
@SuppressWarnings("unused")
107+
public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {
108+
@Override
109+
@Nonnull
110+
public String getDisplayName() {
111+
return Messages.sshUserPrivateKey();
112+
}
113+
114+
@Override
115+
public String getIconClassName() {
116+
return "icon-ssh-credentials-ssh-key";
117+
}
118+
119+
@Override
120+
public boolean isApplicable(CredentialsProvider provider) {
121+
return provider instanceof AwsCredentialsProvider;
122+
}
123+
}
124+
}
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+
}

0 commit comments

Comments
 (0)