From 3fed37d7dc1c3a6310ac3c0e089db338a168a526 Mon Sep 17 00:00:00 2001 From: Chris Kilding <590569+chriskilding@users.noreply.github.com> Date: Sat, 20 Jun 2020 17:42:07 +0100 Subject: [PATCH] Feature: SecretSource API support (#27) --- docs/README.md | 79 ++++-- docs/filters/index.md | 25 ++ pom.xml | 26 +- .../secretsmanager/AwsSecretSource.java | 98 ++++++++ .../secretsmanager/AbstractPluginIT.java | 230 ------------------ .../secretsmanager/CredentialsProviderIT.java | 122 +++++++--- .../secretsmanager/FileCredentialsIT.java | 134 ++++++---- .../credentials/secretsmanager/FiltersIT.java | 44 ++-- .../secretsmanager/GitPluginIT.java | 76 ++++-- .../secretsmanager/SSHUserPrivateKeyIT.java | 128 ++++++---- .../secretsmanager/SecretSourceIT.java | 181 ++++++++++++++ .../StandardCertificateCredentialsIT.java | 139 +++++++---- ...StandardUsernamePasswordCredentialsIT.java | 113 +++++---- .../secretsmanager/StringCredentialsIT.java | 92 ++++--- .../config/AbstractCheckConnectionIT.java | 7 - ...ava => AbstractPluginConfigurationIT.java} | 2 +- .../config/CheckConnectionApiIT.java | 10 +- .../config/CheckConnectionWebIT.java | 10 +- ...st.java => PluginCasCConfigurationIT.java} | 5 +- ...est.java => PluginWebConfigurationIT.java} | 2 +- .../util/AWSSecretsManagerRule.java | 31 +++ .../AutoErasingAWSSecretsManagerRule.java | 31 +++ .../secretsmanager/util/AwsTags.java | 29 +++ .../util/CreateSecretOperation.java | 103 -------- .../secretsmanager/util/CredentialNames.java | 13 + .../util/CredentialSnapshots.java | 16 ++ .../secretsmanager/util/Crypto.java | 32 +-- .../util/JenkinsCredentials.java | 42 ++++ .../secretsmanager/util/JenkinsPipelines.java | 42 ++++ .../secretsmanager/util/Lists.java | 18 ++ .../credentials/secretsmanager/util/Maps.java | 27 -- .../util/MyJenkinsConfiguredWithCodeRule.java | 17 ++ .../secretsmanager/util/Rules.java | 12 + .../util/assertions/CustomAssertions.java | 51 ++++ .../util/assertions/CustomSoftAssertions.java | 8 + .../assertions/FileCredentialsAssert.java | 72 ++++++ .../util/assertions/KeyStoreAssert.java | 49 ++++ .../KeyStoreSoftAssertionsProvider.java | 11 + .../util/assertions/ListBoxModelAssert.java | 23 ++ .../assertions/SSHUserPrivateKeyAssert.java | 62 +++++ .../StandardCertificateCredentialsAssert.java | 46 ++++ ...cateCredentialsSoftAssertionsProvider.java | 10 + .../assertions/StandardCredentialsAssert.java | 39 +++ ...dardCredentialsSoftAssertionsProvider.java | 10 + ...dardUsernamePasswordCredentialsAssert.java | 50 ++++ .../assertions/StringCredentialsAssert.java | 35 +++ .../util/assertions/WorkflowRunAssert.java | 4 - .../secretsmanager/util/git/GitSshServer.java | 3 +- 48 files changed, 1674 insertions(+), 735 deletions(-) create mode 100644 docs/filters/index.md create mode 100644 src/main/java/io/jenkins/plugins/credentials/secretsmanager/AwsSecretSource.java delete mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/AbstractPluginIT.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/SecretSourceIT.java rename src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/{AbstractPluginConfigurationTest.java => AbstractPluginConfigurationIT.java} (96%) rename src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/{PluginCasCConfigurationTest.java => PluginCasCConfigurationIT.java} (86%) rename src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/{PluginWebConfigurationTest.java => PluginWebConfigurationIT.java} (97%) create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AWSSecretsManagerRule.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AutoErasingAWSSecretsManagerRule.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java delete mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CreateSecretOperation.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CredentialNames.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CredentialSnapshots.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/JenkinsCredentials.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/JenkinsPipelines.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Lists.java delete mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Maps.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/MyJenkinsConfiguredWithCodeRule.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Rules.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomAssertions.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomSoftAssertions.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/FileCredentialsAssert.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/KeyStoreAssert.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/KeyStoreSoftAssertionsProvider.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/ListBoxModelAssert.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/SSHUserPrivateKeyAssert.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCertificateCredentialsAssert.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCertificateCredentialsSoftAssertionsProvider.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCredentialsAssert.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCredentialsSoftAssertionsProvider.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardUsernamePasswordCredentialsAssert.java create mode 100644 src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StringCredentialsAssert.java diff --git a/docs/README.md b/docs/README.md index 7d145add..7e161f0b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ Access credentials from AWS Secrets Manager in your Jenkins jobs. ## Contents - [Caching](caching/index.md) +- [Filters](filters/index.md) - [Networking](networking/index.md) - [Screenshots](screenshots/index.md) - Project @@ -18,8 +19,8 @@ Access credentials from AWS Secrets Manager in your Jenkins jobs. ## Features - Read-only view of Secrets Manager. +- `CredentialsProvider` and `SecretSource` API support. - Credential metadata caching (duration: 5 minutes). -- Jenkins [Configuration As Code](https://github.com/jenkinsci/configuration-as-code-plugin) support. - [Cross-account](http://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html) Secrets Manager support with IAM roles. ## Setup @@ -43,12 +44,20 @@ Optional permissions: ## Usage -1. **Upload the secret** to Secrets Manager as shown below (see also the [AWS documentation](https://docs.aws.amazon.com/cli/latest/reference/secretsmanager/create-secret.html)). -2. **Reference the secret** by name in your Jenkins job. +The plugin supports the following secrets resolution APIs: -A Secrets Manager secret acts as one of the following Jenkins credential types, depending on the `jenkins:credentials:type` tag that you add to it. The tag's value must be the relevant Jenkinsfile credentials binding [type name](https://jenkins.io/doc/pipeline/steps/credentials-binding/), e.g. `string` for Secret Text. +- [CredentialsProvider](#CredentialsProvider) (high-level API) +- [SecretSource](#SecretSource) (low-level API) -### Secret Text +Note: Any string secret is accessible through SecretSource, but only a secret with the `jenkins:credentials:type` tag is accessible through CredentialsProvider. This distinction allows you to share tagged secrets between both APIs, while untagged secrets are only accessible through SecretSource. + +### CredentialsProvider + +The plugin allows secrets from Secrets Manager to be used as Jenkins credentials. + +A secret will act as one of the following Jenkins [credential types](https://jenkins.io/doc/pipeline/steps/credentials-binding/), based on the `jenkins:credentials:type` tag that you add to it. + +#### Secret Text A simple text *secret*. @@ -56,10 +65,9 @@ A simple text *secret*. - Tags: - `jenkins:credentials:type` = `string` -:white_check_mark: Use this credential type whenever it is practical. It is the simplest and most widely compatible type. +##### Example - -#### Example +AWS CLI: ```bash aws secretsmanager create-secret --name 'newrelic-api-key' --secret-string 'abc123' --tags 'Key=jenkins:credentials:type,Value=string' --description 'Acme Corp Newrelic API key' @@ -93,7 +101,7 @@ node { } ``` -### Username with Password +#### Username with Password A *username* and *password* pair. @@ -102,7 +110,9 @@ A *username* and *password* pair. - `jenkins:credentials:type` = `usernamePassword` - `jenkins:credentials:username` = *username* -#### Example +##### Example + +AWS CLI: ```bash aws secretsmanager create-secret --name 'artifactory' --secret-string 'supersecret' --tags 'Key=jenkins:credentials:type,Value=usernamePassword' 'Key=jenkins:credentials:username,Value=joe' --description 'Acme Corp Artifactory login' @@ -137,7 +147,7 @@ node { } ``` -### SSH User Private Key +#### SSH User Private Key An SSH *private key*, with a *username*. @@ -148,7 +158,9 @@ An SSH *private key*, with a *username*. Common private key formats include PKCS#1 (starts with `-----BEGIN [ALGORITHM] PRIVATE KEY-----`) and PKCS#8 (starts with `-----BEGIN PRIVATE KEY-----`). -#### Example +##### Example + +AWS CLI: ```bash ssh-keygen -t rsa -b 4096 -C 'acme@example.com' -f id_rsa @@ -184,7 +196,7 @@ node { } ``` -### Certificate +#### Certificate A client certificate *keystore* in PKCS#12 format, encrypted with a zero-length password. @@ -192,7 +204,9 @@ A client certificate *keystore* in PKCS#12 format, encrypted with a zero-length - Tags: - `jenkins:credentials:type` = `certificate` -#### Example +##### Example + +AWS CLI: ```bash openssl pkcs12 -export -in /path/to/cert.pem -inkey /path/to/key.pem -out certificate.p12 -passout pass: @@ -209,7 +223,7 @@ node { } ``` -### Secret File +#### Secret File A secret file with binary *content* and an optional *filename*. @@ -220,7 +234,9 @@ A secret file with binary *content* and an optional *filename*. The credential ID is used as the filename by default. In the rare cases when you need to override this (for example, if the credential ID would be an invalid filename on your filesystem), you can set the `jenkins:credentials:filename` tag. -#### Example +##### Example + +AWS CLI: ```bash echo -n $'\x01\x02\x03' > license.bin @@ -255,6 +271,30 @@ node { } ``` +### SecretSource + +The plugin allows JCasC to interpolate string secrets from Secrets Manager. + +#### Example + +AWS CLI: + +```bash +aws secretsmanager create-secret --name 'my-password' --secret-string 'abc123' --description 'Jenkins user password' +``` + +JCasC: + +```yaml +jenkins: + securityRealm: + local: + allowsSignup: false + users: + - id: "foo" + password: "${my-password}" +``` + ## Configuration Available settings: @@ -304,14 +344,15 @@ All secrets must be uploaded via the AWS CLI or API. This is because the AWS Web In Maven: -```bash -mvn verify +```shell script +mvn clean verify ``` In your IDE: -1. Generate translations: `mvn localizer:generate`. (This is a one-off task. You only need to re-run this if you change the translations, or if you clean the Maven target directory.) +1. Generate translations: `mvn localizer:generate`. (This is a one-off task. You only need to re-run this if you change the translations, or if you clean the Maven `target` directory.) 2. Compile. 3. Start Moto: `mvn docker:build docker:start`. 4. Run tests. 5. Stop Moto: `mvn docker:stop`. + diff --git a/docs/filters/index.md b/docs/filters/index.md new file mode 100644 index 00000000..4110a9a9 --- /dev/null +++ b/docs/filters/index.md @@ -0,0 +1,25 @@ +# Filters + +The CredentialsProvider implementation in this plugin calls `secretsmanager:ListSecrets` to cache the secrets' metadata. At this time, Secrets Manager does not support server-side restrictions on this list, so it returns all secrets in the AWS account, whether you have given Jenkins the `secretsmanager:GetSecretValue` permission to actually resolve those secrets or not. This can result in unwanted entries appearing in the credentials UI, which users will mistake for resolvable credentials. + +To improve the user experience of this aspect of Jenkins, you can specify optional filters in the plugin configuration. Only the secrets that match the filter criteria will be presented through the CredentialsProvider. This hides unwanted entries from the credentials UI. + +Notes: + +- These are client-side filters. As such they only provide usability benefits. They have no security benefits, as Jenkins still fetches the full secret list from AWS. +- The SecretSource implementation does not use the filters, as they are not relevant to it. + +## Tag Filter + +You can choose to only show credentials that have a tag with a particular key and value. + +Example: `product` = `foo`. + +```yaml +unclassified: + awsCredentialsProvider: + filters: + tag: + key: product + value: foo +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0e8541ad..263acc80 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,12 @@ aws-java-sdk 1.11.636 + + io.jenkins + configuration-as-code + ${jcasc.version} + true + org.jenkins-ci.plugins bouncycastle-api @@ -90,7 +96,7 @@ org.assertj assertj-core - 3.11.1 + 3.16.1 test @@ -99,12 +105,6 @@ 3.10.0 test - - io.jenkins - configuration-as-code - ${jcasc.version} - test - io.jenkins.configuration-as-code test-harness @@ -187,18 +187,6 @@ org.apache.maven.plugins maven-dependency-plugin - - org.jacoco - jacoco-maven-plugin - - - - prepare-agent - report - - - - org.apache.maven.plugins maven-release-plugin diff --git a/src/main/java/io/jenkins/plugins/credentials/secretsmanager/AwsSecretSource.java b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/AwsSecretSource.java new file mode 100644 index 00000000..929773b2 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/credentials/secretsmanager/AwsSecretSource.java @@ -0,0 +1,98 @@ +package io.jenkins.plugins.credentials.secretsmanager; + +import com.amazonaws.SdkClientException; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.secretsmanager.AWSSecretsManager; +import com.amazonaws.services.secretsmanager.AWSSecretsManagerClient; +import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder; +import com.amazonaws.services.secretsmanager.model.AWSSecretsManagerException; +import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest; +import com.amazonaws.services.secretsmanager.model.GetSecretValueResult; +import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException; +import hudson.Extension; +import io.jenkins.plugins.casc.SecretSource; +import io.jenkins.plugins.credentials.secretsmanager.config.EndpointConfiguration; +import io.jenkins.plugins.credentials.secretsmanager.config.PluginConfiguration; + +import java.io.IOException; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Extension(optional = true) +public class AwsSecretSource extends SecretSource { + + private static final Logger LOG = Logger.getLogger(AwsSecretSource.class.getName()); + + private static final String AWS_SERVICE_ENDPOINT = "AWS_SERVICE_ENDPOINT"; + private static final String AWS_SIGNING_REGION = "AWS_SIGNING_REGION"; + + private transient AWSSecretsManager client = null; + + @Override + public Optional reveal(String id) throws IOException { + try { + final GetSecretValueResult result = client.getSecretValue(new GetSecretValueRequest().withSecretId(id)); + + if (result.getSecretBinary() != null) { + throw new IOException(String.format("The binary secret '%s' is not supported. Please change its value to a string, or alternatively delete it.", result.getName())); + } + + return Optional.ofNullable(result.getSecretString()); + } catch (ResourceNotFoundException e) { + // Recoverable errors + LOG.info(e.getMessage()); + return Optional.empty(); + } catch (AWSSecretsManagerException e) { + // Unrecoverable errors + throw new IOException(e); + } + } + + @Override + public void init() { + try { + client = createClient(); + } catch (SdkClientException e) { + LOG.log(Level.WARNING, "Could not set up AWS Secrets Manager client. Reason: {0}", e.getMessage()); + } + } + + private static AWSSecretsManager createClient() throws SdkClientException { + final PluginConfiguration config = PluginConfiguration.getInstance(); + final EndpointConfiguration ec = config.getEndpointConfiguration(); + + final AWSSecretsManagerClientBuilder builder = AWSSecretsManagerClient.builder(); + + final Optional maybeServiceEndpoint = getServiceEndpoint(ec); + final Optional maybeSigningRegion = getSigningRegion(ec); + + if (maybeServiceEndpoint.isPresent() && maybeSigningRegion.isPresent()) { + LOG.log(Level.CONFIG, "Custom Endpoint Configuration: {0}", ec); + + final AwsClientBuilder.EndpointConfiguration endpointConfiguration = + new AwsClientBuilder.EndpointConfiguration(maybeServiceEndpoint.get(), maybeSigningRegion.get()); + builder.setEndpointConfiguration(endpointConfiguration); + } else { + LOG.log(Level.CONFIG, "Default Endpoint Configuration"); + } + + return builder.build(); + } + + private static Optional getServiceEndpoint(EndpointConfiguration ec) { + if ((ec != null) && (ec.getServiceEndpoint() != null)) { + return Optional.of(ec.getServiceEndpoint()); + } else { + return Optional.ofNullable(System.getenv(AWS_SERVICE_ENDPOINT)); + } + } + + private static Optional getSigningRegion(EndpointConfiguration ec) { + if ((ec != null) && (ec.getSigningRegion() != null)) { + return Optional.of(ec.getSigningRegion()); + } else { + return Optional.ofNullable(System.getenv(AWS_SIGNING_REGION)); + } + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/AbstractPluginIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/AbstractPluginIT.java deleted file mode 100644 index bf14eb20..00000000 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/AbstractPluginIT.java +++ /dev/null @@ -1,230 +0,0 @@ -package io.jenkins.plugins.credentials.secretsmanager; - -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.services.secretsmanager.AWSSecretsManager; -import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder; -import com.amazonaws.services.secretsmanager.model.DeleteSecretRequest; -import com.amazonaws.services.secretsmanager.model.ResourceNotFoundException; -import com.amazonaws.services.secretsmanager.model.RestoreSecretRequest; -import com.cloudbees.plugins.credentials.Credentials; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.CredentialsStore; -import com.cloudbees.plugins.credentials.common.IdCredentials; -import com.cloudbees.plugins.credentials.common.StandardCredentials; - -import io.jenkins.plugins.credentials.secretsmanager.factory.Tags; -import io.jenkins.plugins.credentials.secretsmanager.factory.Type; -import io.jenkins.plugins.credentials.secretsmanager.util.Maps; -import org.apache.commons.lang3.SerializationUtils; -import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; -import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.jenkinsci.plugins.workflow.job.WorkflowRun; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.jvnet.hudson.test.JenkinsRule; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -import hudson.model.queue.QueueTaskFuture; -import hudson.security.ACL; -import hudson.util.ListBoxModel; -import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation.Result; - -public abstract class AbstractPluginIT { - - // TODO use a unique name - private static final String BAR = "bar"; - - // TODO use a unique name - private static final String FOO = "foo"; - - private final AWSSecretsManager client = AWSSecretsManagerClientBuilder.standard() - .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://localhost:4584", "us-east-1")) - .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials("test", "test"))) - .build(); - - @Rule - public JenkinsRule r = new JenkinsConfiguredWithCodeRule(); - - private CredentialsStore store; - - @BeforeClass - public static void fakeAwsCredentials() { - System.setProperty("aws.accessKeyId", "test"); - System.setProperty("aws.secretKey", "test"); - } - - @Before - public void setup() { - store = CredentialsProvider.lookupStores(r.jenkins).iterator().next(); - - for (String secretId: Arrays.asList(FOO, BAR)) { - restoreSecret(secretId); - forceDeleteSecret(secretId); - } - } - - WorkflowRun runPipeline(String definition) { - try { - final WorkflowJob project = r.jenkins.createProject(WorkflowJob.class, "example"); - project.setDefinition(new CpsFlowDefinition(definition, true)); - final QueueTaskFuture workflowRunFuture = project.scheduleBuild2(0); - final WorkflowRun workflowRun = workflowRunFuture.waitForStart(); - r.waitForCompletion(workflowRun); - - return workflowRun; - } catch (IOException | InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - } - - List lookupCredentials(Class type) { - return CredentialsProvider.lookupCredentials(type, r.jenkins, ACL.SYSTEM, Collections.emptyList()); - } - - ListBoxModel listCredentials(Class type) { - return CredentialsProvider.listCredentials(type, r.jenkins, null, null, null); - } - - List lookupCredentialNames(Class type) { - final ListBoxModel result = CredentialsProvider.listCredentials(type, r.jenkins, ACL.SYSTEM, null, null); - - return result.stream() - .map(o -> o.name) - .collect(Collectors.toList()); - } - - Result createStringSecret(String secretString) { - final CreateSecretOperation create = new CreateSecretOperation(client); - - return create.run(FOO, secretString, opts -> { - opts.tags = Collections.singletonMap(Tags.type, Type.string); - }); - } - - Result createOtherStringSecret(String secretString) { - final CreateSecretOperation create = new CreateSecretOperation(client); - return create.run(BAR, secretString, opts -> { - opts.tags = Collections.singletonMap(Tags.type, Type.string); - }); - } - - Result createUsernamePasswordSecret(String username, String password) { - final CreateSecretOperation create = new CreateSecretOperation(client); - - return create.run(FOO, password, opts -> { - opts.tags = Maps.of( - Tags.type, Type.usernamePassword, - Tags.username, username); - }); - } - - Result createSshUserPrivateKeySecret(String username, String privateKey) { - final CreateSecretOperation create = new CreateSecretOperation(client); - - return create.run(FOO, privateKey, opts -> { - opts.tags = Maps.of( - Tags.type, Type.sshUserPrivateKey, - Tags.username, username); - }); - } - - Result createCertificateSecret(byte[] secretBinary) { - final CreateSecretOperation create = new CreateSecretOperation(client); - return create.run(FOO, secretBinary, opts -> { - opts.tags = Collections.singletonMap(Tags.type, Type.certificate); - }); - } - - Result createFileSecret(byte[] content) { - final CreateSecretOperation create = new CreateSecretOperation(client); - - return create.run(FOO, content, opts -> { - opts.tags = Collections.singletonMap(Tags.type, Type.file); - }); - } - - Result createFileSecret(String filename, byte[] content) { - final CreateSecretOperation create = new CreateSecretOperation(client); - - return create.run(FOO, content, opts -> { - opts.tags = Maps.of( - Tags.type, Type.file, - Tags.filename, filename); - }); - } - - /** - * Low-level API to create any kind of string secret. Warning: YOU MUST SUPPLY YOUR OWN TYPE TAG! - */ - Result createSecret(String secretString, Consumer opts) { - final CreateSecretOperation create = new CreateSecretOperation(client); - return create.run(FOO, secretString, opts); - } - - /** - * Low-level API to create any kind of string secret. Warning: YOU MUST SUPPLY YOUR OWN TYPE TAG! - */ - Result createOtherSecret(String secretString, Consumer opts) { - final CreateSecretOperation create = new CreateSecretOperation(client); - return create.run(BAR, secretString, opts); - } - - void deleteSecret(String secretId) { - final DeleteSecretRequest request = new DeleteSecretRequest() - .withSecretId(secretId); - - try { - client.deleteSecret(request); - } catch (ResourceNotFoundException e) { - // Don't care - } - } - - CredentialsStore store() { - return this.store; - } - - private void forceDeleteSecret(String secretId) { - final DeleteSecretRequest request = new DeleteSecretRequest() - .withSecretId(secretId) - .withForceDeleteWithoutRecovery(true); - - try { - client.deleteSecret(request); - } catch (ResourceNotFoundException e) { - // Don't care - } - } - - private void restoreSecret(String secretId) { - final RestoreSecretRequest request = new RestoreSecretRequest().withSecretId(secretId); - try { - client.restoreSecret(request); - } catch (ResourceNotFoundException e) { - // Don't care - } - } - - C lookupCredential(Class type, String id) { - return lookupCredentials(type).stream() - .filter(c -> c.getId().equals(id)) - .findFirst() - .orElseThrow(() -> new RuntimeException("Expected a credential but none was present")); - } - - static C snapshot(C credentials) { - return SerializationUtils.deserialize(SerializationUtils.serialize(CredentialsProvider.snapshot(credentials))); - } -} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/CredentialsProviderIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/CredentialsProviderIT.java index 5e021b64..31d3ceb0 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/CredentialsProviderIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/CredentialsProviderIT.java @@ -1,41 +1,59 @@ package io.jenkins.plugins.credentials.secretsmanager; +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.CreateSecretResult; +import com.amazonaws.services.secretsmanager.model.DeleteSecretRequest; +import com.amazonaws.services.secretsmanager.model.Tag; import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.CredentialsStore; import com.cloudbees.plugins.credentials.CredentialsUnavailableException; +import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.domains.Domain; - -import io.jenkins.plugins.credentials.secretsmanager.factory.Tags; +import hudson.util.ListBoxModel; +import hudson.util.Secret; +import io.jenkins.plugins.casc.misc.ConfiguredWithCode; import io.jenkins.plugins.credentials.secretsmanager.factory.Type; -import io.jenkins.plugins.credentials.secretsmanager.util.Maps; +import io.jenkins.plugins.credentials.secretsmanager.util.*; import org.jenkinsci.plugins.plaincredentials.StringCredentials; import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; -import java.util.Collections; import java.util.List; -import hudson.util.Secret; -import io.jenkins.plugins.casc.misc.ConfiguredWithCode; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation.Result; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.tuple; +import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.SoftAssertions.assertSoftly; /** * The plugin should support CredentialsProvider usage to list available credentials. */ -public class CredentialsProviderIT extends AbstractPluginIT { +public class CredentialsProviderIT { private static final String SECRET = "supersecret"; + public final MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule(); + public final AWSSecretsManagerRule secretsManager = new AutoErasingAWSSecretsManagerRule(); + + @Rule + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins) + .around(secretsManager); + + private CredentialsStore store; + + @Before + public void setupStore() { + store = jenkins.getCredentials().lookupStores().iterator().next(); + } + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldStartEmpty() { // When - final List credentials = lookupCredentials(StringCredentials.class); + final List credentials = lookup(StringCredentials.class); // Then assertThat(credentials).isEmpty(); @@ -45,7 +63,7 @@ public void shouldStartEmpty() { @ConfiguredWithCode(value = "/default.yml") public void shouldFailGracefullyWhenSecretsManagerUnavailable() { // When - final List credentials = lookupCredentials(StringCredentials.class); + final List credentials = lookup(StringCredentials.class); // Then assertThat(credentials).isEmpty(); @@ -55,25 +73,27 @@ public void shouldFailGracefullyWhenSecretsManagerUnavailable() { @ConfiguredWithCode(value = "/integration.yml") public void shouldUseSecretNameAsCredentialName() { // Given - final Result foo = createStringSecret(SECRET); + final CreateSecretResult foo = createStringSecret(SECRET); // When - final List credentialNames = lookupCredentialNames(StringCredentials.class); + final ListBoxModel credentialNames = jenkins.getCredentials().list(StringCredentials.class); // Then - assertThat(credentialNames).containsOnly(foo.getName()); + assertThat(credentialNames) + .extracting("name") + .containsOnly(foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldTolerateDeletedCredentials() { // Given - final CreateSecretOperation.Result foo = createStringSecret(SECRET); - final CreateSecretOperation.Result bar = createOtherStringSecret(SECRET); + final CreateSecretResult foo = createStringSecret(SECRET); + final CreateSecretResult bar = createStringSecret(SECRET); // When deleteSecret(bar.getName()); - final List credentials = lookupCredentials(StringCredentials.class); + final List credentials = lookup(StringCredentials.class); // Then assertThat(credentials) @@ -85,11 +105,11 @@ public void shouldTolerateDeletedCredentials() { @ConfiguredWithCode(value = "/integration.yml") public void shouldTolerateRecentlyDeletedCredentials() { // Given - final CreateSecretOperation.Result foo = createStringSecret(SECRET); - final CreateSecretOperation.Result bar = createOtherStringSecret(SECRET); + final CreateSecretResult foo = createStringSecret(SECRET); + final CreateSecretResult bar = createStringSecret(SECRET); // When - final List credentials = lookupCredentials(StringCredentials.class); + final List credentials = lookup(StringCredentials.class); deleteSecret(bar.getName()); // Then @@ -106,13 +126,11 @@ public void shouldTolerateRecentlyDeletedCredentials() { @ConfiguredWithCode(value = "/integration.yml") public void shouldIgnoreUntaggedSecrets() { // Given - final CreateSecretOperation.Result foo = createStringSecret(SECRET); - final CreateSecretOperation.Result bar = createOtherSecret(SECRET, opts -> { - opts.tags = Collections.emptyMap(); - }); + final CreateSecretResult foo = createStringSecret(SECRET); + final CreateSecretResult bar = createSecret(SECRET, Lists.of()); // When - final List credentials = lookupCredentials(StringCredentials.class); + final List credentials = lookup(StringCredentials.class); // Then assertThat(credentials) @@ -124,16 +142,16 @@ public void shouldIgnoreUntaggedSecrets() { @ConfiguredWithCode(value = "/integration.yml") public void shouldTolerateUnrelatedTags() { // Given - final CreateSecretOperation.Result foo = createSecret(SECRET, opts -> { - opts.tags = Maps.of( - Tags.type, Type.string, - "foo", "bar", - null, "baz", - "qux", null); - }); + final List tags = Lists.of( + AwsTags.type(Type.string), + AwsTags.tag("foo", "bar"), + AwsTags.tag(null, "baz"), + AwsTags.tag("qux", null)); + + final CreateSecretResult foo = createSecret(SECRET, tags); // When - final List credentials = lookupCredentials(StringCredentials.class); + final List credentials = lookup(StringCredentials.class); // Then assertThat(credentials) @@ -147,7 +165,7 @@ public void shouldNotSupportUpdates() { final StringCredentialsImpl credential = new StringCredentialsImpl(CredentialsScope.GLOBAL,"foo", "desc", Secret.fromString(SECRET)); assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> store().updateCredentials(Domain.global(), credential, credential)) + .isThrownBy(() -> store.updateCredentials(Domain.global(), credential, credential)) .withMessage("Jenkins may not update credentials in AWS Secrets Manager"); } @@ -155,7 +173,7 @@ public void shouldNotSupportUpdates() { @ConfiguredWithCode(value = "/integration.yml") public void shouldNotSupportInserts() { assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> store().addCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, "foo", "desc", Secret.fromString(SECRET)))) + .isThrownBy(() -> store.addCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, "foo", "desc", Secret.fromString(SECRET)))) .withMessage("Jenkins may not add credentials to AWS Secrets Manager"); } @@ -163,7 +181,31 @@ public void shouldNotSupportInserts() { @ConfiguredWithCode(value = "/integration.yml") public void shouldNotSupportDeletes() { assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> store().removeCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, "foo", "desc", Secret.fromString(SECRET)))) + .isThrownBy(() -> store.removeCredentials(Domain.global(), new StringCredentialsImpl(CredentialsScope.GLOBAL, "foo", "desc", Secret.fromString(SECRET)))) .withMessage("Jenkins may not remove credentials from AWS Secrets Manager"); } + + private List lookup(Class type) { + return jenkins.getCredentials().lookup(type); + } + + private void deleteSecret(String secretId) { + final DeleteSecretRequest request = new DeleteSecretRequest().withSecretId(secretId); + secretsManager.getClient().deleteSecret(request); + } + + private CreateSecretResult createStringSecret(String secretString) { + final List tags = Lists.of(AwsTags.type(Type.string)); + + return createSecret(secretString, tags); + } + + private CreateSecretResult createSecret(String secretString, List tags) { + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretString(secretString) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } } diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/FileCredentialsIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/FileCredentialsIT.java index 70c309fb..cb3b29df 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/FileCredentialsIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/FileCredentialsIT.java @@ -1,127 +1,143 @@ package io.jenkins.plugins.credentials.secretsmanager; +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.CreateSecretResult; +import com.amazonaws.services.secretsmanager.model.Tag; import com.cloudbees.plugins.credentials.SecretBytes; -import com.google.common.io.ByteStreams; +import com.cloudbees.plugins.credentials.common.StandardCredentials; import hudson.util.ListBoxModel; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation; -import io.jenkins.plugins.credentials.secretsmanager.util.Strings; -import io.jenkins.plugins.credentials.secretsmanager.util.assertions.WorkflowRunAssert; +import io.jenkins.plugins.credentials.secretsmanager.factory.Type; +import io.jenkins.plugins.credentials.secretsmanager.util.*; import org.jenkinsci.plugins.plaincredentials.FileCredentials; import org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.List; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; -import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static io.jenkins.plugins.credentials.secretsmanager.util.assertions.CustomAssertions.assertThat; /** * The plugin should support secret file credentials. */ -public class FileCredentialsIT extends AbstractPluginIT implements CredentialsTests { +public class FileCredentialsIT implements CredentialsTests { private static final String FILENAME = "hello.txt"; private static final byte[] CONTENT = {0x01, 0x02, 0x03}; + public MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule(); + public final AWSSecretsManagerRule secretsManager = new AutoErasingAWSSecretsManagerRule(); + + @Rule + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins) + .around(secretsManager); + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveId() { // Given - final CreateSecretOperation.Result foo = createFileSecret(CONTENT); + final CreateSecretResult foo = createFileSecret(CONTENT); // When - final FileCredentials credential = lookupCredential(FileCredentials.class, foo.getName()); + final FileCredentials credential = lookup(FileCredentials.class, foo.getName()); // Then - assertThat(credential.getId()).isEqualTo(foo.getName()); + assertThat(credential) + .hasId(foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveFileName() { // Given - final CreateSecretOperation.Result foo = createFileSecret(CONTENT); + final CreateSecretResult foo = createFileSecret(CONTENT); // When - final FileCredentials credential = lookupCredential(FileCredentials.class, foo.getName()); + final FileCredentials credential = lookup(FileCredentials.class, foo.getName()); // Then - assertThat(credential.getFileName()).isEqualTo(foo.getName()); + assertThat(credential) + .hasFileName(foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveCustomisableFileName() { // Given - final CreateSecretOperation.Result foo = createFileSecret(FILENAME, CONTENT); + final CreateSecretResult foo = createFileSecret(CONTENT, FILENAME); // When - final FileCredentials credential = lookupCredential(FileCredentials.class, foo.getName()); + final FileCredentials credential = lookup(FileCredentials.class, foo.getName()); // Then - assertThat(credential.getFileName()).isEqualTo(FILENAME); + assertThat(credential) + .hasFileName(FILENAME); } @Test @ConfiguredWithCode(value = "/integration.yml") - public void shouldHaveContent() throws IOException { + public void shouldHaveContent() { // Given - final CreateSecretOperation.Result foo = createFileSecret(CONTENT); + final CreateSecretResult foo = createFileSecret(CONTENT); // When - final FileCredentials credential = lookupCredential(FileCredentials.class, foo.getName()); + final FileCredentials credential = lookup(FileCredentials.class, foo.getName()); // Then - assertThat(ByteStreams.toByteArray(credential.getContent())).isEqualTo(CONTENT); + assertThat(credential) + .hasContent(CONTENT); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveDescriptorIcon() { - final CreateSecretOperation.Result foo = createFileSecret(CONTENT); - final FileCredentials ours = lookupCredential(FileCredentials.class, foo.getName()); + final CreateSecretResult foo = createFileSecret(CONTENT); + final FileCredentials ours = lookup(FileCredentials.class, foo.getName()); final FileCredentials theirs = new FileCredentialsImpl(null, "id", "description", "filename", SecretBytes.fromBytes(CONTENT)); - assertThat(ours.getDescriptor().getIconClassName()) - .isEqualTo(theirs.getDescriptor().getIconClassName()); + assertThat(ours) + .hasSameDescriptorIconAs(theirs); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportListView() { // Given - final CreateSecretOperation.Result foo = createFileSecret(CONTENT); + final CreateSecretResult foo = createFileSecret(CONTENT); // When - final ListBoxModel list = listCredentials(FileCredentials.class); + final ListBoxModel list = jenkins.getCredentials().list(FileCredentials.class); // Then assertThat(list) - .extracting("name", "value") - .containsOnly(tuple(foo.getName(), foo.getName())); + .containsOption(foo.getName(), foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportWithCredentialsBinding() { // Given - final CreateSecretOperation.Result foo = createFileSecret(CONTENT); + final CreateSecretResult foo = createFileSecret(CONTENT); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = runPipeline("", "node {", " withCredentials([file(credentialsId: '" + foo.getName() + "', variable: 'FILE')]) {", " echo \"Credential: {fileName: $FILE}\"", " }", - "}")); + "}"); // Then - WorkflowRunAssert.assertThat(run) + assertThat(run) .hasResult(hudson.model.Result.SUCCESS) .hasLogContaining("Credential: {fileName: ****}"); } @@ -130,10 +146,10 @@ public void shouldSupportWithCredentialsBinding() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportEnvironmentBinding() { // Given - final CreateSecretOperation.Result foo = createFileSecret(CONTENT); + final CreateSecretResult foo = createFileSecret(CONTENT); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = runPipeline("", "pipeline {", " agent any", " stages {", @@ -146,10 +162,10 @@ public void shouldSupportEnvironmentBinding() { " }", " }", " }", - "}")); + "}"); // Then - WorkflowRunAssert.assertThat(run) + assertThat(run) .hasResult(hudson.model.Result.SUCCESS) .hasLogContaining("{filename: ****}"); } @@ -158,18 +174,17 @@ public void shouldSupportEnvironmentBinding() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportSnapshots() { // Given - final CreateSecretOperation.Result foo = createFileSecret(CONTENT); - final FileCredentials before = lookupCredential(FileCredentials.class, foo.getName()); + final CreateSecretResult foo = createFileSecret(CONTENT); + final FileCredentials before = lookup(FileCredentials.class, foo.getName()); // When - final FileCredentials after = snapshot(before); + final FileCredentials after = CredentialSnapshots.snapshot(before); // Then - assertSoftly(s -> { - s.assertThat(after.getId()).as("ID").isEqualTo(before.getId()); - s.assertThat(after.getFileName()).as("Filename").isEqualTo(before.getFileName()); - s.assertThat(getContent(after)).as("Content").hasSameContentAs(getContent(before)); - }); + assertThat(after) + .hasFileName(before.getFileName()) + .hasContent(getContent(before)) + .hasId(before.getId()); } private static InputStream getContent(FileCredentials credentials) { @@ -179,4 +194,33 @@ private static InputStream getContent(FileCredentials credentials) { throw new RuntimeException(e); } } + + private CreateSecretResult createFileSecret(byte[] content) { + final List tags = Lists.of(AwsTags.type(Type.file)); + + return createSecret(content,tags); + } + + private CreateSecretResult createFileSecret(byte[] content, String filename) { + final List tags = Lists.of(AwsTags.type(Type.file), AwsTags.filename(filename)); + + return createSecret(content, tags); + } + + private CreateSecretResult createSecret(byte[] content, List tags) { + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretBinary(ByteBuffer.wrap(content)) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } + + private C lookup(Class type, String id) { + return jenkins.getCredentials().lookup(type, id); + } + + private WorkflowRun runPipeline(String... pipeline) { + return jenkins.getPipelines().run(Strings.m(pipeline)); + } } diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/FiltersIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/FiltersIT.java index eafac8c2..2df9f71a 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/FiltersIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/FiltersIT.java @@ -1,41 +1,55 @@ package io.jenkins.plugins.credentials.secretsmanager; +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.CreateSecretResult; +import com.amazonaws.services.secretsmanager.model.Tag; import hudson.util.Secret; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; -import io.jenkins.plugins.credentials.secretsmanager.factory.Tags; import io.jenkins.plugins.credentials.secretsmanager.factory.Type; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation; -import io.jenkins.plugins.credentials.secretsmanager.util.Maps; +import io.jenkins.plugins.credentials.secretsmanager.util.*; import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; -public class FiltersIT extends AbstractPluginIT { +public class FiltersIT { + + public final MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule(); + public final AWSSecretsManagerRule secretsManager = new AutoErasingAWSSecretsManagerRule(); + + @Rule + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins) + .around(secretsManager); + @Test @ConfiguredWithCode(value = "/tags.yml") public void shouldFilterByTag() { // Given - final CreateSecretOperation.Result foo = createSecret("supersecret", opts -> { - opts.tags = Maps.of( - Tags.type, Type.string, - "product", "roadrunner"); - }); - final CreateSecretOperation.Result bar = createOtherSecret("supersecret", opts -> { - opts.tags = Maps.of( - Tags.type, Type.string, - "product", "coyote"); - }); + final CreateSecretResult foo = createSecret("supersecret", Lists.of(AwsTags.type(Type.string), AwsTags.tag("product", "roadrunner"))); + final CreateSecretResult bar = createSecret("supersecret", Lists.of(AwsTags.type(Type.string), AwsTags.tag("product", "coyote"))); // When - final List credentials = lookupCredentials(StringCredentials.class); + final List credentials = jenkins.getCredentials().lookup(StringCredentials.class); // Then assertThat(credentials) .extracting("id", "secret") .containsOnly(tuple(foo.getName(), Secret.fromString("supersecret"))); } + + private CreateSecretResult createSecret(String secretString, List tags) { + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretString(secretString) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } } diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/GitPluginIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/GitPluginIT.java index ef1fcf59..9b7d7891 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/GitPluginIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/GitPluginIT.java @@ -1,21 +1,26 @@ package io.jenkins.plugins.credentials.secretsmanager; +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.CreateSecretResult; +import com.amazonaws.services.secretsmanager.model.Tag; import hudson.model.Label; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation; -import io.jenkins.plugins.credentials.secretsmanager.util.Crypto; -import io.jenkins.plugins.credentials.secretsmanager.util.Strings; -import io.jenkins.plugins.credentials.secretsmanager.util.assertions.WorkflowRunAssert; +import io.jenkins.plugins.credentials.secretsmanager.factory.Type; +import io.jenkins.plugins.credentials.secretsmanager.util.*; import io.jenkins.plugins.credentials.secretsmanager.util.git.GitHttpServer; import io.jenkins.plugins.credentials.secretsmanager.util.git.GitSshServer; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; +import org.junit.rules.RuleChain; import org.junit.runner.RunWith; import java.security.KeyPair; import java.util.Collections; +import java.util.List; + +import static io.jenkins.plugins.credentials.secretsmanager.util.assertions.CustomAssertions.*; /** * The credentials provider should work with the Git plugin, a key credentials consumer @@ -23,11 +28,20 @@ @RunWith(Enclosed.class) public class GitPluginIT { - public static class SSHUserPrivateKeyIT extends AbstractPluginIT { + public static class SSHUserPrivateKeyIT { private final String repo = "foo"; private final KeyPair sshKey = Crypto.newKeyPair(); private final String username = "joe"; + public final MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule(); + public final AWSSecretsManagerRule secretsManager = new AutoErasingAWSSecretsManagerRule(); + + @Rule + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins) + .around(secretsManager); + @Rule public final GitSshServer git = new GitSshServer.Builder() .withRepos(repo) @@ -38,25 +52,47 @@ public static class SSHUserPrivateKeyIT extends AbstractPluginIT { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportGitPlugin() throws Exception { final String slaveName = "agent"; - r.createSlave(Label.get(slaveName)); + jenkins.createSlave(Label.get(slaveName)); // Given - final CreateSecretOperation.Result foo = createSshUserPrivateKeySecret(username, Crypto.save(sshKey.getPrivate())); + final CreateSecretResult foo = createSshUserPrivateKeySecret(username, Crypto.save(sshKey.getPrivate())); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = jenkins.getPipelines().run(Strings.m("", "node('" + slaveName + "') {", " git url: '" + git.getCloneUrl(repo, username) + "', credentialsId: '" + foo.getName() + "', branch: 'master'", "}")); // Then - WorkflowRunAssert.assertThat(run) + assertThat(run) .hasResult(hudson.model.Result.SUCCESS) .hasLogContaining("Commit message: \"Initial commit\""); } + + private CreateSecretResult createSshUserPrivateKeySecret(String username, String privateKey) { + final List tags = Lists.of( + AwsTags.type(Type.sshUserPrivateKey), + AwsTags.username(username)); + + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretString(privateKey) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } } - public static class StandardUsernamePasswordCredentialsIT extends AbstractPluginIT { + public static class StandardUsernamePasswordCredentialsIT { + + public final MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule(); + public final AWSSecretsManagerRule secretsManager = new AutoErasingAWSSecretsManagerRule(); + + @Rule + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins) + .around(secretsManager); @Rule public final GitHttpServer git = new GitHttpServer(); @@ -65,23 +101,35 @@ public static class StandardUsernamePasswordCredentialsIT extends AbstractPlugin @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportGitPlugin() throws Exception { final String slaveName = "agent"; - r.createSlave(Label.get(slaveName)); + jenkins.createSlave(Label.get(slaveName)); // Given - final CreateSecretOperation.Result foo = createUsernamePasswordSecret("agitter", "letmein"); + final CreateSecretResult foo = createUsernamePasswordSecret("agitter", "letmein"); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = jenkins.getPipelines().run(Strings.m("", "node('" + slaveName + "') {", " git url: '" + git.getCloneUrl() + "', credentialsId: '" + foo.getName() + "', branch: 'master'", "}")); // Then - WorkflowRunAssert.assertThat(run) + assertThat(run) .hasResult(hudson.model.Result.SUCCESS) .hasLogContaining("Commit message: \"Initial commit\""); } + private CreateSecretResult createUsernamePasswordSecret(String username, String password) { + final List tags = Lists.of( + AwsTags.type(Type.usernamePassword), + AwsTags.username(username)); + + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretString(password) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } } } diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SSHUserPrivateKeyIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SSHUserPrivateKeyIT.java index 2caf8011..9e38d06c 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SSHUserPrivateKeyIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SSHUserPrivateKeyIT.java @@ -1,125 +1,139 @@ package io.jenkins.plugins.credentials.secretsmanager; +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.CreateSecretResult; +import com.amazonaws.services.secretsmanager.model.Tag; import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; +import com.cloudbees.plugins.credentials.common.StandardCredentials; import hudson.util.ListBoxModel; -import hudson.util.Secret; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation; -import io.jenkins.plugins.credentials.secretsmanager.util.Crypto; -import io.jenkins.plugins.credentials.secretsmanager.util.Strings; -import io.jenkins.plugins.credentials.secretsmanager.util.assertions.WorkflowRunAssert; +import io.jenkins.plugins.credentials.secretsmanager.factory.Type; +import io.jenkins.plugins.credentials.secretsmanager.util.*; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; -import static org.assertj.core.api.SoftAssertions.assertSoftly; +import java.util.List; + +import static io.jenkins.plugins.credentials.secretsmanager.util.assertions.CustomAssertions.assertThat; /** * The plugin should support SSH private key credentials. */ -public class SSHUserPrivateKeyIT extends AbstractPluginIT implements CredentialsTests { +public class SSHUserPrivateKeyIT implements CredentialsTests { - private static final Secret EMPTY_PASSPHRASE = Secret.fromString(""); private static final String PRIVATE_KEY = Crypto.newPrivateKey(); private static final String USERNAME = "joe"; + public final MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule(); + public final AWSSecretsManagerRule secretsManager = new AutoErasingAWSSecretsManagerRule(); + + @Rule + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins) + .around(secretsManager); + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportListView() { // Given - final CreateSecretOperation.Result foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); // When - final ListBoxModel list = listCredentials(SSHUserPrivateKey.class); + final ListBoxModel list = jenkins.getCredentials().list(SSHUserPrivateKey.class); // Then assertThat(list) - .extracting("name", "value") - .containsOnly(tuple(USERNAME, foo.getName())); + .containsOption(USERNAME, foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveId() { // Given - final CreateSecretOperation.Result foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); // When - final SSHUserPrivateKey credential = lookupCredential(SSHUserPrivateKey.class, foo.getName()); + final SSHUserPrivateKey credential = lookup(SSHUserPrivateKey.class, foo.getName()); // Then - assertThat(credential.getId()).isEqualTo(foo.getName()); + assertThat(credential) + .hasId(foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveUsername() { // Given - final CreateSecretOperation.Result foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); // When - final SSHUserPrivateKey credential = lookupCredential(SSHUserPrivateKey.class, foo.getName()); + final SSHUserPrivateKey credential = lookup(SSHUserPrivateKey.class, foo.getName()); // Then - assertThat(credential.getUsername()).isEqualTo(USERNAME); + assertThat(credential) + .hasUsername(USERNAME); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHavePrivateKey() { // Given - final CreateSecretOperation.Result foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); // When - final SSHUserPrivateKey credential = lookupCredential(SSHUserPrivateKey.class, foo.getName()); + final SSHUserPrivateKey credential = lookup(SSHUserPrivateKey.class, foo.getName()); // Then - assertThat(credential.getPrivateKeys()).containsOnly(PRIVATE_KEY); + assertThat(credential) + .hasPrivateKeys(Lists.of(PRIVATE_KEY)); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveEmptyPassphrase() { // Given - final CreateSecretOperation.Result foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); // When - final SSHUserPrivateKey credential = lookupCredential(SSHUserPrivateKey.class, foo.getName()); + final SSHUserPrivateKey credential = lookup(SSHUserPrivateKey.class, foo.getName()); // Then - assertThat(credential.getPassphrase()).isEqualTo(EMPTY_PASSPHRASE); + assertThat(credential) + .doesNotHavePassphrase(); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveDescriptorIcon() { - final CreateSecretOperation.Result foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); - final SSHUserPrivateKey ours = lookupCredential(SSHUserPrivateKey.class, foo.getName()); + final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final SSHUserPrivateKey ours = lookup(SSHUserPrivateKey.class, foo.getName()); final BasicSSHUserPrivateKey theirs = new BasicSSHUserPrivateKey(null, "id", "username", null, "passphrase", "description"); - assertThat(ours.getDescriptor().getIconClassName()) - .isEqualTo(theirs.getDescriptor().getIconClassName()); + assertThat(ours) + .hasSameDescriptorIconAs(theirs); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportWithCredentialsBinding() { // Given - final CreateSecretOperation.Result foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = runPipeline("", "node {", " withCredentials([sshUserPrivateKey(credentialsId: '" + foo.getName() + "', keyFileVariable: 'KEYFILE', usernameVariable: 'USERNAME')]) {", " echo \"Credential: {username: $USERNAME, keyFile: $KEYFILE}\"", " }", - "}")); + "}"); // Then - WorkflowRunAssert.assertThat(run) + assertThat(run) .hasResult(hudson.model.Result.SUCCESS) .hasLogContaining("Credential: {username: ****, keyFile: ****}"); } @@ -128,10 +142,10 @@ public void shouldSupportWithCredentialsBinding() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportEnvironmentBinding() { // Given - final CreateSecretOperation.Result foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = runPipeline("", "pipeline {", " agent any", " stages {", @@ -144,10 +158,10 @@ public void shouldSupportEnvironmentBinding() { " }", " }", " }", - "}")); + "}"); // Then - WorkflowRunAssert.assertThat(run) + assertThat(run) .hasResult(hudson.model.Result.SUCCESS) .hasLogContaining("{variable: ****, username: ****}"); } @@ -156,18 +170,38 @@ public void shouldSupportEnvironmentBinding() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportSnapshots() { // Given - final CreateSecretOperation.Result foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); - final SSHUserPrivateKey before = lookupCredential(SSHUserPrivateKey.class, foo.getName()); + final CreateSecretResult foo = createSshUserPrivateKeySecret(USERNAME, PRIVATE_KEY); + final SSHUserPrivateKey before = lookup(SSHUserPrivateKey.class, foo.getName()); // When - final SSHUserPrivateKey after = snapshot(before); + final SSHUserPrivateKey after = CredentialSnapshots.snapshot(before); // Then - assertSoftly(s -> { - s.assertThat(after.getId()).as("ID").isEqualTo(before.getId()); - s.assertThat(after.getUsername()).as("Username").isEqualTo(before.getUsername()); - s.assertThat(after.getPassphrase()).as("Passphrase").isEqualTo(before.getPassphrase()); - s.assertThat(after.getPrivateKeys()).as("Private Key").isEqualTo(before.getPrivateKeys()); - }); + assertThat(after) + .hasUsername(before.getUsername()) + .hasPassphrase(before.getPassphrase()) + .hasPrivateKeys(before.getPrivateKeys()) + .hasId(before.getId()); + } + + private CreateSecretResult createSshUserPrivateKeySecret(String username, String privateKey) { + final List tags = Lists.of( + AwsTags.type(Type.sshUserPrivateKey), + AwsTags.username(username)); + + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretString(privateKey) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } + + private C lookup(Class type, String id) { + return jenkins.getCredentials().lookup(type, id); + } + + private WorkflowRun runPipeline(String... pipeline) { + return jenkins.getPipelines().run(Strings.m(pipeline)); } } diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SecretSourceIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SecretSourceIT.java new file mode 100644 index 00000000..3c886e01 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/SecretSourceIT.java @@ -0,0 +1,181 @@ +package io.jenkins.plugins.credentials.secretsmanager; + +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.CreateSecretResult; +import com.amazonaws.services.secretsmanager.model.DeleteSecretRequest; +import com.amazonaws.services.secretsmanager.model.Tag; +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import io.jenkins.plugins.casc.ConfigurationContext; +import io.jenkins.plugins.casc.ConfiguratorRegistry; +import io.jenkins.plugins.casc.SecretSourceResolver; +import io.jenkins.plugins.casc.misc.ConfiguredWithCode; +import io.jenkins.plugins.casc.misc.EnvVarsRule; +import io.jenkins.plugins.credentials.secretsmanager.factory.Type; +import io.jenkins.plugins.credentials.secretsmanager.util.*; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; + +import java.nio.ByteBuffer; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +public class SecretSourceIT { + + private static final String SECRET_STRING = "supersecret"; + private static final byte[] SECRET_BINARY = {0x01, 0x02, 0x03}; + + public final AWSSecretsManagerRule secretsManager = new AutoErasingAWSSecretsManagerRule(); + public final MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule(); + + @Rule + public final RuleChain chain = RuleChain + .outerRule(new EnvVarsRule() + .set("AWS_ACCESS_KEY_ID", "fake") + .set("AWS_SECRET_ACCESS_KEY", "fake") + // Invent 2 environment variables which don't technically exist in AWS SDK + .set("AWS_SERVICE_ENDPOINT", "http://localhost:4584") + .set("AWS_SIGNING_REGION", "us-east-1")) + .around(jenkins) + .around(secretsManager); + + private ConfigurationContext context; + + @Before + public void refreshConfigurationContext() { + final ConfiguratorRegistry registry = ConfiguratorRegistry.get(); + context = new ConfigurationContext(registry); + } + + /** + * Note: When Secrets Manager is unavailable, the AWS SDK treats this the same as '404 not found'. + */ + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldReturnEmptyWhenSecretWasNotFound() { + // When + final String secret = revealSecret("foo"); + + // Then + assertThat(secret).isEmpty(); + } + + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldRevealSecret() { + // Given + final CreateSecretResult foo = createSecret(SECRET_STRING, Lists.of()); + + // When + final String secret = revealSecret(foo.getName()); + + // Then + assertThat(secret).isEqualTo(SECRET_STRING); + } + + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldThrowExceptionWhenSecretWasSoftDeleted() { + final CreateSecretResult foo = createSecret(SECRET_STRING, Lists.of()); + deleteSecret(foo.getName()); + + assertThatIOException() + .isThrownBy(() -> revealSecret(foo.getName())); + } + + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldThrowExceptionWhenSecretWasBinary() { + final CreateSecretResult foo = createSecret(SECRET_BINARY, Lists.of()); + + assertThatIOException() + .isThrownBy(() -> revealSecret(foo.getName())); + } + + @Test + @ConfiguredWithCode(value = "/tags.yml") + public void shouldIgnoreFilters() { + // Given + final CreateSecretResult foo = createSecret(SECRET_STRING, Lists.of(AwsTags.tag("wrong", "tag"))); + + // When + final String secret = revealSecret(foo.getName()); + + // Then + assertThat(secret).isEqualTo(SECRET_STRING); + } + + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldRevealSecretsOutsideCredentialsProvider() { + final CreateSecretResult foo = createSecret(SECRET_STRING, Lists.of()); + + assertSoftly(s -> { + s.assertThat(revealSecret(foo.getName())).as("SecretSource").isEqualTo(SECRET_STRING); + s.assertThat(lookupCredentials(StandardCredentials.class)).as("CredentialsProvider").isEmpty(); + }); + } + + @Test + @ConfiguredWithCode(value = "/integration.yml") + public void shouldRevealSecretsInsideCredentialsProvider() { + final CreateSecretResult foo = createStringSecret(SECRET_STRING); + + assertSoftly(s -> { + s.assertThat(revealSecret(foo.getName())).as("SecretSource").isEqualTo(SECRET_STRING); + s.assertThat(lookupCredential(StringCredentials.class, foo.getName()).getSecret().getPlainText()).as("CredentialsProvider").isEqualTo(SECRET_STRING); + }); + } + + private C lookupCredential(Class type, String id) { + return jenkins.getCredentials().lookup(type, id); + } + + private List lookupCredentials(Class type) { + return jenkins.getCredentials().lookup(type); + } + + private CreateSecretResult createStringSecret(String secretString) { + final List tags = Lists.of(AwsTags.type(Type.string)); + + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretString(secretString) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } + + private CreateSecretResult createSecret(String secretString, List tags) { + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretString(secretString) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } + + private CreateSecretResult createSecret(byte[] secretBinary, List tags) { + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretBinary(ByteBuffer.wrap(secretBinary)) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } + + private void deleteSecret(String secretId) { + final DeleteSecretRequest request = new DeleteSecretRequest().withSecretId(secretId); + secretsManager.getClient().deleteSecret(request); + } + + private String revealSecret(String id) { + return SecretSourceResolver.resolve(context, "${" + id + "}"); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardCertificateCredentialsIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardCertificateCredentialsIT.java index fd89f594..1ffc483c 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardCertificateCredentialsIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardCertificateCredentialsIT.java @@ -1,129 +1,146 @@ package io.jenkins.plugins.credentials.secretsmanager; +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.CreateSecretResult; +import com.amazonaws.services.secretsmanager.model.Tag; import com.cloudbees.plugins.credentials.CredentialsUnavailableException; import com.cloudbees.plugins.credentials.SecretBytes; import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; +import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl; import hudson.util.ListBoxModel; -import hudson.util.Secret; +import hudson.model.Result; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation; -import io.jenkins.plugins.credentials.secretsmanager.util.Crypto; -import io.jenkins.plugins.credentials.secretsmanager.util.Strings; -import io.jenkins.plugins.credentials.secretsmanager.util.assertions.WorkflowRunAssert; +import io.jenkins.plugins.credentials.secretsmanager.factory.Type; +import io.jenkins.plugins.credentials.secretsmanager.util.*; +import io.jenkins.plugins.credentials.secretsmanager.util.assertions.CustomSoftAssertions; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; +import java.nio.ByteBuffer; import java.security.KeyPair; import java.security.KeyStore; import java.security.cert.Certificate; -import java.util.Collections; +import java.util.List; + +import static io.jenkins.plugins.credentials.secretsmanager.util.assertions.CustomAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static io.jenkins.plugins.credentials.secretsmanager.util.Crypto.keystoreToMap; -import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.SoftAssertions.assertSoftly; /** * The plugin should support Certificate credentials. */ -public class StandardCertificateCredentialsIT extends AbstractPluginIT implements CredentialsTests { +public class StandardCertificateCredentialsIT implements CredentialsTests { private static final String ALIAS = "test"; - private static final Secret EMPTY_PASSPHRASE = Secret.fromString(""); private static final KeyPair KEY_PAIR = Crypto.newKeyPair(); private static final char[] PASSWORD = new char[]{}; private static final String CN = "CN=localhost"; - private static final Certificate CERT = Crypto.newSelfSignedCertificate(CN, KEY_PAIR); + private static final Certificate[] CERTIFICATE_CHAIN = { Crypto.newSelfSignedCertificate(CN, KEY_PAIR) }; + + public final MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule(); + public final AWSSecretsManagerRule secretsManager = new AutoErasingAWSSecretsManagerRule(); + + @Rule + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins) + .around(secretsManager); @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportListView() { // Given - final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, new Certificate[]{CERT}); - final CreateSecretOperation.Result foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); + final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, CERTIFICATE_CHAIN); + final CreateSecretResult foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); // When - final ListBoxModel list = listCredentials(StandardCertificateCredentials.class); + final ListBoxModel list = jenkins.getCredentials().list(StandardCertificateCredentials.class); // Then assertThat(list) - .extracting("name", "value") - .containsOnly(tuple(CN, foo.getName())); + .containsOption(CN, foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveDescriptorIcon() { - final byte[] keystore = Crypto.save(Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, new Certificate[]{CERT}), PASSWORD); - final CreateSecretOperation.Result foo = createCertificateSecret(keystore); - final StandardCertificateCredentials ours = lookupCredential(StandardCertificateCredentials.class, foo.getName()); + final byte[] keystore = Crypto.save(Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, CERTIFICATE_CHAIN), PASSWORD); + final CreateSecretResult foo = createCertificateSecret(keystore); + final StandardCertificateCredentials ours = lookup(StandardCertificateCredentials.class, foo.getName()); final StandardCertificateCredentials theirs = new CertificateCredentialsImpl(null, "id", "description", "password", new CertificateCredentialsImpl.UploadedKeyStoreSource(SecretBytes.fromBytes(keystore))); - assertThat(ours.getDescriptor().getIconClassName()).isEqualTo(theirs.getDescriptor().getIconClassName()); + assertThat(ours) + .hasSameDescriptorIconAs(theirs); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveId() { // Given - final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, new Certificate[]{CERT}); - final CreateSecretOperation.Result foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); + final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, CERTIFICATE_CHAIN); + final CreateSecretResult foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); // When - final StandardCertificateCredentials credential = lookupCredential(StandardCertificateCredentials.class, foo.getName()); + final StandardCertificateCredentials credential = lookup(StandardCertificateCredentials.class, foo.getName()); // Then - assertThat(credential.getId()).isEqualTo(foo.getName()); + assertThat(credential) + .hasId(foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveEmptyPassword() { // Given - final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, new Certificate[]{CERT}); - final CreateSecretOperation.Result foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); + final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, CERTIFICATE_CHAIN); + final CreateSecretResult foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); // When - final StandardCertificateCredentials credential = lookupCredential(StandardCertificateCredentials.class, foo.getName()); + final StandardCertificateCredentials credential = lookup(StandardCertificateCredentials.class, foo.getName()); // Then - assertThat(credential.getPassword()).isEqualTo(EMPTY_PASSPHRASE); + assertThat(credential) + .doesNotHavePassword(); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveKeystore() { // Given - final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, new Certificate[]{CERT}); - final CreateSecretOperation.Result foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); + final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, CERTIFICATE_CHAIN); + final CreateSecretResult foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); // When - final StandardCertificateCredentials credential = lookupCredential(StandardCertificateCredentials.class, foo.getName()); + final StandardCertificateCredentials credential = lookup(StandardCertificateCredentials.class, foo.getName()); // Then - assertThat(Crypto.keystoreToMap(credential.getKeyStore())).containsEntry(ALIAS, Collections.singletonList(CERT)); + assertThat(credential.getKeyStore()) + .containsEntry(ALIAS, CERTIFICATE_CHAIN); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportWithCredentialsBinding() { // Given - final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, new Certificate[]{CERT}); - final CreateSecretOperation.Result foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); + final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, CERTIFICATE_CHAIN); + final CreateSecretResult foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = runPipeline("", "node {", " withCredentials([certificate(credentialsId: '" + foo.getName() + "', keystoreVariable: 'KEYSTORE')]) {", " echo \"Credential: {keystore: $KEYSTORE}\"", " }", - "}")); + "}"); // Then - WorkflowRunAssert.assertThat(run) - .hasResult(hudson.model.Result.SUCCESS) + assertThat(run) + .hasResult(Result.SUCCESS) .hasLogContaining("Credential: {keystore: ****}"); } @@ -136,31 +153,51 @@ public void shouldSupportEnvironmentBinding() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportSnapshots() { // Given - final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, new Certificate[]{CERT}); - final CreateSecretOperation.Result foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); - final StandardCertificateCredentials before = lookupCredential(StandardCertificateCredentials.class, foo.getName()); + final KeyStore keyStore = Crypto.singletonKeyStore(ALIAS, KEY_PAIR.getPrivate(), PASSWORD, CERTIFICATE_CHAIN); + final CreateSecretResult foo = createCertificateSecret(Crypto.save(keyStore, PASSWORD)); + final StandardCertificateCredentials before = lookup(StandardCertificateCredentials.class, foo.getName()); // When - final StandardCertificateCredentials after = snapshot(before); + final StandardCertificateCredentials after = CredentialSnapshots.snapshot(before); // Then - assertSoftly(s -> { - s.assertThat(after.getId()).as("ID").isEqualTo(before.getId()); - s.assertThat(after.getPassword()).as("Password").isEqualTo(before.getPassword()); - s.assertThat(keystoreToMap(after.getKeyStore())).as("KeyStore").containsEntry(ALIAS, Collections.singletonList(CERT)); - }); + final CustomSoftAssertions s = new CustomSoftAssertions(); + s.assertThat(after).hasId(before.getId()); + s.assertThat(after).hasPassword(before.getPassword()); + s.assertThat(after.getKeyStore()).containsEntry(ALIAS, CERTIFICATE_CHAIN); + s.assertAll(); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldNotTolerateMalformattedKeyStore() { // Given - final CreateSecretOperation.Result foo = createCertificateSecret(new byte[] {0x00, 0x01}); + final CreateSecretResult foo = createCertificateSecret(new byte[] {0x00, 0x01}); // When - final StandardCertificateCredentials credential = lookupCredential(StandardCertificateCredentials.class, foo.getName()); + final StandardCertificateCredentials credential = jenkins.getCredentials().lookup(StandardCertificateCredentials.class, foo.getName()); // Then - assertThatThrownBy(credential::getKeyStore).isInstanceOf(CredentialsUnavailableException.class); + assertThatThrownBy(credential::getKeyStore) + .isInstanceOf(CredentialsUnavailableException.class); + } + + private CreateSecretResult createCertificateSecret(byte[] secretBinary) { + final List tags = Lists.of(AwsTags.type(Type.certificate)); + + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretBinary(ByteBuffer.wrap(secretBinary)) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } + + private C lookup(Class type, String id) { + return jenkins.getCredentials().lookup(type, id); + } + + private WorkflowRun runPipeline(String... pipeline) { + return jenkins.getPipelines().run(Strings.m(pipeline)); } } diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardUsernamePasswordCredentialsIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardUsernamePasswordCredentialsIT.java index 1d72d660..db678d98 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardUsernamePasswordCredentialsIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StandardUsernamePasswordCredentialsIT.java @@ -1,99 +1,113 @@ package io.jenkins.plugins.credentials.secretsmanager; +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.CreateSecretResult; +import com.amazonaws.services.secretsmanager.model.Tag; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl; import hudson.util.ListBoxModel; -import hudson.util.Secret; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation; -import io.jenkins.plugins.credentials.secretsmanager.util.Strings; -import io.jenkins.plugins.credentials.secretsmanager.util.assertions.WorkflowRunAssert; +import io.jenkins.plugins.credentials.secretsmanager.factory.Type; +import io.jenkins.plugins.credentials.secretsmanager.util.*; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; -import static org.assertj.core.api.SoftAssertions.assertSoftly; +import java.util.List; + +import static io.jenkins.plugins.credentials.secretsmanager.util.assertions.CustomAssertions.assertThat; /** * The plugin should support Username With Password credentials. */ -public class StandardUsernamePasswordCredentialsIT extends AbstractPluginIT implements CredentialsTests { +public class StandardUsernamePasswordCredentialsIT implements CredentialsTests { private static final String USERNAME = "joe"; private static final String PASSWORD = "supersecret"; + public final MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule(); + public final AWSSecretsManagerRule secretsManager = new AutoErasingAWSSecretsManagerRule(); + + @Rule + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins) + .around(secretsManager); + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportListView() { // Given - final CreateSecretOperation.Result foo = createUsernamePasswordSecret(USERNAME, PASSWORD); + final CreateSecretResult foo = createUsernamePasswordSecret(USERNAME, PASSWORD); // When - final ListBoxModel list = listCredentials(StandardUsernamePasswordCredentials.class); + final ListBoxModel list = jenkins.getCredentials().list(StandardUsernamePasswordCredentials.class); // Then assertThat(list) - .extracting("name", "value") - .containsOnly(tuple(USERNAME + "/******", foo.getName())); + .containsOption(USERNAME + "/******", foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHavePassword() { // Given - final CreateSecretOperation.Result foo = createUsernamePasswordSecret(USERNAME, PASSWORD); + final CreateSecretResult foo = createUsernamePasswordSecret(USERNAME, PASSWORD); // When final StandardUsernamePasswordCredentials credential = - lookupCredential(StandardUsernamePasswordCredentials.class, foo.getName()); + jenkins.getCredentials().lookup(StandardUsernamePasswordCredentials.class, foo.getName()); // Then - assertThat(credential.getPassword()).isEqualTo(Secret.fromString(PASSWORD)); + assertThat(credential) + .hasPassword(PASSWORD); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveUsername() { // Given - final CreateSecretOperation.Result foo = createUsernamePasswordSecret(USERNAME, PASSWORD); + final CreateSecretResult foo = createUsernamePasswordSecret(USERNAME, PASSWORD); // When final StandardUsernamePasswordCredentials credential = - lookupCredential(StandardUsernamePasswordCredentials.class, foo.getName()); + jenkins.getCredentials().lookup(StandardUsernamePasswordCredentials.class, foo.getName()); // Then - assertThat(credential.getUsername()).isEqualTo(USERNAME); + assertThat(credential) + .hasUsername(USERNAME); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveId() { // Given - final CreateSecretOperation.Result foo = createUsernamePasswordSecret(USERNAME, PASSWORD); + final CreateSecretResult foo = createUsernamePasswordSecret(USERNAME, PASSWORD); // When final StandardUsernamePasswordCredentials credential = - lookupCredential(StandardUsernamePasswordCredentials.class, foo.getName()); + jenkins.getCredentials().lookup(StandardUsernamePasswordCredentials.class, foo.getName()); // Then - assertThat(credential.getId()).isEqualTo(foo.getName()); + assertThat(credential) + .hasId(foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportWithCredentialsBinding() { // Given - final CreateSecretOperation.Result foo = createUsernamePasswordSecret(USERNAME, PASSWORD); + final CreateSecretResult foo = createUsernamePasswordSecret(USERNAME, PASSWORD); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = runPipeline("", "withCredentials([usernamePassword(credentialsId: '" + foo.getName() + "', usernameVariable: 'USR', passwordVariable: 'PSW')]) {", " echo \"Credential: {username: $USR, password: $PSW}\"", - "}")); + "}"); // Then - WorkflowRunAssert.assertThat(run) + assertThat(run) .hasResult(hudson.model.Result.SUCCESS) .hasLogContaining("Credential: {username: ****, password: ****}"); } @@ -102,10 +116,10 @@ public void shouldSupportWithCredentialsBinding() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportEnvironmentBinding() { // Given - final CreateSecretOperation.Result foo = createUsernamePasswordSecret(USERNAME, PASSWORD); + final CreateSecretResult foo = createUsernamePasswordSecret(USERNAME, PASSWORD); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = runPipeline("", "pipeline {", " agent none", " stages {", @@ -118,10 +132,10 @@ public void shouldSupportEnvironmentBinding() { " }", " }", " }", - "}")); + "}"); // Then - WorkflowRunAssert.assertThat(run) + assertThat(run) .hasResult(hudson.model.Result.SUCCESS) .hasLogContaining("{variable: ****, username: ****, password: ****}"); } @@ -130,29 +144,46 @@ public void shouldSupportEnvironmentBinding() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportSnapshots() { // Given - final CreateSecretOperation.Result foo = createUsernamePasswordSecret(USERNAME, PASSWORD); - final StandardUsernamePasswordCredentials before = lookupCredential(StandardUsernamePasswordCredentials.class, foo.getName()); + final CreateSecretResult foo = createUsernamePasswordSecret(USERNAME, PASSWORD); + final StandardUsernamePasswordCredentials before = jenkins.getCredentials().lookup(StandardUsernamePasswordCredentials.class, foo.getName()); // When - final StandardUsernamePasswordCredentials after = snapshot(before); + final StandardUsernamePasswordCredentials after = CredentialSnapshots.snapshot(before); // Then - assertSoftly(s -> { - s.assertThat(after.getId()).as("ID").isEqualTo(before.getId()); - s.assertThat(after.getUsername()).as("Username").isEqualTo(before.getUsername()); - s.assertThat(after.getPassword()).as("Password").isEqualTo(before.getPassword()); - }); + assertThat(after) + .hasUsername(before.getUsername()) + .hasPassword(before.getPassword()) + .hasId(before.getId()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveDescriptorIcon() { - final CreateSecretOperation.Result foo = createUsernamePasswordSecret(USERNAME, PASSWORD); - final StandardUsernamePasswordCredentials ours = lookupCredential(StandardUsernamePasswordCredentials.class, foo.getName()); + final CreateSecretResult foo = createUsernamePasswordSecret(USERNAME, PASSWORD); + final StandardUsernamePasswordCredentials ours = jenkins.getCredentials().lookup(StandardUsernamePasswordCredentials.class, foo.getName()); + // the default username/password implementation final StandardUsernamePasswordCredentials theirs = new UsernamePasswordCredentialsImpl(null, "id", "description", "username", "password"); - assertThat(ours.getDescriptor().getIconClassName()) - .isEqualTo(theirs.getDescriptor().getIconClassName()); + assertThat(ours) + .hasSameDescriptorIconAs(theirs); + } + + private CreateSecretResult createUsernamePasswordSecret(String username, String password) { + final List tags = Lists.of( + AwsTags.type(Type.usernamePassword), + AwsTags.username(username)); + + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretString(password) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } + + private WorkflowRun runPipeline(String... pipeline) { + return jenkins.getPipelines().run(Strings.m(pipeline)); } } diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StringCredentialsIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StringCredentialsIT.java index cb62766c..dba2047c 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StringCredentialsIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/StringCredentialsIT.java @@ -1,94 +1,109 @@ package io.jenkins.plugins.credentials.secretsmanager; +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.CreateSecretResult; +import com.amazonaws.services.secretsmanager.model.Tag; import hudson.util.ListBoxModel; import hudson.util.Secret; import io.jenkins.plugins.casc.misc.ConfiguredWithCode; -import io.jenkins.plugins.credentials.secretsmanager.util.CreateSecretOperation; -import io.jenkins.plugins.credentials.secretsmanager.util.Strings; -import io.jenkins.plugins.credentials.secretsmanager.util.assertions.WorkflowRunAssert; +import io.jenkins.plugins.credentials.secretsmanager.factory.Type; +import io.jenkins.plugins.credentials.secretsmanager.util.*; import org.jenkinsci.plugins.plaincredentials.StringCredentials; import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; +import java.util.List; + +import static io.jenkins.plugins.credentials.secretsmanager.util.assertions.CustomAssertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; /** * The plugin should support Secret Text credentials. */ -public class StringCredentialsIT extends AbstractPluginIT implements CredentialsTests { +public class StringCredentialsIT implements CredentialsTests { private static final String SECRET = "supersecret"; + public final MyJenkinsConfiguredWithCodeRule jenkins = new MyJenkinsConfiguredWithCodeRule(); + public final AWSSecretsManagerRule secretsManager = new AutoErasingAWSSecretsManagerRule(); + + @Rule + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins) + .around(secretsManager); + @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportListView() { // Given - final CreateSecretOperation.Result foo = createStringSecret(SECRET); + final CreateSecretResult foo = createStringSecret(SECRET); // When - final ListBoxModel list = listCredentials(StringCredentials.class); + final ListBoxModel list = jenkins.getCredentials().list(StringCredentials.class); // Then assertThat(list) - .extracting("name", "value") - .containsOnly(tuple(foo.getName(), foo.getName())); + .containsOption(foo.getName(), foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveId() { // Given - final CreateSecretOperation.Result foo = createStringSecret(SECRET); + final CreateSecretResult foo = createStringSecret(SECRET); // When - final StringCredentials credential = lookupCredential(StringCredentials.class, foo.getName()); + final StringCredentials credential = jenkins.getCredentials().lookup(StringCredentials.class, foo.getName()); // Then - assertThat(credential.getId()).isEqualTo(foo.getName()); + assertThat(credential) + .hasId(foo.getName()); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveSecret() { // Given - final CreateSecretOperation.Result foo = createStringSecret(SECRET); + final CreateSecretResult foo = createStringSecret(SECRET); // When - final StringCredentials credential = lookupCredential(StringCredentials.class, foo.getName()); + final StringCredentials credential = jenkins.getCredentials().lookup(StringCredentials.class, foo.getName()); // Then - assertThat(credential.getSecret()).isEqualTo(Secret.fromString(SECRET)); + assertThat(credential) + .hasSecret(SECRET); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldHaveDescriptorIcon() { - final CreateSecretOperation.Result foo = createStringSecret(SECRET); - final StringCredentials ours = lookupCredential(StringCredentials.class, foo.getName()); + final CreateSecretResult foo = createStringSecret(SECRET); + final StringCredentials ours = jenkins.getCredentials().lookup(StringCredentials.class, foo.getName()); final StringCredentials theirs = new StringCredentialsImpl(null, "id", "description", Secret.fromString("secret")); - assertThat(ours.getDescriptor().getIconClassName()) - .isEqualTo(theirs.getDescriptor().getIconClassName()); + assertThat(ours) + .hasSameDescriptorIconAs(theirs); } @Test @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportWithCredentialsBinding() { // Given - final CreateSecretOperation.Result foo = createStringSecret(SECRET); + final CreateSecretResult foo = createStringSecret(SECRET); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = runPipeline("", "withCredentials([string(credentialsId: '" + foo.getName() + "', variable: 'VAR')]) {", " echo \"Credential: $VAR\"", - "}")); + "}"); // Then - WorkflowRunAssert.assertThat(run) + assertThat(run) .hasResult(hudson.model.Result.SUCCESS) .hasLogContaining("Credential: ****"); } @@ -97,10 +112,10 @@ public void shouldSupportWithCredentialsBinding() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportEnvironmentBinding() { // Given - final CreateSecretOperation.Result foo = createStringSecret(SECRET); + final CreateSecretResult foo = createStringSecret(SECRET); // When - final WorkflowRun run = runPipeline(Strings.m("", + final WorkflowRun run = runPipeline("", "pipeline {", " agent none", " stages {", @@ -113,10 +128,10 @@ public void shouldSupportEnvironmentBinding() { " }", " }", " }", - "}")); + "}"); // Then - WorkflowRunAssert.assertThat(run) + assertThat(run) .hasResult(hudson.model.Result.SUCCESS) .hasLogContaining("{variable: ****}"); } @@ -125,11 +140,11 @@ public void shouldSupportEnvironmentBinding() { @ConfiguredWithCode(value = "/integration.yml") public void shouldSupportSnapshots() { // Given - final CreateSecretOperation.Result foo = createStringSecret(SECRET); - final StringCredentials before = lookupCredential(StringCredentials.class, foo.getName()); + final CreateSecretResult foo = createStringSecret(SECRET); + final StringCredentials before = jenkins.getCredentials().lookup(StringCredentials.class, foo.getName()); // When - final StringCredentials after = snapshot(before); + final StringCredentials after = CredentialSnapshots.snapshot(before); // Then assertSoftly(s -> { @@ -137,4 +152,19 @@ public void shouldSupportSnapshots() { s.assertThat(after.getSecret()).as("Secret").isEqualTo(before.getSecret()); }); } + + private CreateSecretResult createStringSecret(String secretString) { + final List tags = Lists.of(AwsTags.type(Type.string)); + + final CreateSecretRequest request = new CreateSecretRequest() + .withName(CredentialNames.random()) + .withSecretString(secretString) + .withTags(tags); + + return secretsManager.getClient().createSecret(request); + } + + private WorkflowRun runPipeline(String... pipeline) { + return jenkins.getPipelines().run(Strings.m(pipeline)); + } } diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/AbstractCheckConnectionIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/AbstractCheckConnectionIT.java index 3933d5d3..68a6b27e 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/AbstractCheckConnectionIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/AbstractCheckConnectionIT.java @@ -1,18 +1,11 @@ package io.jenkins.plugins.credentials.secretsmanager.config; -import org.junit.BeforeClass; import org.junit.Test; import static org.assertj.core.api.SoftAssertions.assertSoftly; public abstract class AbstractCheckConnectionIT { - @BeforeClass - public static void fakeAwsCredentials() { - System.setProperty("aws.accessKeyId", "test"); - System.setProperty("aws.secretKey", "test"); - } - protected abstract Result validate(String serviceEndpoint, String signingRegion); @Test diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/AbstractPluginConfigurationTest.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/AbstractPluginConfigurationIT.java similarity index 96% rename from src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/AbstractPluginConfigurationTest.java rename to src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/AbstractPluginConfigurationIT.java index fd4d50e2..7a54748f 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/AbstractPluginConfigurationTest.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/AbstractPluginConfigurationIT.java @@ -5,7 +5,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; -public abstract class AbstractPluginConfigurationTest { +public abstract class AbstractPluginConfigurationIT { protected abstract PluginConfiguration getPluginConfiguration(); diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/CheckConnectionApiIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/CheckConnectionApiIT.java index 173778f1..4e270d64 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/CheckConnectionApiIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/CheckConnectionApiIT.java @@ -1,6 +1,8 @@ package io.jenkins.plugins.credentials.secretsmanager.config; +import io.jenkins.plugins.credentials.secretsmanager.util.Rules; import org.junit.Rule; +import org.junit.rules.RuleChain; import org.jvnet.hudson.test.JenkinsRule; import org.xml.sax.SAXException; @@ -11,8 +13,12 @@ public class CheckConnectionApiIT extends AbstractCheckConnectionIT { + public final JenkinsRule jenkins = new JenkinsRule(); + @Rule - public JenkinsRule r = new JenkinsRule(); + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins); @Override protected Result validate(String serviceEndpoint, String signingRegion) { @@ -31,7 +37,7 @@ protected Result validate(String serviceEndpoint, String signingRegion) { private JenkinsRule.JSONWebResponse doPost(String path, Object json) { try { - return r.postJSON(path, json); + return jenkins.postJSON(path, json); } catch (SAXException | IOException e) { throw new RuntimeException(e); } diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/CheckConnectionWebIT.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/CheckConnectionWebIT.java index 09cf5500..1fb3c801 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/CheckConnectionWebIT.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/CheckConnectionWebIT.java @@ -5,7 +5,9 @@ import com.gargoylesoftware.htmlunit.html.HtmlButton; import com.gargoylesoftware.htmlunit.html.HtmlElement; import io.jenkins.plugins.credentials.secretsmanager.util.JenkinsConfiguredWithWebRule; +import io.jenkins.plugins.credentials.secretsmanager.util.Rules; import org.junit.Rule; +import org.junit.rules.RuleChain; import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; @@ -13,15 +15,19 @@ public class CheckConnectionWebIT extends AbstractCheckConnectionIT { + public final JenkinsConfiguredWithWebRule jenkins = new JenkinsConfiguredWithWebRule(); + @Rule - public final JenkinsConfiguredWithWebRule r = new JenkinsConfiguredWithWebRule(); + public final RuleChain chain = RuleChain + .outerRule(Rules.awsAccessKey("fake", "fake")) + .around(jenkins); @Override protected Result validate(String serviceEndpoint, String signingRegion) { AtomicReference result = new AtomicReference<>(); - r.configure(form -> { + jenkins.configure(form -> { form.getInputByName("_.endpointConfiguration").setChecked(true); form.getInputByName("_.serviceEndpoint").setValueAttribute(serviceEndpoint); form.getInputByName("_.signingRegion").setValueAttribute(signingRegion); diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginCasCConfigurationTest.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginCasCConfigurationIT.java similarity index 86% rename from src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginCasCConfigurationTest.java rename to src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginCasCConfigurationIT.java index c1f5516d..0a569640 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginCasCConfigurationTest.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginCasCConfigurationIT.java @@ -6,10 +6,7 @@ import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -public class PluginCasCConfigurationTest extends AbstractPluginConfigurationTest { +public class PluginCasCConfigurationIT extends AbstractPluginConfigurationIT { @Rule public JenkinsRule r = new JenkinsConfiguredWithCodeRule(); diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginWebConfigurationTest.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginWebConfigurationIT.java similarity index 97% rename from src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginWebConfigurationTest.java rename to src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginWebConfigurationIT.java index c9e5616e..772cc537 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginWebConfigurationTest.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/config/PluginWebConfigurationIT.java @@ -6,7 +6,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; -public class PluginWebConfigurationTest extends AbstractPluginConfigurationTest { +public class PluginWebConfigurationIT extends AbstractPluginConfigurationIT { @Rule public final JenkinsConfiguredWithWebRule r = new JenkinsConfiguredWithWebRule(); diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AWSSecretsManagerRule.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AWSSecretsManagerRule.java new file mode 100644 index 00000000..2f75f515 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AWSSecretsManagerRule.java @@ -0,0 +1,31 @@ +package io.jenkins.plugins.credentials.secretsmanager.util; + +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.secretsmanager.AWSSecretsManager; +import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder; +import org.junit.rules.ExternalResource; + +/** + * Wraps client-side access to AWS Secrets Manager in tests. Defers client initialization in case you want to set AWS + * environment variables or Java properties in a wrapper Rule first. + */ +public class AWSSecretsManagerRule extends ExternalResource { + + private transient AWSSecretsManager client; + + @Override + public void before() { + client = AWSSecretsManagerClientBuilder.standard() + .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://localhost:4584", "us-east-1")) + .build(); + } + + @Override + protected void after() { + client = null; + } + + public AWSSecretsManager getClient() { + return client; + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AutoErasingAWSSecretsManagerRule.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AutoErasingAWSSecretsManagerRule.java new file mode 100644 index 00000000..5ce5475e --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AutoErasingAWSSecretsManagerRule.java @@ -0,0 +1,31 @@ +package io.jenkins.plugins.credentials.secretsmanager.util; + +import com.amazonaws.services.secretsmanager.model.*; + +/** + * Guarantees to wipe any secrets left in Secrets Manager before your test. + */ +public class AutoErasingAWSSecretsManagerRule extends AWSSecretsManagerRule { + + @Override + public void before() { + super.before(); + + clear(); + } + + private void clear() { + final ListSecretsResult listSecretsResult = getClient().listSecrets(new ListSecretsRequest().withMaxResults(100)); + + for(SecretListEntry entry: listSecretsResult.getSecretList()) { + final String secretId = entry.getName(); + + try { + getClient().restoreSecret(new RestoreSecretRequest().withSecretId(secretId)); + getClient().deleteSecret(new DeleteSecretRequest().withSecretId(secretId).withForceDeleteWithoutRecovery(true)); + } catch (ResourceNotFoundException e) { + // Don't care + } + } + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java new file mode 100644 index 00000000..c774b619 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/AwsTags.java @@ -0,0 +1,29 @@ +package io.jenkins.plugins.credentials.secretsmanager.util; + +import com.amazonaws.services.secretsmanager.model.Tag; +import io.jenkins.plugins.credentials.secretsmanager.factory.Tags; + +/** + * Tags that the Jenkins plugin looks for. + */ +public abstract class AwsTags { + private AwsTags() { + + } + + public static Tag filename(String filename) { + return AwsTags.tag(Tags.filename, filename); + } + + public static Tag username(String username) { + return AwsTags.tag(Tags.username, username); + } + + public static Tag type(String type) { + return tag(Tags.type, type); + } + + public static Tag tag(String key, String value) { + return new Tag().withKey(key).withValue(value); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CreateSecretOperation.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CreateSecretOperation.java deleted file mode 100644 index e435196d..00000000 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CreateSecretOperation.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.jenkins.plugins.credentials.secretsmanager.util; - -import com.amazonaws.services.secretsmanager.AWSSecretsManager; -import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; -import com.amazonaws.services.secretsmanager.model.CreateSecretResult; -import com.amazonaws.services.secretsmanager.model.Tag; - -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -public class CreateSecretOperation { - - private final AWSSecretsManager client; - - public CreateSecretOperation(AWSSecretsManager client) { - this.client = client; - } - - public Result run(String name, String secretString) { - return run(name, secretString, o -> {}); - } - - public Result run(String name, String secretString, Consumer opts) { - final Opts o = new Opts(); - opts.accept(o); - - final String description = o.description; - final Map tags = o.tags; - - CreateSecretRequest request = new CreateSecretRequest() - .withName(name) - .withDescription(description) - .withSecretString(secretString); - - if (tags != null) { - final List t = tags.entrySet().stream() - .map((entry) -> new Tag().withKey(entry.getKey()).withValue(entry.getValue())) - .collect(Collectors.toList()); - - request = request.withTags(t); - } - - final CreateSecretResult result = client.createSecret(request); - - if (result.getSdkHttpMetadata().getHttpStatusCode() >= 400) { - throw new RuntimeException("Failed to create secret."); - } - - return new Result(name); - } - - public Result run(String name, byte[] secretBinary) { - return run(name, secretBinary, o -> {}); - } - - public Result run(String name, byte[] secretBinary, Consumer opts) { - final Opts o = new Opts(); - opts.accept(o); - - final String description = o.description; - final Map tags = o.tags; - - final List t = tags.entrySet().stream() - .map((entry) -> new Tag().withKey(entry.getKey()).withValue(entry.getValue())) - .collect(Collectors.toList()); - - final CreateSecretRequest request = new CreateSecretRequest() - .withName(name) - .withDescription(description) - .withSecretBinary(ByteBuffer.wrap(secretBinary)) - .withTags(t); - - final CreateSecretResult result = client.createSecret(request); - - if (result.getSdkHttpMetadata().getHttpStatusCode() >= 400) { - throw new RuntimeException("Failed to create secret."); - } - - return new Result(name); - } - - public static class Opts { - public String description = ""; - public Map tags = Collections.emptyMap(); - } - - public static class Result { - private final String name; - - public Result(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - } -} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CredentialNames.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CredentialNames.java new file mode 100644 index 00000000..a5651e77 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CredentialNames.java @@ -0,0 +1,13 @@ +package io.jenkins.plugins.credentials.secretsmanager.util; + +import java.util.UUID; + +public class CredentialNames { + /** + * @return a random name for a Jenkins credential + */ + public static String random() { + // The CredentialsNameProvider does not like hyphens in the name, so we remove them + return UUID.randomUUID().toString().replace("-", ""); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CredentialSnapshots.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CredentialSnapshots.java new file mode 100644 index 00000000..64e32cb3 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/CredentialSnapshots.java @@ -0,0 +1,16 @@ +package io.jenkins.plugins.credentials.secretsmanager.util; + +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import org.apache.commons.lang3.SerializationUtils; + +public abstract class CredentialSnapshots { + + private CredentialSnapshots() { + + } + + public static C snapshot(C credentials) { + return SerializationUtils.deserialize(SerializationUtils.serialize(CredentialsProvider.snapshot(credentials))); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Crypto.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Crypto.java index c90d33ce..adbd49fb 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Crypto.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Crypto.java @@ -13,23 +13,11 @@ import java.io.IOException; import java.io.StringWriter; import java.math.BigInteger; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.SecureRandom; +import java.security.*; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.Arrays; import java.util.Date; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; /** * Encapsulate BouncyCastle operations. @@ -118,22 +106,4 @@ public static X509Certificate newSelfSignedCertificate(String name, KeyPair keyP throw new RuntimeException(e); } } - - public static Map> keystoreToMap(KeyStore keyStore) { - final Map> ks = new HashMap<>(); - - try { - final Enumeration aliases = keyStore.aliases(); - while (aliases.hasMoreElements()) { - final String a = aliases.nextElement(); - final Certificate[] certificateChain = keyStore.getCertificateChain(a); - final List certificateChainList = Arrays.asList(certificateChain); - ks.put(a, certificateChainList); - } - } catch (KeyStoreException e) { - throw new RuntimeException(e); - } - - return ks; - } } diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/JenkinsCredentials.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/JenkinsCredentials.java new file mode 100644 index 00000000..4332be4b --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/JenkinsCredentials.java @@ -0,0 +1,42 @@ +package io.jenkins.plugins.credentials.secretsmanager.util; + +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsStore; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import hudson.security.ACL; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; + +import java.util.List; + +/** + * Convenience methods for using credentials. + */ +public class JenkinsCredentials { + + private final Jenkins jenkins; + + public JenkinsCredentials(Jenkins jenkins) { + this.jenkins = jenkins; + } + + public Iterable lookupStores() { + return CredentialsProvider.lookupStores(jenkins); + } + + public List lookup(Class type) { + return CredentialsProvider.lookupCredentials(type, jenkins, ACL.SYSTEM, Lists.of()); + } + + public ListBoxModel list(Class type) { + return CredentialsProvider.listCredentials(type, jenkins, null, null, null); + } + + public C lookup(Class type, String id) { + return lookup(type).stream() + .filter(c -> c.getId().equals(id)) + .findFirst() + .orElseThrow(() -> new RuntimeException(String.format("Could not find a credential with id <%s>", id))); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/JenkinsPipelines.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/JenkinsPipelines.java new file mode 100644 index 00000000..801ba380 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/JenkinsPipelines.java @@ -0,0 +1,42 @@ +package io.jenkins.plugins.credentials.secretsmanager.util; + +import hudson.model.Run; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; + +import hudson.model.queue.QueueTaskFuture; +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +/** + * Convenience methods for using pipelines. + */ +public class JenkinsPipelines { + + private final Jenkins jenkins; + + public JenkinsPipelines(Jenkins jenkins) { + this.jenkins = jenkins; + } + + public WorkflowRun run(String definition) { + try { + final WorkflowJob project = jenkins.createProject(WorkflowJob.class, "example"); + project.setDefinition(new CpsFlowDefinition(definition, true)); + final QueueTaskFuture workflowRunFuture = project.scheduleBuild2(0); + final WorkflowRun workflowRun = workflowRunFuture.waitForStart(); + waitForCompletion(workflowRun); + return workflowRun; + } catch (IOException | InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + private > void waitForCompletion(R r) throws InterruptedException { + while(r.isBuilding()) { + Thread.sleep(100L); + } + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Lists.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Lists.java new file mode 100644 index 00000000..58ce2266 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Lists.java @@ -0,0 +1,18 @@ +package io.jenkins.plugins.credentials.secretsmanager.util; + +import java.util.*; + +/** + * Polyfill for the Java 9 List.of API methods. + */ +public abstract class Lists { + + private Lists() { + + } + + @SafeVarargs + public static List of(V... elems) { + return Arrays.asList(elems); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Maps.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Maps.java deleted file mode 100644 index 1d01c3af..00000000 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Maps.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.jenkins.plugins.credentials.secretsmanager.util; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Polyfill for the Java 9 Map.of API methods. - */ -public final class Maps { - - public static Map of(K k1, V v1, K k2, V v2) { - final Map m = new HashMap<>(); - m.put(k1, v1); - m.put(k2, v2); - return Collections.unmodifiableMap(m); - } - - public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) { - final Map m = new HashMap<>(); - m.put(k1, v1); - m.put(k2, v2); - m.put(k3, v3); - m.put(k4, v4); - return Collections.unmodifiableMap(m); - } -} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/MyJenkinsConfiguredWithCodeRule.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/MyJenkinsConfiguredWithCodeRule.java new file mode 100644 index 00000000..4e89e253 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/MyJenkinsConfiguredWithCodeRule.java @@ -0,0 +1,17 @@ +package io.jenkins.plugins.credentials.secretsmanager.util; + +import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; + +/** + * Extensions on the standard jenkins rule. + */ +public class MyJenkinsConfiguredWithCodeRule extends JenkinsConfiguredWithCodeRule { + + public JenkinsCredentials getCredentials() { + return new JenkinsCredentials(this.jenkins); + } + + public JenkinsPipelines getPipelines() { + return new JenkinsPipelines(this.jenkins); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Rules.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Rules.java new file mode 100644 index 00000000..747a4dce --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/Rules.java @@ -0,0 +1,12 @@ +package io.jenkins.plugins.credentials.secretsmanager.util; + +import io.jenkins.plugins.casc.misc.EnvVarsRule; +import org.junit.rules.TestRule; + +public class Rules { + public static TestRule awsAccessKey(String id, String secret) { + return new EnvVarsRule() + .set("AWS_ACCESS_KEY_ID", id) + .set("AWS_SECRET_ACCESS_KEY", secret); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomAssertions.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomAssertions.java new file mode 100644 index 00000000..4d3f6563 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomAssertions.java @@ -0,0 +1,51 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import hudson.util.ListBoxModel; +import org.jenkinsci.plugins.plaincredentials.FileCredentials; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; + +import java.security.KeyStore; + +public class CustomAssertions { + + public static StandardCredentialsAssert assertThat(StandardCredentials actual) { + return new StandardCredentialsAssert(actual); + } + + public static FileCredentialsAssert assertThat(FileCredentials actual) { + return new FileCredentialsAssert(actual); + } + + public static StringCredentialsAssert assertThat(StringCredentials actual) { + return new StringCredentialsAssert(actual); + } + + public static StandardUsernamePasswordCredentialsAssert assertThat(StandardUsernamePasswordCredentials actual) { + return new StandardUsernamePasswordCredentialsAssert(actual); + } + + public static StandardCertificateCredentialsAssert assertThat(StandardCertificateCredentials actual) { + return new StandardCertificateCredentialsAssert(actual); + } + + public static SSHUserPrivateKeyAssert assertThat(SSHUserPrivateKey actual) { + return new SSHUserPrivateKeyAssert(actual); + } + + public static WorkflowRunAssert assertThat(WorkflowRun actual) { + return new WorkflowRunAssert(actual); + } + + public static ListBoxModelAssert assertThat(ListBoxModel actual) { + return new ListBoxModelAssert(actual); + } + + public static KeyStoreAssert assertThat(KeyStore actual) { + return new KeyStoreAssert(actual); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomSoftAssertions.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomSoftAssertions.java new file mode 100644 index 00000000..fdfb5fde --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/CustomSoftAssertions.java @@ -0,0 +1,8 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import org.assertj.core.api.SoftAssertions; + +public class CustomSoftAssertions extends SoftAssertions implements KeyStoreSoftAssertionsProvider, + StandardCredentialsSoftAssertionsProvider, StandardCertificateCredentialsSoftAssertionsProvider { + +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/FileCredentialsAssert.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/FileCredentialsAssert.java new file mode 100644 index 00000000..cf28d2a7 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/FileCredentialsAssert.java @@ -0,0 +1,72 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import com.google.common.io.ByteStreams; +import org.assertj.core.api.AbstractAssert; +import org.jenkinsci.plugins.plaincredentials.FileCredentials; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Objects; + +public class FileCredentialsAssert extends AbstractAssert { + + public FileCredentialsAssert(FileCredentials actual) { + super(actual, FileCredentialsAssert.class); + } + + public FileCredentialsAssert hasFileName(String fileName) { + isNotNull(); + + if (!Objects.equals(actual.getFileName(), fileName)) { + failWithMessage("Expected file name to be <%s> but was <%s>", fileName, actual.getFileName()); + } + + return this; + } + + public FileCredentialsAssert hasContent(byte[] content) { + isNotNull(); + + try { + final byte[] actualContent = ByteStreams.toByteArray(actual.getContent()); + + if (!Arrays.equals(actualContent, content)) { + failWithMessage("Expected content to be <%s> but was <%s>", content, actualContent); + } + } catch (IOException e) { + failWithMessage("Could not get file credential's content"); + } + + return this; + } + + public FileCredentialsAssert hasContent(InputStream content) { + isNotNull(); + + try { + final byte[] expectedContent = ByteStreams.toByteArray(content); + final byte[] actualContent = ByteStreams.toByteArray(actual.getContent()); + + if (!Arrays.equals(actualContent, expectedContent)) { + failWithMessage("Expected content to be <%s> but was <%s>", expectedContent, actualContent); + } + } catch (IOException e) { + failWithMessage("Could not get file credential's content"); + } + + return this; + } + + public FileCredentialsAssert hasId(String id) { + new StandardCredentialsAssert(actual).hasId(id); + + return this; + } + + public FileCredentialsAssert hasSameDescriptorIconAs(FileCredentials theirs) { + new StandardCredentialsAssert(actual).hasSameDescriptorIconAs(theirs); + + return this; + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/KeyStoreAssert.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/KeyStoreAssert.java new file mode 100644 index 00000000..d5c4af05 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/KeyStoreAssert.java @@ -0,0 +1,49 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import org.assertj.core.api.AbstractAssert; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.Certificate; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public class KeyStoreAssert extends AbstractAssert { + public KeyStoreAssert(KeyStore keyStore) { + super(keyStore, KeyStoreAssert.class); + } + + public KeyStoreAssert containsEntry(String alias, Certificate[] certificateChain) { + isNotNull(); + + final boolean foundEntry = keystoreToMap(actual) + .entrySet() + .stream() + .anyMatch(entry -> entry.getKey().equals(alias) && Arrays.equals(entry.getValue(), certificateChain)); + + if (!foundEntry) { + failWithMessage("Expected KeyStore to contain entry alias=<%s> with certificate chain, but it did not", alias); + } + + return this; + } + + private static Map keystoreToMap(KeyStore keyStore) { + final Map ks = new HashMap<>(); + + try { + final Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + final String a = aliases.nextElement(); + final Certificate[] certificateChain = keyStore.getCertificateChain(a); + ks.put(a, certificateChain); + } + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + + return ks; + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/KeyStoreSoftAssertionsProvider.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/KeyStoreSoftAssertionsProvider.java new file mode 100644 index 00000000..ca6b99a8 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/KeyStoreSoftAssertionsProvider.java @@ -0,0 +1,11 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import org.assertj.core.api.SoftAssertionsProvider; + +import java.security.KeyStore; + +public interface KeyStoreSoftAssertionsProvider extends SoftAssertionsProvider { + default KeyStoreAssert assertThat(KeyStore actual) { + return proxy(KeyStoreAssert.class, KeyStore.class, actual); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/ListBoxModelAssert.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/ListBoxModelAssert.java new file mode 100644 index 00000000..92f9961b --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/ListBoxModelAssert.java @@ -0,0 +1,23 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import hudson.util.ListBoxModel; +import org.assertj.core.api.AbstractAssert; + +public class ListBoxModelAssert extends AbstractAssert { + public ListBoxModelAssert(ListBoxModel options) { + super(options, ListBoxModelAssert.class); + } + + public ListBoxModelAssert containsOption(String name, String value) { + isNotNull(); + + final boolean yes = actual.stream() + .anyMatch(option -> (option.name.equals(name)) && option.value.equals(value)); + + if (!yes) { + failWithMessage("Did not find an option with name <%s> and value <%s>", name, value); + } + + return this; + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/SSHUserPrivateKeyAssert.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/SSHUserPrivateKeyAssert.java new file mode 100644 index 00000000..d1169cb1 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/SSHUserPrivateKeyAssert.java @@ -0,0 +1,62 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; +import hudson.util.Secret; +import org.assertj.core.api.AbstractAssert; + +import java.util.List; +import java.util.Objects; + +public class SSHUserPrivateKeyAssert extends AbstractAssert { + + public SSHUserPrivateKeyAssert(SSHUserPrivateKey actual) { + super(actual, SSHUserPrivateKeyAssert.class); + } + + public SSHUserPrivateKeyAssert hasUsername(String username) { + isNotNull(); + + if (!Objects.equals(actual.getUsername(), username)) { + failWithMessage("Expected username to be <%s> but was <%s>", username, actual.getUsername()); + } + + return this; + } + + public SSHUserPrivateKeyAssert doesNotHavePassphrase() { + return hasPassphrase(Secret.fromString("")); + } + + public SSHUserPrivateKeyAssert hasPassphrase(Secret passphrase) { + isNotNull(); + + if (!Objects.equals(actual.getPassphrase(), passphrase)) { + failWithMessage("Expected passphrase to be <%s> but was <%s>", passphrase, actual.getPassphrase()); + } + + return this; + } + + public SSHUserPrivateKeyAssert hasPrivateKeys(List privateKeys) { + isNotNull(); + + if (!Objects.equals(actual.getPrivateKeys(), privateKeys)) { + failWithMessage("Expected private keys to be <%s> but was <%s>", privateKeys, actual.getPrivateKeys()); + } + + return this; + } + + public SSHUserPrivateKeyAssert hasId(String id) { + new StandardCredentialsAssert(actual).hasId(id); + + return this; + } + + public SSHUserPrivateKeyAssert hasSameDescriptorIconAs(BasicSSHUserPrivateKey theirs) { + new StandardCredentialsAssert(actual).hasSameDescriptorIconAs(theirs); + + return this; + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCertificateCredentialsAssert.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCertificateCredentialsAssert.java new file mode 100644 index 00000000..e67efbbe --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCertificateCredentialsAssert.java @@ -0,0 +1,46 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; +import hudson.util.Secret; +import org.assertj.core.api.AbstractAssert; + +import java.util.Objects; + +public class StandardCertificateCredentialsAssert extends AbstractAssert { + + public StandardCertificateCredentialsAssert(StandardCertificateCredentials actual) { + super(actual, StandardCertificateCredentialsAssert.class); + } + + public StandardCertificateCredentialsAssert hasPassword(Secret password) { + isNotNull(); + + if (!Objects.equals(actual.getPassword(), password)) { + failWithMessage("Expected password to be <%s> but was <%s>", password, actual.getPassword()); + } + + return this; + } + + public StandardCertificateCredentialsAssert doesNotHavePassword() { + isNotNull(); + + if (!Objects.equals(actual.getPassword(), Secret.fromString(""))) { + failWithMessage("Should not have password, but it was <%s>", actual.getPassword()); + } + + return this; + } + + public StandardCertificateCredentialsAssert hasId(String id) { + new StandardCredentialsAssert(actual).hasId(id); + + return this; + } + + public StandardCertificateCredentialsAssert hasSameDescriptorIconAs(StandardCertificateCredentials theirs) { + new StandardCredentialsAssert(actual).hasSameDescriptorIconAs(theirs); + + return this; + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCertificateCredentialsSoftAssertionsProvider.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCertificateCredentialsSoftAssertionsProvider.java new file mode 100644 index 00000000..9cdf2ff3 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCertificateCredentialsSoftAssertionsProvider.java @@ -0,0 +1,10 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; +import org.assertj.core.api.SoftAssertionsProvider; + +public interface StandardCertificateCredentialsSoftAssertionsProvider extends SoftAssertionsProvider { + default StandardCertificateCredentialsAssert assertThat(StandardCertificateCredentials actual) { + return proxy(StandardCertificateCredentialsAssert.class, StandardCertificateCredentials.class, actual); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCredentialsAssert.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCredentialsAssert.java new file mode 100644 index 00000000..c719bf3a --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCredentialsAssert.java @@ -0,0 +1,39 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import org.assertj.core.api.AbstractAssert; + +import java.util.Objects; + +public class StandardCredentialsAssert extends AbstractAssert { + + public StandardCredentialsAssert(StandardCredentials actual) { + super(actual, StandardCredentialsAssert.class); + } + + public StandardCredentialsAssert(StandardCredentials actual, Class klass) { + super(actual, klass); + } + + public StandardCredentialsAssert hasId(String id) { + isNotNull(); + + if (!Objects.equals(actual.getId(), id)) { + failWithMessage("Expected ID to be <%s> but was <%s>", id, actual.getId()); + } + + return this; + } + + public StandardCredentialsAssert hasSameDescriptorIconAs(StandardCredentials other) { + isNotNull(); + + final String ourIconClassName = actual.getDescriptor().getIconClassName(); + final String theirIconClassName = other.getDescriptor().getIconClassName(); + if (!Objects.equals(ourIconClassName, theirIconClassName)) { + failWithMessage("Expected descriptor's icon class name to be <%s> but was <%s>", theirIconClassName, ourIconClassName); + } + + return this; + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCredentialsSoftAssertionsProvider.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCredentialsSoftAssertionsProvider.java new file mode 100644 index 00000000..f7786d6b --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardCredentialsSoftAssertionsProvider.java @@ -0,0 +1,10 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import org.assertj.core.api.SoftAssertionsProvider; + +public interface StandardCredentialsSoftAssertionsProvider extends SoftAssertionsProvider { + default StandardCredentialsAssert assertThat(StandardCredentials actual) { + return proxy(StandardCredentialsAssert.class, StandardCredentials.class, actual); + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardUsernamePasswordCredentialsAssert.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardUsernamePasswordCredentialsAssert.java new file mode 100644 index 00000000..9bb928c4 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StandardUsernamePasswordCredentialsAssert.java @@ -0,0 +1,50 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import hudson.util.Secret; +import org.assertj.core.api.AbstractAssert; + +import java.util.Objects; + +public class StandardUsernamePasswordCredentialsAssert extends AbstractAssert { + + public StandardUsernamePasswordCredentialsAssert(StandardUsernamePasswordCredentials actual) { + super(actual, StandardUsernamePasswordCredentialsAssert.class); + } + + public StandardUsernamePasswordCredentialsAssert hasUsername(String username) { + isNotNull(); + + if (!Objects.equals(actual.getUsername(), username)) { + failWithMessage("Expected username to be <%s> but was <%s>", username, actual.getUsername()); + } + + return this; + } + + public StandardUsernamePasswordCredentialsAssert hasPassword(String password) { + return hasPassword(Secret.fromString(password)); + } + + public StandardUsernamePasswordCredentialsAssert hasPassword(Secret password) { + isNotNull(); + + if (!Objects.equals(actual.getPassword(), password)) { + failWithMessage("Expected password to be <%s> but was <%s>", password, actual.getPassword()); + } + + return this; + } + + public StandardUsernamePasswordCredentialsAssert hasId(String id) { + new StandardCredentialsAssert(actual).hasId(id); + + return this; + } + + public StandardUsernamePasswordCredentialsAssert hasSameDescriptorIconAs(StandardUsernamePasswordCredentials theirs) { + new StandardCredentialsAssert(actual).hasSameDescriptorIconAs(theirs); + + return this; + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StringCredentialsAssert.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StringCredentialsAssert.java new file mode 100644 index 00000000..9f72c0ad --- /dev/null +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/StringCredentialsAssert.java @@ -0,0 +1,35 @@ +package io.jenkins.plugins.credentials.secretsmanager.util.assertions; + +import hudson.util.Secret; +import org.assertj.core.api.AbstractAssert; +import org.jenkinsci.plugins.plaincredentials.StringCredentials; + +import java.util.Objects; + +public class StringCredentialsAssert extends AbstractAssert { + public StringCredentialsAssert(StringCredentials actual) { + super(actual, StringCredentialsAssert.class); + } + + public StringCredentialsAssert hasSecret(String secret) { + isNotNull(); + + if (!Objects.equals(actual.getSecret(), Secret.fromString(secret))) { + failWithMessage("Expected secret to be <%s> but was <%s>", secret, actual.getSecret().getPlainText()); + } + + return this; + } + + public StringCredentialsAssert hasId(String id) { + new StandardCredentialsAssert(actual).hasId(id); + + return this; + } + + public StringCredentialsAssert hasSameDescriptorIconAs(StringCredentials theirs) { + new StandardCredentialsAssert(actual).hasSameDescriptorIconAs(theirs); + + return this; + } +} diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/WorkflowRunAssert.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/WorkflowRunAssert.java index 46bb075b..3e9a1f7f 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/WorkflowRunAssert.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/assertions/WorkflowRunAssert.java @@ -13,10 +13,6 @@ public WorkflowRunAssert(WorkflowRun actual) { super(actual, WorkflowRunAssert.class); } - public static WorkflowRunAssert assertThat(WorkflowRun actual) { - return new WorkflowRunAssert(actual); - } - public WorkflowRunAssert hasResult(Result result) { isNotNull(); diff --git a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/git/GitSshServer.java b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/git/GitSshServer.java index 40d2007d..078be591 100644 --- a/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/git/GitSshServer.java +++ b/src/test/java/io/jenkins/plugins/credentials/secretsmanager/util/git/GitSshServer.java @@ -2,6 +2,7 @@ import com.jcraft.jsch.JSch; +import io.jenkins.plugins.credentials.secretsmanager.util.Lists; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.junit.ssh.SshTestGitServer; import org.eclipse.jgit.lib.Repository; @@ -91,7 +92,7 @@ public String getCloneUrl(String repo, String username) { } public static class Builder { - private List repos = Collections.emptyList(); + private List repos = Lists.of(); private Map users = Collections.emptyMap(); public Builder withRepos(String... repos) {