Skip to content

Commit

Permalink
[feature] Use Jenkins proxy settings for Secrets Manager communication (
Browse files Browse the repository at this point in the history
#306)

Co-authored-by: Preslav Petkov <[email protected]>
Co-authored-by: Chris Kilding <[email protected]>
  • Loading branch information
3 people authored Dec 5, 2023
1 parent ca3f373 commit a0ad826
Show file tree
Hide file tree
Showing 16 changed files with 361 additions and 6 deletions.
28 changes: 28 additions & 0 deletions docs/client/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,34 @@ unclassified:
secretKey: "${aws-secret-key}" # e.g. wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
```

## Client Configuration

The plugin will use the default AWS client configuration if no overrides are set.

If the Jenkins system-wide HTTP proxy is configured, the plugin will use the Jenkins proxy settings:

```yaml
jenkins:
proxy:
name: "localhost"
port: 5000
userName: "user"
secretPassword: "fake"
```

Alternatively you can set the AWS client configuration for the client. This will take precedence over any Jenkins proxy settings that may be present. (This may be useful if you need to apply different HTTP proxy settings just for Secrets Manager.)

```yaml
unclassified:
awsCredentialsProvider:
client:
clientConfiguration:
proxyHost: "localhost"
proxyPort: 5000
proxyUsername: "user"
proxyPassword: "fake"
```

## Endpoint Configuration

You can set the AWS endpoint configuration for the client.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,54 @@
import com.amazonaws.services.secretsmanager.AWSSecretsManager;
import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder;
import hudson.Extension;
import hudson.ProxyConfiguration;
import hudson.Util;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.util.ListBoxModel;
import hudson.util.Secret;
import io.jenkins.plugins.credentials.secretsmanager.Messages;
import io.jenkins.plugins.credentials.secretsmanager.config.credentialsProvider.CredentialsProvider;
import io.jenkins.plugins.credentials.secretsmanager.config.credentialsProvider.DefaultAWSCredentialsProviderChain;
import jenkins.model.Jenkins;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import javax.annotation.Nonnull;
import java.io.Serializable;
import java.util.Objects;
import java.util.Optional;

public class Client extends AbstractDescribableImpl<Client> implements Serializable {

private static final long serialVersionUID = 1L;

private ClientConfiguration clientConfiguration;

private CredentialsProvider credentialsProvider;

private EndpointConfiguration endpointConfiguration;

private String region;

@DataBoundConstructor
public Client(CredentialsProvider credentialsProvider, EndpointConfiguration endpointConfiguration, String region) {
public Client(ClientConfiguration clientConfiguration, CredentialsProvider credentialsProvider, EndpointConfiguration endpointConfiguration, String region) {
this.clientConfiguration = clientConfiguration;
this.credentialsProvider = credentialsProvider;
this.endpointConfiguration = endpointConfiguration;
this.region = region;
}

public ClientConfiguration getClientConfiguration() {
return clientConfiguration;
}

@DataBoundSetter
public void setClientConfiguration(ClientConfiguration clientConfiguration) {
this.clientConfiguration = clientConfiguration;
}

Check warning on line 53 in src/main/java/io/jenkins/plugins/credentials/secretsmanager/config/Client.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 52-53 are not covered by tests

public EndpointConfiguration getEndpointConfiguration() {
return endpointConfiguration;
}
Expand Down Expand Up @@ -63,9 +79,43 @@ public void setRegion(String region) {
this.region = Util.fixEmptyAndTrim(region);
}

private static Optional<ProxyConfiguration> getProxyConfiguration() {
// jenkins object could be null
final var maybeJenkins = Optional.ofNullable(Jenkins.getInstanceOrNull());

// proxy object could also be null
return maybeJenkins.flatMap(j -> Optional.ofNullable(j.getProxy()));
}

static com.amazonaws.ClientConfiguration toClientConfiguration(ProxyConfiguration proxyConfiguration) {
final var configuration = new com.amazonaws.ClientConfiguration();

configuration.setProxyHost(proxyConfiguration.getName());
configuration.setProxyPort(proxyConfiguration.getPort());
configuration.setProxyUsername(proxyConfiguration.getUserName());
configuration.setProxyPassword(Secret.toString(proxyConfiguration.getSecretPassword()));
configuration.setNonProxyHosts(proxyConfiguration.getNoProxyHost());

return configuration;
}

public AWSSecretsManager build() {
final AWSSecretsManagerClientBuilder builder = AWSSecretsManagerClientBuilder.standard();

if (clientConfiguration != null) {

Check warning on line 105 in src/main/java/io/jenkins/plugins/credentials/secretsmanager/config/Client.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 105 is only partially covered, one branch is missing
builder.setClientConfiguration(clientConfiguration.build());

Check warning on line 106 in src/main/java/io/jenkins/plugins/credentials/secretsmanager/config/Client.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 106 is not covered by tests
} else {
// If Jenkins has a system-wide proxy configuration set, use it.
// Otherwise, leave the AWS client configuration as default.
final var proxyConfiguration = getProxyConfiguration();

proxyConfiguration.ifPresent(p -> {
final var proxyClientConfiguration = toClientConfiguration(p);

builder.setClientConfiguration(proxyClientConfiguration);
});
}

if (credentialsProvider != null) {
builder.setCredentials(credentialsProvider.build());
}
Expand All @@ -86,14 +136,15 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Client client = (Client) o;
return Objects.equals(credentialsProvider, client.credentialsProvider) &&
return Objects.equals(clientConfiguration, client.clientConfiguration) &&
Objects.equals(credentialsProvider, client.credentialsProvider) &&
Objects.equals(endpointConfiguration, client.endpointConfiguration) &&
Objects.equals(region, client.region);
}

@Override
public int hashCode() {
return Objects.hash(credentialsProvider, endpointConfiguration, region);
return Objects.hash(clientConfiguration, credentialsProvider, endpointConfiguration, region);

Check warning on line 147 in src/main/java/io/jenkins/plugins/credentials/secretsmanager/config/Client.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 113-147 are not covered by tests
}

@Extension
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package io.jenkins.plugins.credentials.secretsmanager.config;

import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.util.Secret;
import io.jenkins.plugins.credentials.secretsmanager.Messages;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import javax.annotation.Nonnull;
import java.io.Serializable;
import java.util.Objects;

public class ClientConfiguration extends AbstractDescribableImpl<ClientConfiguration> implements Serializable {

private static final long serialVersionUID = 1L;

private String nonProxyHosts;
private String proxyHost;
private Integer proxyPort;
private String proxyUsername;
private Secret proxyPassword;

@DataBoundConstructor
public ClientConfiguration(String nonProxyHosts, String proxyHost, Integer proxyPort, String proxyUsername, Secret proxyPassword) {
this.nonProxyHosts = nonProxyHosts;
this.proxyHost = proxyHost;
this.proxyPort = proxyPort;
this.proxyUsername = proxyUsername;
this.proxyPassword = proxyPassword;
}

public String getNonProxyHosts() {
return nonProxyHosts;
}

@DataBoundSetter
public void setNonProxyHosts(String nonProxyHosts) {
this.nonProxyHosts = nonProxyHosts;
}

public String getProxyHost() {
return proxyHost;
}

@DataBoundSetter
public void setProxyHost(String proxyHost) {
this.proxyHost = proxyHost;
}

public Integer getProxyPort() {
return proxyPort;
}

@DataBoundSetter
public void setProxyPort(Integer proxyPort) {
this.proxyPort = proxyPort;
}

public String getProxyUsername() {
return proxyUsername;
}

@DataBoundSetter
public void setProxyUsername(String proxyUsername) {
this.proxyUsername = proxyUsername;
}

public Secret getProxyPassword() {
return proxyPassword;
}

@DataBoundSetter
public void setProxyPassword(Secret proxyPassword) {
this.proxyPassword = proxyPassword;
}

Check warning on line 78 in src/main/java/io/jenkins/plugins/credentials/secretsmanager/config/ClientConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 36-78 are not covered by tests

@Override
public boolean equals(Object o) {
if (this == o) return true;

Check warning on line 82 in src/main/java/io/jenkins/plugins/credentials/secretsmanager/config/ClientConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 82 is only partially covered, one branch is missing
if (o == null || getClass() != o.getClass()) return false;

Check warning on line 83 in src/main/java/io/jenkins/plugins/credentials/secretsmanager/config/ClientConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 83 is only partially covered, 2 branches are missing
ClientConfiguration that = (ClientConfiguration) o;
return Objects.equals(proxyPort, that.proxyPort) && Objects.equals(nonProxyHosts, that.nonProxyHosts) && Objects.equals(proxyHost, that.proxyHost) && Objects.equals(proxyUsername, that.proxyUsername) && Objects.equals(proxyPassword, that.proxyPassword);

Check warning on line 85 in src/main/java/io/jenkins/plugins/credentials/secretsmanager/config/ClientConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 85 is only partially covered, 5 branches are missing
}

@Override
public int hashCode() {
return Objects.hash(nonProxyHosts, proxyHost, proxyPort, proxyUsername, proxyPassword);
}

public com.amazonaws.ClientConfiguration build() {
final var configuration = new com.amazonaws.ClientConfiguration();

configuration.setNonProxyHosts(nonProxyHosts);
configuration.setProxyHost(proxyHost);
if (proxyPort != null) {
configuration.setProxyPort(proxyPort);
}
configuration.setProxyUsername(proxyUsername);
configuration.setProxyPassword(Secret.toString(proxyPassword));

return configuration;

Check warning on line 104 in src/main/java/io/jenkins/plugins/credentials/secretsmanager/config/ClientConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 90-104 are not covered by tests
}

@Extension
@Symbol("clientConfiguration")
@SuppressWarnings("unused")
public static class DescriptorImpl extends Descriptor<ClientConfiguration> {
@Override
@Nonnull
public String getDisplayName() {
return Messages.clientConfiguration();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public static Duration normalize(Boolean cache) {

protected Object readResolve() {
if (endpointConfiguration != null) {
client = new Client(null, endpointConfiguration, null);
client = new Client(null, null, endpointConfiguration, null);
endpointConfiguration = null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private static Function<String, String> createDescriptionFormatter(PluginConfigu

private static AWSSecretsManager createClient(PluginConfiguration config) {
final Client clientConfig = Optional.ofNullable(config.getClient())
.orElse(new Client(null, null, null));
.orElse(new Client(null, null, null, null));

return clientConfig.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ statik = Static
hide = Hide
removePrefix = Remove Prefix
removePrefixes = Remove Prefixes
prefix = Prefix
prefix = Prefix
clientConfiguration = Client Configuration
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:optionalProperty field="clientConfiguration" title="${%clientConfiguration}" />
<f:dropdownDescriptorSelector field="credentialsProvider" default="${descriptor.defaultCredentialsProvider}" title="${%credentialsProvider}" />
<f:optionalProperty field="endpointConfiguration" title="${%endpointConfiguration}" />
<f:entry title="${%region}" field="region" description="${%leaveBlankToAutoDetect}">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
clientConfiguration = Client Configuration
credentialsProvider = Credentials Provider
endpointConfiguration = Endpoint Configuration
leaveBlankToAutoDetect = Leave blank to auto-detect
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<p>Override the AWS client configuration.</p>
<p>When this is in default mode, the plugin will automatically use the Jenkins HTTP proxy settings if you have configured them.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry title="${%nonProxyHosts}">
<f:textbox field="nonProxyHosts" />
</f:entry>
<f:entry title="${%proxyHost}">
<f:textbox field="proxyHost" />
</f:entry>
<f:entry title="${%proxyPort}">
<f:number field="proxyPort" min="0" max="65535" />
</f:entry>
<f:entry title="${%proxyUsername}">
<f:textbox field="proxyUsername" />
</f:entry>
<f:entry title="${%proxyPassword}">
<f:password field="proxyPassword" />
</f:entry>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
nonProxyHosts = Non-Proxy Hosts
proxyHost = Proxy Host
proxyPort = Proxy Port
proxyUsername = Proxy Username
proxyPassword = Proxy Password
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.jenkins.plugins.credentials.secretsmanager.config;

import hudson.ProxyConfiguration;
import org.junit.Test;

import static org.assertj.core.api.SoftAssertions.assertSoftly;

public class ClientTest {

@Test
public void shouldConvertProxyConfigurationToClientConfiguration() {
// Given
final var host = "localhost";
final var port = 8000;
final var noProxyHost = "example.com";
final var username = "foo";
final var password = "fake";
final var proxyConfiguration = new ProxyConfiguration(host, port, username, password, noProxyHost);

// When
final var clientConfiguration = Client.toClientConfiguration(proxyConfiguration);

// Then
assertSoftly(s -> {
s.assertThat(clientConfiguration.getProxyHost()).as("Host").isEqualTo(host);
s.assertThat(clientConfiguration.getProxyPort()).as("Port").isEqualTo(port);
s.assertThat(clientConfiguration.getProxyUsername()).as("Username").isEqualTo(username);
s.assertThat(clientConfiguration.getProxyPassword()).as("Password").isEqualTo(password);
s.assertThat(clientConfiguration.getNonProxyHosts()).as("Non-Proxy Hosts").isEqualTo(noProxyHost);
});
}
}
Loading

0 comments on commit a0ad826

Please sign in to comment.