diff --git a/docs/README.md b/docs/README.md index f9e42f4c..6fa2cbaf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -167,6 +167,24 @@ node { } ``` +### Username with Password (JSON format) + +A *username* and *password* pair. + +- Value: Valid JSON describing an object with a `username` field and and `password` field. +- Tags: + - `jenkins:credentials:type` = `jsonUsernamePassword` + +#### Example + +AWS CLI: + +```bash +aws secretsmanager create-secret --name 'artifactory' --secret-string '{ username: "joe", password: "supersecret" }' --tags 'Key=jenkins:credentials:type,Value=jsonUsernamePassword' --description 'Acme Corp Artifactory login' +``` + +Declarative and Scripted Pipeline behavior is (exactly) the same as non-JSON-format Username and Password. + ### SSH User Private Key An SSH *private key*, with a *username*. @@ -218,6 +236,28 @@ node { } ``` +### SSH User Private Key (JSON format) + +An SSH *private key*, with a *username* and optional *passphrase* for the private key. + +- Value: Valid JSON describing an object with a `username` field, `privatekey` field and optionally a `passphrase` field. +- Tags: + - `jenkins:credentials:type` = `jsonSshUserPrivateKey` + +**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. + +#### Example + +AWS CLI: + +```bash +ssh-keygen -t rsa -b 4096 -C 'acme@example.com' -f id_rsa -N mySecretPassPhrase +jq -n --arg key "$(cat id_rsa)" '{ username: "joe", privatekey: $key, passphrase: "mySecretPassPhrase" }' >json +aws secretsmanager create-secret --name 'ssh-key' --secret-string 'file://json' --tags 'Key=jenkins:credentials:type,Value=jsonSshUserPrivateKey' --description 'Acme Corp SSH key' +``` + +Declarative and Scripted Pipeline behavior is (exactly) the same as non-JSON-format SSH User Private Key. + ### Certificate A client certificate *keystore* in PKCS#12 format, encrypted with a zero-length password. diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/BaseAwsJsonCredentials.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/BaseAwsJsonCredentials.java new file mode 100644 index 00000000..ce1a6af0 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/BaseAwsJsonCredentials.java @@ -0,0 +1,123 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory; + +import java.util.function.Supplier; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import com.cloudbees.plugins.credentials.CredentialsUnavailableException; +import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.util.Secret; +import io.jenkins.plugins.credentials.secretsmanager.Messages; +import net.sf.json.JSON; +import net.sf.json.JSONException; +import net.sf.json.JSONObject; +import net.sf.json.JSONSerializer; + +/** + * Base class for credentials where the AWS secret data is JSON format instead + * of raw data. + */ +@Restricted(NoExternalUse.class) +public abstract class BaseAwsJsonCredentials extends BaseStandardCredentials { + /** How to access the JSON */ + private final Supplier json; + + /** + * Constructs a new instance with new data. + * + * @param id The value for {@link #getId()}. + * @param description The value for {@link #getDescription()}. + * @param json Supplies the data that {@link #getSecretJson()} will + * decode. + */ + protected BaseAwsJsonCredentials(String id, String description, Supplier json) { + super(id, description); + this.json = json; + } + + /** + * Constructs an instance that is an unchanging snapshot of another instance. + * + * @param toSnapshot The instance to be copied. + */ + protected BaseAwsJsonCredentials(BaseAwsJsonCredentials toSnapshot) { + super(toSnapshot.getId(), toSnapshot.getDescription()); + final Secret secretDataToSnapshot = toSnapshot.json.get(); + this.json = new Snapshot(secretDataToSnapshot); + } + + // Note: + // We MUST NOT tell anyone what the JSON is, or give any hints as to its + // contents, as that could then leak sensitive data so, if anything goes wrong, + // we have to suppress the informative exception(s) and just tell the user that + // it didn't work. + + /** + * Reads the secret JSON and returns the field requested. + * + * @param secretJson The {@link JSONObject} we're going to look in, which likely + * came from {@link #getSecretJson()}. + * @param fieldname The (top-level) field that we want (which must be a + * {@link String}). + * @return The contents of that JSON field. + * @throws CredentialsUnavailableException if the JSON is missing the field, or + * the field is not a {@link String}. + */ + protected String getMandatoryField(@NonNull JSONObject secretJson, @NonNull String fieldname) { + final String fieldValue; + try { + fieldValue = secretJson.getString(fieldname); + } catch (JSONException | NullPointerException ex) { + throw new CredentialsUnavailableException("secret", Messages.wrongJsonError(getId(), fieldname)); + } + return fieldValue; + } + + /** + * Reads the secret JSON and returns the field requested. + * + * @param secretJson The {@link JSONObject} we're going to look in, which likely + * came from {@link #getSecretJson()}. + * @param fieldname The (top-level) field that we want (which must be a + * {@link String}). + * @return The contents of that JSON field. + */ + protected String getOptionalField(@NonNull JSONObject secretJson, @NonNull String fieldname) { + final String fieldValue = secretJson.optString(fieldname); + return fieldValue; + } + + /** + * Reads the secret JSON and returns it. + * + * @return The contents of that JSON field. + * @throws CredentialsUnavailableException if there is no JSON, or it is not + * valid JSON. + */ + @NonNull + protected JSONObject getSecretJson() { + final Secret secret = json.get(); + final String rawSecretJson = secret == null ? "" : secret.getPlainText(); + if (rawSecretJson.isEmpty()) { + throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId())); + } + final JSON parsedJson; + try { + parsedJson = JSONSerializer.toJSON(rawSecretJson); + } catch (JSONException ex) { + throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId())); + } + // if we got this far then we have some syntactically-valid JSON + // ... but it might not be a JSON object containing the field we wanted. + final JSONObject jsonObject; + try { + jsonObject = (JSONObject) parsedJson; + } catch (ClassCastException ex) { + throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId())); + } + return jsonObject; + } +} diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java index 7dd01198..547e3687 100644 --- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/CredentialsFactory.java @@ -12,8 +12,10 @@ import io.jenkins.plugins.credentials.secretsmanager.Messages; import io.jenkins.plugins.credentials.secretsmanager.factory.certificate.AwsCertificateCredentials; import io.jenkins.plugins.credentials.secretsmanager.factory.file.AwsFileCredentials; +import io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key.AwsJsonSshUserPrivateKey; import io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key.AwsSshUserPrivateKey; import io.jenkins.plugins.credentials.secretsmanager.factory.string.AwsStringCredentials; +import io.jenkins.plugins.credentials.secretsmanager.factory.username_password.AwsJsonUsernamePasswordCredentials; import io.jenkins.plugins.credentials.secretsmanager.factory.username_password.AwsUsernamePasswordCredentials; import java.util.Map; @@ -46,8 +48,12 @@ public static Optional create(String arn, String name, Stri return Optional.of(new AwsStringCredentials(name, description, new SecretSupplier(client, arn))); case Type.usernamePassword: return Optional.of(new AwsUsernamePasswordCredentials(name, description, new SecretSupplier(client, arn), username)); + case Type.jsonUsernamePassword: + return Optional.of(new AwsJsonUsernamePasswordCredentials(name, description, new SecretSupplier(client, arn))); case Type.sshUserPrivateKey: return Optional.of(new AwsSshUserPrivateKey(name, description, new StringSupplier(client, arn), username)); + case Type.jsonSshUserPrivateKey: + return Optional.of(new AwsJsonSshUserPrivateKey(name, description, new SecretSupplier(client, arn))); case Type.certificate: return Optional.of(new AwsCertificateCredentials(name, description, new SecretBytesSupplier(client, arn))); case Type.file: diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Type.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Type.java index 75322c32..662d6e12 100644 --- a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Type.java +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/Type.java @@ -7,10 +7,11 @@ public abstract class Type { public static final String certificate = "certificate"; public static final String file = "file"; public static final String usernamePassword = "usernamePassword"; + public static final String jsonUsernamePassword = "jsonUsernamePassword"; public static final String sshUserPrivateKey = "sshUserPrivateKey"; + public static final String jsonSshUserPrivateKey = "jsonSshUserPrivateKey"; public static final String string = "string"; private Type() { - } } diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKey.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKey.java new file mode 100644 index 00000000..81270084 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKey.java @@ -0,0 +1,124 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.plugins.credentials.CredentialsProvider; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.util.Secret; +import io.jenkins.plugins.credentials.secretsmanager.AwsCredentialsProvider; +import io.jenkins.plugins.credentials.secretsmanager.Messages; +import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentials; + +/** + * Similar to {@link AwsSshUserPrivateKey} but expects the AWS secret data to be + * JSON containing two or three fields, {@value #JSON_FIELDNAME_FOR_USERNAME}, + * {@value #JSON_FIELDNAME_FOR_PRIVATE_KEY} and optionally a + * {@value #JSON_FIELDNAME_FOR_PASSPHRASE} that provide the username, key and + * (optional) passphrase. The secret JSON may contain other fields too, but + * we'll ignore them. + */ +public class AwsJsonSshUserPrivateKey extends BaseAwsJsonCredentials implements SSHUserPrivateKey { + /** + * Name of the JSON field that we expect to be present and to contain the + * credential's username. + */ + @Restricted(NoExternalUse.class) + public static final String JSON_FIELDNAME_FOR_USERNAME = "username"; + /** + * Name of the JSON field that we expect to be present and to contain the + * credential's private key. + */ + @Restricted(NoExternalUse.class) + public static final String JSON_FIELDNAME_FOR_PRIVATE_KEY = "privatekey"; + /** + * Name of the JSON field that we look for and, if present, expect it to contain + * the credential's password. If it isn't present then we assume no passphrase. + */ + @Restricted(NoExternalUse.class) + public static final String JSON_FIELDNAME_FOR_PASSPHRASE = "passphrase"; + + /** + * Constructs a new instance. + * + * @param id The value for {@link #getId()}. + * @param description The value for {@link #getDescription()}. + * @param json Supplies JSON containing a + * {@value #JSON_FIELDNAME_FOR_USERNAME} field, a + * {@value #JSON_FIELDNAME_FOR_PRIVATE_KEY} field and + * optionally a {@value #JSON_FIELDNAME_FOR_PASSPHRASE} + * field. + */ + public AwsJsonSshUserPrivateKey(String id, String description, Supplier json) { + super(id, description, json); + } + + /** + * Constructs a snapshot of an existing instance. + * + * @param toBeSnapshotted The instance that contains the live data to be + * snapshotted. + */ + @Restricted(NoExternalUse.class) + AwsJsonSshUserPrivateKey(AwsJsonSshUserPrivateKey toBeSnapshotted) { + super(toBeSnapshotted); + } + + @NonNull + @Deprecated + @Override + public String getPrivateKey() { + return getMandatoryField(getSecretJson(), JSON_FIELDNAME_FOR_PRIVATE_KEY); + } + + @Override + public Secret getPassphrase() { + return Secret.fromString(getOptionalField(getSecretJson(), JSON_FIELDNAME_FOR_PASSPHRASE)); + } + + @NonNull + @Override + public List getPrivateKeys() { + return Collections.singletonList(getPrivateKey()); + } + + @NonNull + @Override + public String getUsername() { + return getMandatoryField(getSecretJson(), JSON_FIELDNAME_FOR_USERNAME); + } + + @Override + public boolean isUsernameSecret() { + return true; + } + + @Extension + @SuppressWarnings("unused") + public static class DescriptorImpl extends BaseStandardCredentialsDescriptor { + @Override + @Nonnull + public String getDisplayName() { + return Messages.sshUserPrivateKey(); + } + + @Override + public String getIconClassName() { + return "icon-ssh-credentials-ssh-key"; + } + + @Override + public boolean isApplicable(CredentialsProvider provider) { + return provider instanceof AwsCredentialsProvider; + } + } +} diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKeySnapshotTaker.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKeySnapshotTaker.java new file mode 100644 index 00000000..d4ef6409 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKeySnapshotTaker.java @@ -0,0 +1,19 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key; + +import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker; +import hudson.Extension; +import io.jenkins.plugins.credentials.secretsmanager.factory.Snapshot; + +@Extension +@SuppressWarnings("unused") +public class AwsJsonSshUserPrivateKeySnapshotTaker extends CredentialsSnapshotTaker { + @Override + public Class type() { + return AwsJsonSshUserPrivateKey.class; + } + + @Override + public AwsJsonSshUserPrivateKey snapshot(AwsJsonSshUserPrivateKey credential) { + return new AwsJsonSshUserPrivateKey(credential); + } +} diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentials.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentials.java new file mode 100644 index 00000000..a577a9b7 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentials.java @@ -0,0 +1,102 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory.username_password; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.util.Secret; +import io.jenkins.plugins.credentials.secretsmanager.AwsCredentialsProvider; +import io.jenkins.plugins.credentials.secretsmanager.Messages; +import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentials; + +/** + * Similar to {@link AwsUsernamePasswordCredentials} but expects the AWS secret + * data to be JSON containing two fields, {@value #JSON_FIELDNAME_FOR_USERNAME} + * and {@value #JSON_FIELDNAME_FOR_PASSWORD} that provide both the username and + * password. The secret JSON may contain other fields too, but we'll ignore + * them. + */ +public class AwsJsonUsernamePasswordCredentials extends BaseAwsJsonCredentials + implements StandardUsernamePasswordCredentials { + /** + * Name of the JSON field that we expect to be present and to contain the + * credential's username. + */ + @Restricted(NoExternalUse.class) + public static final String JSON_FIELDNAME_FOR_USERNAME = "username"; + /** + * Name of the JSON field that we expect to be present and to contain the + * credential's password. + */ + @Restricted(NoExternalUse.class) + public static final String JSON_FIELDNAME_FOR_PASSWORD = "password"; + + /** + * Constructs a new instance. + * + * @param id The value for {@link #getId()}. + * @param description The value for {@link #getDescription()}. + * @param json Supplies JSON containing a + * {@value #JSON_FIELDNAME_FOR_USERNAME} field and a + * {@value #JSON_FIELDNAME_FOR_PASSWORD} field. + */ + public AwsJsonUsernamePasswordCredentials(String id, String description, Supplier json) { + super(id, description, json); + } + + /** + * Constructs a snapshot of an existing instance. + * + * @param toBeSnapshotted The instance that contains the live data to be + * snapshotted. + */ + @Restricted(NoExternalUse.class) + AwsJsonUsernamePasswordCredentials(AwsJsonUsernamePasswordCredentials toBeSnapshotted) { + super(toBeSnapshotted); + } + + @NonNull + @Override + public Secret getPassword() { + return Secret.fromString(getMandatoryField(getSecretJson(), JSON_FIELDNAME_FOR_PASSWORD)); + } + + @NonNull + @Override + public String getUsername() { + return getMandatoryField(getSecretJson(), JSON_FIELDNAME_FOR_USERNAME); + } + + @Override + public boolean isUsernameSecret() { + return true; + } + + @Extension + @SuppressWarnings("unused") + public static class DescriptorImpl extends BaseStandardCredentialsDescriptor { + @Override + @Nonnull + public String getDisplayName() { + return Messages.usernamePassword(); + } + + @Override + public String getIconClassName() { + return "icon-credentials-userpass"; + } + + @Override + public boolean isApplicable(CredentialsProvider provider) { + return provider instanceof AwsCredentialsProvider; + } + } +} diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentialsSnapshotTaker.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentialsSnapshotTaker.java new file mode 100644 index 00000000..3f90065b --- /dev/null +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentialsSnapshotTaker.java @@ -0,0 +1,20 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory.username_password; + +import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker; +import hudson.Extension; +import hudson.util.Secret; +import io.jenkins.plugins.credentials.secretsmanager.factory.Snapshot; + +@Extension +@SuppressWarnings("unused") +public class AwsJsonUsernamePasswordCredentialsSnapshotTaker extends CredentialsSnapshotTaker { + @Override + public Class type() { + return AwsJsonUsernamePasswordCredentials.class; + } + + @Override + public AwsJsonUsernamePasswordCredentials snapshot(AwsJsonUsernamePasswordCredentials credential) { + return new AwsJsonUsernamePasswordCredentials(credential); + } +} diff --git a/src/main/resources/io/jenkins/plugins/credentials/secretsmanager/Messages.properties b/src/main/resources/io/jenkins/plugins/credentials/secretsmanager/Messages.properties index 6afde777..31eda047 100644 --- a/src/main/resources/io/jenkins/plugins/credentials/secretsmanager/Messages.properties +++ b/src/main/resources/io/jenkins/plugins/credentials/secretsmanager/Messages.properties @@ -19,6 +19,8 @@ couldNotRetrieveCredentialError = Could not retrieve the credential {0} from AWS noUsernameError = Credential did not have a username noPrivateKeyError = Credential did not contain a valid private key in PEM format noCertificateError = Credential did not contain a valid certificate bundle in PKCS#12 format +noValidJsonError = Credential {0} did not contain valid JSON +wrongJsonError = Credential {0} did not contain a JSON object containing a field called {1} emptySecretError = AWS Secrets Manager entry {0} contained neither a secretString nor a secretBinary value roles = Roles role = Role diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/BaseAwsJsonCredentialsSnapshotTakerTest.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/BaseAwsJsonCredentialsSnapshotTakerTest.java new file mode 100644 index 00000000..ab57b84f --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/BaseAwsJsonCredentialsSnapshotTakerTest.java @@ -0,0 +1,181 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.Before; +import org.junit.Test; + +import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker; + +import hudson.Extension; + +public abstract class BaseAwsJsonCredentialsSnapshotTakerTest> { + protected final Class classUnderTest; + protected final Class credentialBeingSnapshotted; + + protected BaseAwsJsonCredentialsSnapshotTakerTest(Class classUnderTest, Class credentialBeingSnapshotted) { + this.classUnderTest = classUnderTest; + this.credentialBeingSnapshotted = credentialBeingSnapshotted; + } + + protected abstract C makeCredential(); + + private LimitedUseSupplier limitedUseSupplier; + + @Before + public void beforeTest() { + limitedUseSupplier = null; + } + + protected Supplier mkSupplier(T supplied) { + final LimitedUseSupplier result = new LimitedUseSupplier<>(supplied); + limitedUseSupplier = result; + return result; + } + + private ST makeInstanceOrThrow() throws NoSuchMethodException, SecurityException, InstantiationException, + IllegalAccessException, IllegalArgumentException, InvocationTargetException { + final Constructor noArgsConstructor = classUnderTest.getConstructor(); + return noArgsConstructor.newInstance(); + } + + @Test + public void hasNoArgsConstructor() { + // Given + + // When + Exception actual = null; + try { + makeInstanceOrThrow(); + } catch (Exception ex) { + actual = ex; + } + + // Then + assertThat(actual).isNull(); + } + + @Test + public void hasExtensionAnnotation() { + // Given + + // When + final Extension actual = classUnderTest.getAnnotation(Extension.class); + + // Then + assertThat(actual).isNotNull(); + } + + @Test + public void typeTest() throws Exception { + // Given + final Class expected = credentialBeingSnapshotted; + final ST instance = makeInstanceOrThrow(); + + // When + final Class actual = instance.type(); + + // Then + assertThat(actual).isEqualTo(expected); + } + + @Test + public void snapshotGivenNullThenThrows() throws Exception { + // Given + final ST instance = makeInstanceOrThrow(); + + // When + Exception actual = null; + try { + instance.snapshot(null); + } catch (Exception ex) { + actual = ex; + } + + // Then + assertThat(actual).isNotNull(); + } + + @Test + public void snapshotGivenInstanceThenCreatesSnapshot() throws Exception { + // Given + final ST instance = makeInstanceOrThrow(); + final C credential = makeCredential(); + final String expectedId = credential.getId(); + final String expectedDescription = credential.getDescription(); + // Use reflection to call all the getter methods + final Map expectedSnapshotContents = new IdentityHashMap<>(); + for (final Method m : credentialBeingSnapshotted.getDeclaredMethods()) { + if (m.getParameterCount() != 0 || !m.isAccessible()) { + continue; + } + final Object expectedValue; + try { + expectedValue = m.invoke(credential); + } catch (IllegalAccessException e) { + continue; + } + expectedSnapshotContents.put(m, expectedValue); + } + + // When + final C actual = instance.snapshot(credential); + limitedUseSupplier.stopAnyFurtherCallsToTheSupplier(); + + // Then + assertThat(actual).isNotNull(); + assertThat(actual).isNotSameAs(credential); + assertThat(actual).isInstanceOf(credentialBeingSnapshotted); + // ...now check it contains the right data AND that getting that data doesn't + // trigger the supplier. + final String actualId = actual.getId(); + assertThat(actualId).as("getId()").isEqualTo(expectedId); + final String actualDescription = actual.getDescription(); + assertThat(actualDescription).as("getDescription").isEqualTo(expectedDescription); + for (final Method m : expectedSnapshotContents.keySet()) { + final Object expectedValue = expectedSnapshotContents.get(m); + final Object actualValue = m.invoke(actual); + assertThat(actualValue).as(m.toGenericString()).isEqualTo(expectedValue); + } + } + + /** + * {@link Supplier} that can be told to stop supplying so that we can verify + * that accessing a "snapshot" is using snapshotted values rather than still + * using live values from here. + * + * @param Type being supplied. + */ + private static class LimitedUseSupplier implements Supplier { + private final T supplied; + private boolean noFurtherUsePermitted = false; + + public LimitedUseSupplier(T supplied) { + this.supplied = supplied; + } + + /** + * Causes all subsequent calls to {@link #get()} to throw an + * {@link IllegalStateException}. + */ + public void stopAnyFurtherCallsToTheSupplier() { + noFurtherUsePermitted = true; + } + + @Override + public T get() { + if (noFurtherUsePermitted) { + throw new IllegalStateException( + "Illegal access to non-snapshotted data. This data should have been copied and only the copy accessed."); + } + return supplied; + } + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/BaseAwsJsonCredentialsTest.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/BaseAwsJsonCredentialsTest.java new file mode 100644 index 00000000..89d84c9f --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/BaseAwsJsonCredentialsTest.java @@ -0,0 +1,355 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.junit.Test; + +import com.cloudbees.plugins.credentials.CredentialsDescriptor; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsUnavailableException; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; + +import hudson.util.Secret; +import io.jenkins.plugins.credentials.secretsmanager.AwsCredentialsProvider; +import io.jenkins.plugins.credentials.secretsmanager.Messages; +import net.sf.json.JSONObject; + +public class BaseAwsJsonCredentialsTest { + @Test + public void getMandatoryFieldGivenValidJsonThenReturnsValue() { + // Given + final String expected = "myFieldValue"; + final String fieldName = "myFieldName"; + final String json = mkJson(fieldName, expected); + final Secret secretJson = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(secretJson); + final TestClass instance = new TestClass(stubSupplier); + + // When + final JSONObject jsonData = instance.getSecretJson(); + final String actual = instance.getMandatoryField(jsonData, fieldName); + + // Then + assertThat(actual).isEqualTo(expected); + } + + @Test + public void getMandatoryFieldGivenValidJsonWithUnwantedDataThenReturnsValue() { + // Given + final String expected = "myFieldValue"; + final String fieldName = "myFieldName"; + final String json = mkJson("someOtherField", "someOtherValue", fieldName, expected); + final Secret secretJson = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(secretJson); + final TestClass instance = new TestClass(stubSupplier); + + // When + final JSONObject jsonData = instance.getSecretJson(); + final String actual = instance.getMandatoryField(jsonData, fieldName); + + // Then + assertThat(actual).isEqualTo(expected); + } + + @Test + public void getOptionalFieldGivenValidJsonThenReturnsValue() { + // Given + final String expected = ""; + final String fieldName = "fieldWeDontHaveInTheJson"; + final String json = mkJson("oneField", "oneValue"); + final Secret secretJson = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(secretJson); + final TestClass instance = new TestClass(stubSupplier); + + // When + final JSONObject jsonData = instance.getSecretJson(); + final String actual = instance.getOptionalField(jsonData, fieldName); + + // Then + assertThat(actual).isEqualTo(expected); + } + + @Test + public void getOptionalFieldGivenValidJsonWithoutFieldThenReturnsEmptyString() { + // Given + final String expected = ""; + final String fieldName = "fieldWeDontHaveInTheJson"; + final String json = mkJson("oneField", "oneValue"); + final Secret secretJson = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(secretJson); + final TestClass instance = new TestClass(stubSupplier); + + // When + final JSONObject jsonData = instance.getSecretJson(); + final String actual = instance.getOptionalField(jsonData, fieldName); + + // Then + assertThat(actual).isEqualTo(expected); + } + + @Test + public void getSecretJsonGivenMissingJsonThenReturnsThrows() { + // Given + final Secret secretJson = null; + final Supplier stubSupplier = new StubSupplier<>(secretJson); + final TestClass instance = new TestClass(stubSupplier); + + // When + CredentialsUnavailableException actual = null; + try { + instance.getSecretJson(); + } catch (CredentialsUnavailableException ex) { + actual = ex; + } + + // Then + assertThat(actual).isNotNull(); + assertThat(actual.getProperty()).isEqualTo("secret"); + assertThat(actual.getMessage()).contains(Messages.noValidJsonError(instance.getId())); + } + + @Test + public void getSecretJsonGivenEmptyJsonThenReturnsThrows() { + // Given + final String json = ""; + final Secret secretJson = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(secretJson); + final TestClass instance = new TestClass(stubSupplier); + + // When + CredentialsUnavailableException actual = null; + try { + instance.getSecretJson(); + } catch (CredentialsUnavailableException ex) { + actual = ex; + } + + // Then + assertThat(actual).isNotNull(); + assertThat(actual.getProperty()).isEqualTo("secret"); + assertThat(actual.getMessage()).contains(Messages.noValidJsonError(instance.getId())); + } + + @Test + public void getSecretJsonGivenInvalidJsonThenReturnsThrows() { + final String json = "1234 is not valid JSON"; + final Secret secretJson = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(secretJson); + final TestClass instance = new TestClass(stubSupplier); + + // When + CredentialsUnavailableException actual = null; + try { + instance.getSecretJson(); + } catch (CredentialsUnavailableException ex) { + actual = ex; + } + + // Then + assertThat(actual).isNotNull(); + assertThat(actual.getProperty()).isEqualTo("secret"); + assertThat(actual.getMessage()).contains(instance.getId()); + assertThat(actual.getMessage()).contains(Messages.noValidJsonError(instance.getId())); + assertThat(actual.getMessage()).doesNotContain("1234"); + } + + @Test + public void getSecretJsonGivenUnexpectedJsonThenReturnsThrows() { + final String json = "[ \"hello\", \"world\" ]"; + final Secret secretJson = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(secretJson); + final TestClass instance = new TestClass(stubSupplier); + + // When + CredentialsUnavailableException actual = null; + try { + instance.getSecretJson(); + } catch (CredentialsUnavailableException ex) { + actual = ex; + } + + // Then + assertThat(actual).isNotNull(); + assertThat(actual.getProperty()).isEqualTo("secret"); + assertThat(actual.getMessage()).contains(instance.getId()); + assertThat(actual.getMessage()).contains(Messages.noValidJsonError(instance.getId())); + assertThat(actual.getMessage()).doesNotContain("hello"); + } + + @Test + public void getMandatoryFieldGivenValidJsonMissingDesiredFieldThenReturnsThrows() { + // Given + final String unexpectedFieldName = "potentiallySecretFieldName"; + final String unexpectedValue = "potentiallySecretValue"; + final String json = mkJson(unexpectedFieldName, unexpectedValue, unexpectedFieldName + "2", unexpectedValue); + final Secret secretJson = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(secretJson); + final TestClass instance = new TestClass(stubSupplier); + final String missingFieldName = "someOtherFieldName"; + + // When + final JSONObject jsonObject = instance.getSecretJson(); + CredentialsUnavailableException actual = null; + try { + instance.getMandatoryField(jsonObject, missingFieldName); + } catch (CredentialsUnavailableException ex) { + actual = ex; + } + + // Then + assertThat(actual).isNotNull(); + assertThat(actual.getProperty()).isEqualTo("secret"); + assertThat(actual.getMessage()).contains(instance.getId()); + assertThat(actual.getMessage()).contains(missingFieldName); + assertThat(actual.getMessage()).contains(Messages.wrongJsonError(instance.getId(), missingFieldName)); + assertThat(actual.getMessage()).doesNotContain(unexpectedFieldName); + assertThat(actual.getMessage()).doesNotContain(unexpectedValue); + } + + @Test + public void snapshotConstructorGivenInstanceToSnapshotThenReturnsClone() { + // Given + final String json = mkJson("oneField", "oneValue"); + final Secret secretJson = Secret.fromString(json); + final StubSingleShotSupplier stubSupplier = new StubSingleShotSupplier(secretJson); + final TestClass original = new TestClass(stubSupplier); + final JSONObject expectedSecretJson = original.getSecretJson(); + final String expectedId = original.getId(); + final String expectedDescription = original.getDescription(); + stubSupplier.forgetPreviousCalls(); + + // When + final TestClass actual = new TestClass(original); + + // Then + final JSONObject actualSecretJson = actual.getSecretJson(); + assertThat((Comparable) actualSecretJson).isEqualTo((Comparable) expectedSecretJson); + final String actualId = actual.getId(); + assertThat(actualId).isEqualTo(expectedId); + final String actualDescription = actual.getDescription(); + assertThat(actualDescription).isEqualTo(expectedDescription); + } + + public static void assertThatJsonCredentialsDescriptorIsTheSameAsTheDescriptorForNonJsonCredentials( + CredentialsDescriptor instanceUnderTest, CredentialsDescriptor nonJsonEquivalent) { + // Given + final CredentialsProvider awsCredProvider = new AwsCredentialsProvider(); + final CredentialsProvider otherCredProvider = new SystemCredentialsProvider.ProviderImpl(); + final String expectedDisplayName = nonJsonEquivalent.getDisplayName(); + final String expectedIconClassName = nonJsonEquivalent.getIconClassName(); + final boolean expectedApplicableToAws = nonJsonEquivalent.isApplicable(awsCredProvider); + final boolean expectedApplicableToOther = nonJsonEquivalent.isApplicable(otherCredProvider); + final String[] expectedDeclaredMethods = toClassAgnosticMethodDescription( + nonJsonEquivalent.getClass().getDeclaredMethods()); + + // When + final String actualDisplayName = instanceUnderTest.getDisplayName(); + final String actualIconClassName = instanceUnderTest.getIconClassName(); + final boolean actualApplicableToAws = instanceUnderTest.isApplicable(awsCredProvider); + final boolean actualApplicableToOther = instanceUnderTest.isApplicable(otherCredProvider); + final String[] actualDeclaredMethods = toClassAgnosticMethodDescription( + instanceUnderTest.getClass().getDeclaredMethods()); + + // Then + assertThat(actualDisplayName).isEqualTo(expectedDisplayName); + assertThat(actualIconClassName).isEqualTo(expectedIconClassName); + assertThat(actualApplicableToAws).isEqualTo(expectedApplicableToAws); + assertThat(actualApplicableToOther).isEqualTo(expectedApplicableToOther); + // Check no other unexpected behavior was added to one and not the other + assertThat(actualDeclaredMethods).containsExactly(expectedDeclaredMethods); + } + + public static String mkJson(String fieldName1, String fieldValue1) { + return "{ \"" + fieldName1 + "\": \"" + fieldValue1 + "\" }"; + } + + public static String mkJson(String fieldName1, String fieldValue1, String fieldName2, String fieldValue2) { + return "{ \"" + fieldName1 + "\": \"" + fieldValue1 + "\", \"" + fieldName2 + "\": \"" + fieldValue2 + "\" }"; + } + + public static String mkJson(String fieldName1, String fieldValue1, String fieldName2, String fieldValue2, + String fieldName3, String fieldValue3) { + return "{ \"" + fieldName1 + "\": \"" + fieldValue1 + "\", \"" + fieldName2 + "\": \"" + fieldValue2 + "\", \"" + + fieldName3 + "\": \"" + fieldValue3 + "\" }"; + } + + private static String[] toClassAgnosticMethodDescription(Method[] m) { + final String[] results = new String[m.length]; + for (int i = 0; i < m.length; i++) { + final Class declaringClass = m[i].getDeclaringClass(); + final String original = m[i].toGenericString(); + final String definingClass = declaringClass.getName(); + final String result = original.replace(definingClass + ".", ""); + results[i] = result; + } + return results; + } + + private static class TestClass extends BaseAwsJsonCredentials { + TestClass(Supplier usernameAndPasswordJson) { + super("TestId", "TestDescription", usernameAndPasswordJson); + } + + TestClass(BaseAwsJsonCredentials toSnapshot) { + super(toSnapshot); + } + + // expose so we can test it + + @Override + public String getMandatoryField(JSONObject secretJson, String fieldname) { + return super.getMandatoryField(secretJson, fieldname); + } + + @Override + public String getOptionalField(JSONObject secretJson, String fieldname) { + return super.getOptionalField(secretJson, fieldname); + } + + @Override + public JSONObject getSecretJson() { + return super.getSecretJson(); + } + } + + // if we had a mocking framework like Mockito on the classpath then we wouldn't + // need this. + public static class StubSupplier implements Supplier { + private final T supplied; + + public StubSupplier(T supplied) { + this.supplied = supplied; + } + + @Override + public T get() { + return supplied; + } + } + + public static class StubSingleShotSupplier implements Supplier { + private final T supplied; + private Throwable whereWeWereCalledFromTheFirstTime = null; + + public StubSingleShotSupplier(T supplied) { + this.supplied = supplied; + } + + public void forgetPreviousCalls() { + whereWeWereCalledFromTheFirstTime = null; + } + + @Override + public T get() { + if (whereWeWereCalledFromTheFirstTime != null) { + throw new IllegalStateException("This provider has already been called before", + whereWeWereCalledFromTheFirstTime); + } + whereWeWereCalledFromTheFirstTime = new Throwable("First call to the supplier was from here."); + return supplied; + } + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKeySnapshotTakerTest.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKeySnapshotTakerTest.java new file mode 100644 index 00000000..ae1fc34a --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKeySnapshotTakerTest.java @@ -0,0 +1,23 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key; + +import java.util.function.Supplier; + +import hudson.util.Secret; +import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsSnapshotTakerTest; + +public class AwsJsonSshUserPrivateKeySnapshotTakerTest extends + BaseAwsJsonCredentialsSnapshotTakerTest { + + public AwsJsonSshUserPrivateKeySnapshotTakerTest() { + super(AwsJsonSshUserPrivateKeySnapshotTaker.class, AwsJsonSshUserPrivateKey.class); + } + + @Override + protected AwsJsonSshUserPrivateKey makeCredential() { + final String json = AwsJsonSshUserPrivateKeyTest.mkUsernameKeyAndPassphraseJson("someUsername", "someKey", + "somePassphrase"); + final Secret secret = Secret.fromString(json); + final Supplier s = super.mkSupplier(secret); + return new AwsJsonSshUserPrivateKey("someId", "someDescription", s); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKeyTest.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKeyTest.java new file mode 100644 index 00000000..4eb67110 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/ssh_user_private_key/AwsJsonSshUserPrivateKeyTest.java @@ -0,0 +1,192 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key; + +import static io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.mkJson; +import static io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.assertThatJsonCredentialsDescriptorIsTheSameAsTheDescriptorForNonJsonCredentials; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import org.junit.Test; + +import com.cloudbees.plugins.credentials.CredentialsUnavailableException; + +import hudson.util.Secret; +import io.jenkins.plugins.credentials.secretsmanager.Messages; +import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.StubSingleShotSupplier; +import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.StubSupplier; + +public class AwsJsonSshUserPrivateKeyTest { + @Test + public void getPassphraseGivenValidJsonThenReturnsSecretPassphrase() { + // Given + final String id = "testId"; + final String description = "some test description"; + final String expected = "mySecretPassphrase"; + final String json = mkUsernameKeyAndPassphraseJson("myUsername", "myKey", expected); + final Secret stubSecret = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(stubSecret); + final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier); + + // When + final Secret actualSecret = instance.getPassphrase(); + final String actualPassphrase = actualSecret.getPlainText(); + + // Then + assertThat(actualPassphrase).isEqualTo(expected); + } + + @Test + public void getPassphraseGivenValidJsonWithNoPassphraseThenReturnsEmpty() { + // Given + final String id = "testId"; + final String description = "some test description"; + final String expected = ""; + final String json = mkUsernameKeyAndNoPassphraseJson("myUsername", "myKey"); + final Secret stubSecret = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(stubSecret); + final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier); + + // When + final Secret actualSecret = instance.getPassphrase(); + final String actualPassphrase = actualSecret.getPlainText(); + + // Then + assertThat(actualPassphrase).isEqualTo(expected); + } + + @Test + public void getPrivateKeysGivenValidJsonThenReturnsSingleKey() { + // Given + final String id = "testId"; + final String description = "some test description"; + final List expected = Collections.singletonList("theKeyThatISet"); + final String json = mkUsernameKeyAndPassphraseJson("myUsername", expected.get(0), "mySecretPassphrase"); + final Secret stubSecret = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(stubSecret); + final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier); + + // When + final List actual = instance.getPrivateKeys(); + + // Then + assertThat(actual).isEqualTo(expected); + } + + @Test + public void getUsernameGivenValidJsonThenReturnsUsername() { + // Given + final String id = "testId"; + final String description = "some test description"; + final String expected = "myUsername"; + final String json = mkUsernameKeyAndPassphraseJson(expected, "myKey", "mySecretPassphrase"); + final Secret stubSecret = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(stubSecret); + final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier); + + // When + final String actual = instance.getUsername(); + + // Then + assertThat(actual).isEqualTo(expected); + } + + @Test + public void isUsernameSecretGivenAnythingThenReturnsTrue() { + // Given + final String id = "testId"; + final String description = "some test description"; + final boolean expected = true; + final String json = mkUsernameKeyAndPassphraseJson("myUser", "myKey", "mySecretPassphrase"); + final Secret stubSecret = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(stubSecret); + final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier); + + // When + final boolean actual = instance.isUsernameSecret(); + + // Then + assertThat(actual).isEqualTo(expected); + } + + @Test + public void getUsernameGivenInvalidJsonThenReturnsThrows() { + // Given + final String id = "testId"; + final String description = "some test description"; + final String unexpectedFieldName = "potentiallySecretFieldName"; + final String unexpectedValue = "potentiallySecretValue"; + final String json = mkJson(unexpectedFieldName, unexpectedValue, unexpectedFieldName + "2", unexpectedValue); + final Secret stubSecret = Secret.fromString(json); + final Supplier stubSupplier = new StubSupplier<>(stubSecret); + final AwsJsonSshUserPrivateKey instance = new AwsJsonSshUserPrivateKey(id, description, stubSupplier); + + // When + CredentialsUnavailableException actual = null; + try { + instance.getUsername(); + } catch (CredentialsUnavailableException ex) { + actual = ex; + } + + // Then + assertThat(actual).isNotNull(); + assertThat(actual.getProperty()).isEqualTo("secret"); + assertThat(actual.getMessage()).contains(id); + assertThat(actual.getMessage()).contains(AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_USERNAME); + assertThat(actual.getMessage()) + .contains(Messages.wrongJsonError(id, AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_USERNAME)); + assertThat(actual.getMessage()).doesNotContain(unexpectedFieldName); + assertThat(actual.getMessage()).doesNotContain(unexpectedValue); + } + + @Test + public void snapshotConstructorGivenInstanceToSnapshotThenReturnsClone() { + // Given + final String expectedUsername = "myUser"; + final String expectedPassphrase = "mySecretPassphrase"; + final String expectedId = "someId"; + final String expectedDescription = "someDescription"; + final String expectedKey = "someKeyThatIMade"; + final String json = mkUsernameKeyAndPassphraseJson(expectedUsername, expectedKey, expectedPassphrase); + final Secret secretJson = Secret.fromString(json); + final StubSingleShotSupplier stubSupplier = new StubSingleShotSupplier(secretJson); + final AwsJsonSshUserPrivateKey original = new AwsJsonSshUserPrivateKey(expectedId, expectedDescription, + stubSupplier); + + // When + final AwsJsonSshUserPrivateKey actual = new AwsJsonSshUserPrivateKey(original); + + // Then + final String actualId = actual.getId(); + assertThat(actualId).isEqualTo(expectedId); + final String actualDescription = actual.getDescription(); + assertThat(actualDescription).isEqualTo(expectedDescription); + final String actualUsername = actual.getUsername(); + assertThat(actualUsername).isEqualTo(expectedUsername); + final String actualPassphrase = actual.getPassphrase().getPlainText(); + assertThat(actualPassphrase).isEqualTo(expectedPassphrase); + final String actualKey = actual.getPrivateKeys().get(0); + assertThat(actualKey).isEqualTo(expectedKey); + } + + @Test + public void ourDescriptorIsTheSameAsDescriptorForNonJsonCredentials() { + // Given + final AwsSshUserPrivateKey.DescriptorImpl expected = new AwsSshUserPrivateKey.DescriptorImpl(); + final AwsJsonSshUserPrivateKey.DescriptorImpl instance = new AwsJsonSshUserPrivateKey.DescriptorImpl(); + assertThatJsonCredentialsDescriptorIsTheSameAsTheDescriptorForNonJsonCredentials(instance, expected); + } + + static String mkUsernameKeyAndPassphraseJson(String username, String key, String passphrase) { + return mkJson(AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_USERNAME, username, + AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_PASSPHRASE, passphrase, + AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_PRIVATE_KEY, key); + } + + private static String mkUsernameKeyAndNoPassphraseJson(String username, String key) { + return mkJson(AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_USERNAME, username, + AwsJsonSshUserPrivateKey.JSON_FIELDNAME_FOR_PRIVATE_KEY, key); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentialsSnapshotTakerTest.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentialsSnapshotTakerTest.java new file mode 100644 index 00000000..8bd2768a --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentialsSnapshotTakerTest.java @@ -0,0 +1,23 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory.username_password; + +import java.util.function.Supplier; + +import hudson.util.Secret; +import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsSnapshotTakerTest; + +public class AwsJsonUsernamePasswordCredentialsSnapshotTakerTest extends + BaseAwsJsonCredentialsSnapshotTakerTest { + + public AwsJsonUsernamePasswordCredentialsSnapshotTakerTest() { + super(AwsJsonUsernamePasswordCredentialsSnapshotTaker.class, AwsJsonUsernamePasswordCredentials.class); + } + + @Override + protected AwsJsonUsernamePasswordCredentials makeCredential() { + final String json = AwsJsonUsernamePasswordCredentialsTest.mkUsernameAndPasswordJson("someUsername", + "somePassword"); + final Secret secret = Secret.fromString(json); + final Supplier s = super.mkSupplier(secret); + return new AwsJsonUsernamePasswordCredentials("someId", "someDescription", s); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentialsTest.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentialsTest.java new file mode 100644 index 00000000..a5b3ebaf --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/factory/username_password/AwsJsonUsernamePasswordCredentialsTest.java @@ -0,0 +1,149 @@ +package io.jenkins.plugins.credentials.secretsmanager.factory.username_password; + +import static io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.mkJson; +import static io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.assertThatJsonCredentialsDescriptorIsTheSameAsTheDescriptorForNonJsonCredentials; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Supplier; + +import org.junit.Test; + +import com.cloudbees.plugins.credentials.CredentialsUnavailableException; + +import hudson.util.Secret; +import io.jenkins.plugins.credentials.secretsmanager.Messages; +import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.StubSingleShotSupplier; +import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentialsTest.StubSupplier; + +public class AwsJsonUsernamePasswordCredentialsTest { + @Test + public void getPasswordGivenValidJsonThenReturnsSecretPassword() { + // Given + final String id = "testId"; + final String description = "some test description"; + final String expected = "mySecretPassword"; + final String usernamePasswordJson = mkUsernameAndPasswordJson("myUsername", expected); + final Secret stubSecret = Secret.fromString(usernamePasswordJson); + final Supplier stubSupplier = new StubSupplier<>(stubSecret); + final AwsJsonUsernamePasswordCredentials instance = new AwsJsonUsernamePasswordCredentials(id, description, + stubSupplier); + + // When + final Secret actualSecret = instance.getPassword(); + final String actualPassword = actualSecret.getPlainText(); + + // Then + assertThat(actualPassword).isEqualTo(expected); + } + + @Test + public void getUsernameGivenValidJsonThenReturnsUsername() { + // Given + final String id = "testId"; + final String description = "some test description"; + final String expected = "myUsername"; + final String usernamePasswordJson = mkUsernameAndPasswordJson(expected, "mySecretPassword"); + final Secret stubSecret = Secret.fromString(usernamePasswordJson); + final Supplier stubSupplier = new StubSupplier<>(stubSecret); + final AwsJsonUsernamePasswordCredentials instance = new AwsJsonUsernamePasswordCredentials(id, description, + stubSupplier); + + // When + final String actual = instance.getUsername(); + + // Then + assertThat(actual).isEqualTo(expected); + } + + @Test + public void isUsernameSecretGivenAnythingThenReturnsTrue() { + // Given + final String id = "testId"; + final String description = "some test description"; + final boolean expected = true; + final String usernamePasswordJson = mkUsernameAndPasswordJson("myUser", "mySecretPassword"); + final Secret stubSecret = Secret.fromString(usernamePasswordJson); + final Supplier stubSupplier = new StubSupplier<>(stubSecret); + final AwsJsonUsernamePasswordCredentials instance = new AwsJsonUsernamePasswordCredentials(id, description, + stubSupplier); + + // When + final boolean actual = instance.isUsernameSecret(); + + // Then + assertThat(actual).isEqualTo(expected); + } + + @Test + public void getUsernameGivenInvalidJsonThenReturnsThrows() { + // Given + final String id = "testId"; + final String description = "some test description"; + final String unexpectedFieldName = "potentiallySecretFieldName"; + final String unexpectedValue = "potentiallySecretValue"; + final String usernamePasswordJson = mkJson(unexpectedFieldName, unexpectedValue, unexpectedFieldName + "2", + unexpectedValue); + final Secret stubSecret = Secret.fromString(usernamePasswordJson); + final Supplier stubSupplier = new StubSupplier<>(stubSecret); + final AwsJsonUsernamePasswordCredentials instance = new AwsJsonUsernamePasswordCredentials(id, description, + stubSupplier); + + // When + CredentialsUnavailableException actual = null; + try { + instance.getUsername(); + } catch (CredentialsUnavailableException ex) { + actual = ex; + } + + // Then + assertThat(actual).isNotNull(); + assertThat(actual.getProperty()).isEqualTo("secret"); + assertThat(actual.getMessage()).contains(id); + assertThat(actual.getMessage()).contains(AwsJsonUsernamePasswordCredentials.JSON_FIELDNAME_FOR_USERNAME); + assertThat(actual.getMessage()) + .contains(Messages.wrongJsonError(id, AwsJsonUsernamePasswordCredentials.JSON_FIELDNAME_FOR_USERNAME)); + assertThat(actual.getMessage()).doesNotContain(unexpectedFieldName); + assertThat(actual.getMessage()).doesNotContain(unexpectedValue); + } + + @Test + public void snapshotConstructorGivenInstanceToSnapshotThenReturnsClone() { + // Given + final String expectedUsername = "myUser"; + final String expectedPassword = "mySecretPassword"; + final String expectedId = "someId"; + final String expectedDescription = "someDescription"; + final String json = mkUsernameAndPasswordJson(expectedUsername, expectedPassword); + final Secret secretJson = Secret.fromString(json); + final StubSingleShotSupplier stubSupplier = new StubSingleShotSupplier(secretJson); + final AwsJsonUsernamePasswordCredentials original = new AwsJsonUsernamePasswordCredentials(expectedId, + expectedDescription, stubSupplier); + + // When + final AwsJsonUsernamePasswordCredentials actual = new AwsJsonUsernamePasswordCredentials(original); + + // Then + final String actualId = actual.getId(); + assertThat(actualId).isEqualTo(expectedId); + final String actualDescription = actual.getDescription(); + assertThat(actualDescription).isEqualTo(expectedDescription); + final String actualUsername = actual.getUsername(); + assertThat(actualUsername).isEqualTo(expectedUsername); + final String actualPassword = actual.getPassword().getPlainText(); + assertThat(actualPassword).isEqualTo(expectedPassword); + } + + @Test + public void ourDescriptorIsTheSameAsDescriptorForNonJsonCredentials() { + // Given + final AwsUsernamePasswordCredentials.DescriptorImpl expected = new AwsUsernamePasswordCredentials.DescriptorImpl(); + final AwsJsonUsernamePasswordCredentials.DescriptorImpl instance = new AwsJsonUsernamePasswordCredentials.DescriptorImpl(); + assertThatJsonCredentialsDescriptorIsTheSameAsTheDescriptorForNonJsonCredentials(instance, expected); + } + + static String mkUsernameAndPasswordJson(String username, String password) { + return mkJson(AwsJsonUsernamePasswordCredentials.JSON_FIELDNAME_FOR_USERNAME, username, + AwsJsonUsernamePasswordCredentials.JSON_FIELDNAME_FOR_PASSWORD, password); + } +}