Skip to content

Commit 9666c6a

Browse files
committed
Base class for JSON-format secret data
1 parent d7e1eb8 commit 9666c6a

File tree

3 files changed

+298
-0
lines changed

3 files changed

+298
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 hudson.util.Secret;
12+
import io.jenkins.plugins.credentials.secretsmanager.Messages;
13+
import net.sf.json.JSON;
14+
import net.sf.json.JSONException;
15+
import net.sf.json.JSONObject;
16+
import net.sf.json.JSONSerializer;
17+
18+
/**
19+
* Base class for credentials where the AWS secret data is JSON format instead
20+
* of raw data.
21+
*/
22+
@Restricted(NoExternalUse.class)
23+
public abstract class BaseAwsJsonCredentials extends BaseStandardCredentials {
24+
/** How to access the JSON */
25+
private final Supplier<Secret> supplierOfSecretData;
26+
27+
protected BaseAwsJsonCredentials(String id, String description, Supplier<Secret> secretJsonSupplier) {
28+
super(id, description);
29+
this.supplierOfSecretData = secretJsonSupplier;
30+
}
31+
32+
/**
33+
* Reads the secret JSON and returns the field requested.
34+
*
35+
* @param fieldname The (top-level) field that we want (which must be a
36+
* {@link String}).
37+
* @return The contents of that JSON field.
38+
* @throws CredentialsUnavailableException if there is no JSON, or it is not
39+
* valid JSON, or it is missing the
40+
* field, or the field is not a
41+
* {@link String}.
42+
*/
43+
protected String getFieldFromSecretJson(String fieldname) {
44+
// Note:
45+
// We MUST NOT tell anyone what the JSON is, or give any hints as to its
46+
// contents, as that could then leak sensitive data so we have to suppress the
47+
// informative exception(s) and just tell the user that it didn't work.
48+
final Secret secret = supplierOfSecretData.get();
49+
final String rawSecretJson = secret == null ? "" : secret.getPlainText();
50+
if (rawSecretJson.isEmpty()) {
51+
throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId()));
52+
}
53+
final JSON parsedJson;
54+
try {
55+
parsedJson = JSONSerializer.toJSON(rawSecretJson);
56+
} catch (JSONException ex) {
57+
throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId()));
58+
}
59+
// if we got this far then we have some syntactically-valid JSON
60+
// ... but it might not be a JSON object containing the field we wanted.
61+
final String fieldValue;
62+
try {
63+
final JSONObject jsonObject = (JSONObject) parsedJson;
64+
fieldValue = jsonObject.getString(fieldname);
65+
} catch (JSONException | ClassCastException | NullPointerException ex) {
66+
throw new CredentialsUnavailableException("secret", Messages.wrongJsonError(getId(), fieldname));
67+
}
68+
return fieldValue;
69+
}
70+
}

src/main/resources/io/jenkins/plugins/credentials/secretsmanager/Messages.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ couldNotRetrieveCredentialError = Could not retrieve the credential {0} from AWS
1919
noUsernameError = Credential did not have a username
2020
noPrivateKeyError = Credential did not contain a valid private key in PEM format
2121
noCertificateError = Credential did not contain a valid certificate bundle in PKCS#12 format
22+
noValidJsonError = Credential {0} did not contain valid JSON
23+
wrongJsonError = Credential {0} did not contain a JSON object containing a field called {1}
2224
emptySecretError = AWS Secrets Manager entry {0} contained neither a secretString nor a secretBinary value
2325
roles = Roles
2426
role = Role
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package io.jenkins.plugins.credentials.secretsmanager.factory;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.lang.reflect.Method;
6+
import java.util.function.Supplier;
7+
8+
import org.junit.Test;
9+
10+
import com.cloudbees.plugins.credentials.CredentialsDescriptor;
11+
import com.cloudbees.plugins.credentials.CredentialsProvider;
12+
import com.cloudbees.plugins.credentials.CredentialsUnavailableException;
13+
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
14+
15+
import hudson.util.Secret;
16+
import io.jenkins.plugins.credentials.secretsmanager.AwsCredentialsProvider;
17+
import io.jenkins.plugins.credentials.secretsmanager.Messages;
18+
19+
public class BaseAwsJsonCredentialsTest {
20+
@Test
21+
public void getFieldFromSecretJsonGivenValidJsonThenReturnsValue() {
22+
// Given
23+
final String expected = "myFieldValue";
24+
final String fieldName = "myFieldName";
25+
final String json = mkJson(fieldName, expected);
26+
final Secret secretJson = Secret.fromString(json);
27+
final Supplier<Secret> stubSupplier = new StubSupplier<>(secretJson);
28+
final TestClass instance = new TestClass(stubSupplier);
29+
30+
// When
31+
final String actual = instance.getFieldFromSecretJson(fieldName);
32+
33+
// Then
34+
assertThat(actual).isEqualTo(expected);
35+
}
36+
37+
@Test
38+
public void getFieldFromSecretJsonGivenValidJsonWithUnwantedDataThenReturnsValue() {
39+
// Given
40+
final String expected = "myFieldValue";
41+
final String fieldName = "myFieldName";
42+
final String json = mkJson("someOtherField", "someOtherValue", fieldName, expected);
43+
final Secret secretJson = Secret.fromString(json);
44+
final Supplier<Secret> stubSupplier = new StubSupplier<>(secretJson);
45+
final TestClass instance = new TestClass(stubSupplier);
46+
47+
// When
48+
final String actual = instance.getFieldFromSecretJson(fieldName);
49+
50+
// Then
51+
assertThat(actual).isEqualTo(expected);
52+
}
53+
54+
@Test
55+
public void getFieldFromSecretJsonGivenMissingJsonThenReturnsThrows() {
56+
// Given
57+
final Secret secretJson = null;
58+
final Supplier<Secret> stubSupplier = new StubSupplier<>(secretJson);
59+
final TestClass instance = new TestClass(stubSupplier);
60+
final String missingFieldName = "someFieldName";
61+
62+
// When
63+
CredentialsUnavailableException actual = null;
64+
try {
65+
instance.getFieldFromSecretJson(missingFieldName);
66+
} catch (CredentialsUnavailableException ex) {
67+
actual = ex;
68+
}
69+
70+
// Then
71+
assertThat(actual).isNotNull();
72+
assertThat(actual.getProperty()).isEqualTo("secret");
73+
assertThat(actual.getMessage()).contains(Messages.noValidJsonError(instance.getId()));
74+
}
75+
76+
@Test
77+
public void getFieldFromSecretJsonGivenEmptyJsonThenReturnsThrows() {
78+
// Given
79+
final String json = "";
80+
final Secret secretJson = Secret.fromString(json);
81+
final Supplier<Secret> stubSupplier = new StubSupplier<>(secretJson);
82+
final TestClass instance = new TestClass(stubSupplier);
83+
final String missingFieldName = "someFieldName";
84+
85+
// When
86+
CredentialsUnavailableException actual = null;
87+
try {
88+
instance.getFieldFromSecretJson(missingFieldName);
89+
} catch (CredentialsUnavailableException ex) {
90+
actual = ex;
91+
}
92+
93+
// Then
94+
assertThat(actual).isNotNull();
95+
assertThat(actual.getProperty()).isEqualTo("secret");
96+
assertThat(actual.getMessage()).contains(Messages.noValidJsonError(instance.getId()));
97+
}
98+
99+
@Test
100+
public void getFieldFromSecretJsonGivenInvalidJsonThenReturnsThrows() {
101+
final String json = "1234 is not valid JSON";
102+
final Secret secretJson = Secret.fromString(json);
103+
final Supplier<Secret> stubSupplier = new StubSupplier<>(secretJson);
104+
final TestClass instance = new TestClass(stubSupplier);
105+
final String missingFieldName = "fieldName";
106+
107+
// When
108+
CredentialsUnavailableException actual = null;
109+
try {
110+
instance.getFieldFromSecretJson(missingFieldName);
111+
} catch (CredentialsUnavailableException ex) {
112+
actual = ex;
113+
}
114+
115+
// Then
116+
assertThat(actual).isNotNull();
117+
assertThat(actual.getProperty()).isEqualTo("secret");
118+
assertThat(actual.getMessage()).contains(instance.getId());
119+
assertThat(actual.getMessage()).contains(Messages.noValidJsonError(instance.getId()));
120+
assertThat(actual.getMessage()).doesNotContain("1234");
121+
}
122+
123+
@Test
124+
public void getFieldFromSecretJsonGivenValidJsonMissingDesiredFieldThenReturnsThrows() {
125+
// Given
126+
final String unexpectedFieldName = "potentiallySecretFieldName";
127+
final String unexpectedValue = "potentiallySecretValue";
128+
final String json = mkJson(unexpectedFieldName, unexpectedValue, unexpectedFieldName + "2", unexpectedValue);
129+
final Secret secretJson = Secret.fromString(json);
130+
final Supplier<Secret> stubSupplier = new StubSupplier<>(secretJson);
131+
final TestClass instance = new TestClass(stubSupplier);
132+
final String missingFieldName = "someOtherFieldName";
133+
134+
// When
135+
CredentialsUnavailableException actual = null;
136+
try {
137+
instance.getFieldFromSecretJson(missingFieldName);
138+
} catch (CredentialsUnavailableException ex) {
139+
actual = ex;
140+
}
141+
142+
// Then
143+
assertThat(actual).isNotNull();
144+
assertThat(actual.getProperty()).isEqualTo("secret");
145+
assertThat(actual.getMessage()).contains(instance.getId());
146+
assertThat(actual.getMessage()).contains(missingFieldName);
147+
assertThat(actual.getMessage()).contains(Messages.wrongJsonError(instance.getId(), missingFieldName));
148+
assertThat(actual.getMessage()).doesNotContain(unexpectedFieldName);
149+
assertThat(actual.getMessage()).doesNotContain(unexpectedValue);
150+
}
151+
152+
public static void assertThatJsonCredentialsDescriptorIsTheSameAsTheDescriptorForNonJsonCredentials(
153+
CredentialsDescriptor instanceUnderTest, CredentialsDescriptor nonJsonEquivalent) {
154+
// Given
155+
final CredentialsProvider awsCredProvider = new AwsCredentialsProvider();
156+
final CredentialsProvider otherCredProvider = new SystemCredentialsProvider.ProviderImpl();
157+
final String expectedDisplayName = nonJsonEquivalent.getDisplayName();
158+
final String expectedIconClassName = nonJsonEquivalent.getIconClassName();
159+
final boolean expectedApplicableToAws = nonJsonEquivalent.isApplicable(awsCredProvider);
160+
final boolean expectedApplicableToOther = nonJsonEquivalent.isApplicable(otherCredProvider);
161+
final String[] expectedDeclaredMethods = toClassAgnosticMethodDescription(
162+
nonJsonEquivalent.getClass().getDeclaredMethods());
163+
164+
// When
165+
final String actualDisplayName = instanceUnderTest.getDisplayName();
166+
final String actualIconClassName = instanceUnderTest.getIconClassName();
167+
final boolean actualApplicableToAws = instanceUnderTest.isApplicable(awsCredProvider);
168+
final boolean actualApplicableToOther = instanceUnderTest.isApplicable(otherCredProvider);
169+
final String[] actualDeclaredMethods = toClassAgnosticMethodDescription(
170+
instanceUnderTest.getClass().getDeclaredMethods());
171+
172+
// Then
173+
assertThat(actualDisplayName).isEqualTo(expectedDisplayName);
174+
assertThat(actualIconClassName).isEqualTo(expectedIconClassName);
175+
assertThat(actualApplicableToAws).isEqualTo(expectedApplicableToAws);
176+
assertThat(actualApplicableToOther).isEqualTo(expectedApplicableToOther);
177+
// Check no other unexpected behavior was added to one and not the other
178+
assertThat(actualDeclaredMethods).containsExactly(expectedDeclaredMethods);
179+
}
180+
181+
public static String mkJson(String fieldName1, String fieldValue1) {
182+
return "{ " + fieldName1 + ": '" + fieldValue1 + "' }";
183+
}
184+
185+
public static String mkJson(String fieldName1, String fieldValue1, String fieldName2, String fieldValue2) {
186+
return "{ " + fieldName1 + ": '" + fieldValue1 + "', " + fieldName2 + ": '" + fieldValue2 + "' }";
187+
}
188+
189+
private static String[] toClassAgnosticMethodDescription(Method[] m) {
190+
final String[] results = new String[m.length];
191+
for (int i = 0; i < m.length; i++) {
192+
final Class<?> declaringClass = m[i].getDeclaringClass();
193+
final String original = m[i].toGenericString();
194+
final String definingClass = declaringClass.getName();
195+
final String result = original.replace(definingClass + ".", "");
196+
results[i] = result;
197+
}
198+
return results;
199+
}
200+
201+
private static class TestClass extends BaseAwsJsonCredentials {
202+
protected TestClass(Supplier<Secret> usernameAndPasswordJson) {
203+
super("TestId", "TestDescription", usernameAndPasswordJson);
204+
}
205+
206+
// expose so we can test it
207+
public String getFieldFromSecretJson(String fieldname) {
208+
return super.getFieldFromSecretJson(fieldname);
209+
}
210+
}
211+
212+
// if we had a mocking framework like Mockito on the classpath then we wouldn't
213+
// need this.
214+
public static class StubSupplier<T> implements Supplier<T> {
215+
private final T supplied;
216+
217+
public StubSupplier(T supplied) {
218+
this.supplied = supplied;
219+
}
220+
221+
@Override
222+
public T get() {
223+
return supplied;
224+
}
225+
}
226+
}

0 commit comments

Comments
 (0)