Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@
@SdkPublicApi
public enum ProxyAuthScheme {
/**
* Basic authentication.
* Basic authentication, as defined by <a href="https://datatracker.ietf.org/doc/html/rfc7617">RFC 7617</a>. Requires a
* username and password.
*/
BASIC("Basic"),

/**
* Kerberos authentication.
* Kerberos authentication, using SPNEGO as defined by
* <a href="https://datatracker.ietf.org/doc/html/rfc4559">RFC 4559</a>.
* <p>
* Credentials are read from the environment Kerberos ticket cache. The client never prompts for a password and never reads a
* keytab, so the environment must already hold a valid ticket-granting ticket, typically obtained by running
* {@code kinit} and verifiable with {@code klist}. The cache location follows the usual Kerberos conventions, including
* the {@code KRB5CCNAME} environment variable. Any username and password configured on the proxy are ignored.
*/
NEGOTIATE("Negotiate"),
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.HashSet;
import java.util.Set;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.http.nio.netty.internal.utils.NettyClientLogger;
import software.amazon.awssdk.utils.ProxyConfigProvider;
import software.amazon.awssdk.utils.ProxyEnvironmentSetting;
import software.amazon.awssdk.utils.ProxySystemSetting;
Expand All @@ -34,6 +35,8 @@
*/
@SdkPublicApi
public final class ProxyConfiguration implements ToCopyableBuilder<ProxyConfiguration.Builder, ProxyConfiguration> {
private static final NettyClientLogger log = NettyClientLogger.getLogger(ProxyConfiguration.class);

private final Boolean useSystemPropertyValues;
private final Boolean useEnvironmentVariablesValues;
private final String scheme;
Expand All @@ -60,6 +63,7 @@ private ProxyConfiguration(BuilderImpl builder) {
this.proxyAuthScheme = builder.proxyAuthScheme;
this.nonProxyHosts = resolveNonProxyHosts(builder, proxyConfigProvider);
validateProxyAuthConfig(proxyAuthScheme, username, password);
warnOnIgnoredCredentials(builder);
}

private static void validateProxyAuthConfig(ProxyAuthScheme proxyAuthScheme, String username, String password) {
Expand All @@ -69,6 +73,21 @@ private static void validateProxyAuthConfig(ProxyAuthScheme proxyAuthScheme, Str
}
}

/**
* NEGOTIATE reads its credentials from the Kerberos ticket cache, so a username and password are dead configuration. Warn
* rather than fail, and only when they were set directly on this builder: values resolved from system properties or
* environment variables may not be under the caller's control, and warning about those would be noise.
*/
private static void warnOnIgnoredCredentials(BuilderImpl builder) {
if (builder.proxyAuthScheme == ProxyAuthScheme.NEGOTIATE
&& (builder.username != null || builder.password != null)) {
log.warn(null, () -> "A proxy username and/or password was configured alongside the "
+ ProxyAuthScheme.NEGOTIATE + " proxy auth scheme, and will be ignored. " +
ProxyAuthScheme.NEGOTIATE + " authenticates using the Kerberos ticket cache. Configure "
+ ProxyAuthScheme.BASIC + " to authenticate with a username and password instead.");
}
}

private static Set<String> resolveNonProxyHosts(BuilderImpl builder, ProxyConfigProvider proxyConfigProvider) {
if (builder.nonProxyHosts != null || proxyConfigProvider == null) {
return builder.nonProxyHosts;
Expand Down Expand Up @@ -278,6 +297,10 @@ public interface Builder extends CopyableBuilder<Builder, ProxyConfiguration> {
* If set to {@link ProxyAuthScheme#BASIC}, {@link #username(String)} and {@link #password(String)} must also be
* configured (directly, or resolved from system properties or environment variables), otherwise
* {@link Builder#build()} throws {@link IllegalArgumentException}.
* <p>
* If set to {@link ProxyAuthScheme#NEGOTIATE}, credentials come from the Kerberos ticket cache rather than from this
* configuration, and any configured username and password are ignored. See {@link ProxyAuthScheme#NEGOTIATE} for the
* environment it requires and for how a missing or expired ticket surfaces.
*
* @param proxyAuthScheme The auth scheme.
* @return This object for method chaining.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
import java.util.Random;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.logging.log4j.Level;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.testutils.LogCaptor;

/**
* Tests for {@link ProxyConfiguration}.
Expand Down Expand Up @@ -209,6 +211,39 @@ void build_negotiateAuthSchemeWithoutCredentials_doesNotThrow() {
assertThat(cfg.proxyAuthScheme()).isEqualTo(ProxyAuthScheme.NEGOTIATE);
}

@Test
void build_negotiateAuthSchemeWithCredentials_warnsAndKeepsBuilding() {
try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) {
ProxyConfiguration cfg = ProxyConfiguration.builder()
.host("localhost")
.port(8888)
.proxyAuthScheme(ProxyAuthScheme.NEGOTIATE)
.username(TEST_USER)
.password(TEST_PASSWORD)
.build();

assertThat(cfg.proxyAuthScheme()).isEqualTo(ProxyAuthScheme.NEGOTIATE);
assertThat(logCaptor.loggedEvents()).singleElement()
.satisfies(event -> assertThat(event.getMessage().getFormattedMessage())
.contains("NEGOTIATE")
.contains("will be ignored"));
}
}

@Test
void build_negotiateAuthSchemeWithSystemPropertyCredentials_doesNotWarn() {
setHttpProxyProperties();

try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) {
ProxyConfiguration.builder()
.proxyAuthScheme(ProxyAuthScheme.NEGOTIATE)
.build();

// Credentials the caller did not set here are not their mistake to fix, so warning about them would be noise.
assertThat(logCaptor.loggedEvents()).isEmpty();
}
}

@Test
void toBuilderModified_doesNotModifySource() {
ProxyConfiguration original = allPropertiesSetConfig();
Expand Down
Loading