Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ private AwsExecutionContextBuilder() {
ExecutionInterceptorChain executionInterceptorChain =
new ExecutionInterceptorChain(clientConfig.option(SdkClientOption.EXECUTION_INTERCEPTORS));

executionAttributes.putAttribute(SdkInternalExecutionAttribute.AUTH_SCHEME_SNAPSHOT_PRE_INTERCEPTORS,
executionAttributes.getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME));

InterceptorContext interceptorContext = InterceptorContext.builder()
.request(originalRequest)
.asyncRequestBody(executionParams.getAsyncRequestBody())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Collectors;
Expand All @@ -35,6 +36,7 @@
import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme;
import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption;
import software.amazon.awssdk.http.auth.spi.signer.HttpSigner;
import software.amazon.awssdk.http.auth.spi.signer.SignerProperty;
import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
import software.amazon.awssdk.identity.spi.Identity;
import software.amazon.awssdk.identity.spi.IdentityProvider;
Expand Down Expand Up @@ -125,21 +127,51 @@ public static SelectedAuthScheme<? extends Identity> selectAuthScheme(

/**
* Merge properties from any pre-existing auth scheme into the selected one.
*
* After auth scheme resolution produces a fresh selectedAuthScheme, this method ensures that any signer properties
* explicitly set by interceptors (e.g., signing region override) take priority over the resolved values.
*/
public static <T extends Identity> SelectedAuthScheme<T> mergePreExistingAuthSchemeProperties(
SelectedAuthScheme<T> selectedAuthScheme,
ExecutionAttributes executionAttributes) {

// The "existing" auth scheme is what's currently on SELECTED_AUTH_SCHEME - potentially modified by interceptors.
SelectedAuthScheme<?> existingAuthScheme =
executionAttributes.getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME);

if (existingAuthScheme == null) {
return selectedAuthScheme;
}

// Snapshot taken before interceptors ran — used to detect what interceptors changed.
SelectedAuthScheme<?> authSchemeBeforeInterceptors =
executionAttributes.getAttribute(SdkInternalExecutionAttribute.AUTH_SCHEME_SNAPSHOT_PRE_INTERCEPTORS);

// If the auth scheme option is the same object reference before and after
// interceptors, no interceptor modified it — skip the merge entirely.
if (authSchemeBeforeInterceptors != null &&
authSchemeBeforeInterceptors.authSchemeOption() == existingAuthScheme.authSchemeOption()) {
return selectedAuthScheme;
}

// Start with the freshly resolved auth scheme as the base.
AuthSchemeOption.Builder mergedOption = selectedAuthScheme.authSchemeOption().toBuilder();

// For each signer property on the interceptor-modified scheme:
// If the interceptor changed it (differs from pre-interceptor snapshot), apply interceptor override
// If unchanged (same as before interceptors) only add if not already on the resolved scheme
existingAuthScheme.authSchemeOption().forEachSignerProperty(new AuthSchemeOption.SignerPropertyConsumer() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we benchmarked this? I'm a little concerned about the performance impact - I'm wondering if theres some way we could at least short circuit this when no interceptors have run. Or can we do an identity check on the authSchemeOption (like beforeInterceptors.authSchemeOption() == selectedAuthScheme.authSchemeOption() ) -since they are immutable, if you change them, it requires building a new object, so could be a quick way to see if any mutation has happened?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Added the check to skips the merge when no interceptors have run.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some inline comments explaining each step. This class is too complex to understand...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments explaining that.

@Override
public <S> void accept(SignerProperty<S> key, S value) {
if (wasModifiedByInterceptor(authSchemeBeforeInterceptors, key, value)) {
mergedOption.putSignerProperty(key, value);
} else {
mergedOption.putSignerPropertyIfAbsent(key, value);
}
}
});

existingAuthScheme.authSchemeOption().forEachIdentityProperty(mergedOption::putIdentityPropertyIfAbsent);
existingAuthScheme.authSchemeOption().forEachSignerProperty(mergedOption::putSignerPropertyIfAbsent);

return new SelectedAuthScheme<>(
selectedAuthScheme.identity(),
Expand All @@ -148,6 +180,62 @@ public static <T extends Identity> SelectedAuthScheme<T> mergePreExistingAuthSch
);
}

/**
* Returns true if the given property value differs from what it was before interceptors ran,
* meaning an interceptor explicitly changed it.
*/
private static <T> boolean wasModifiedByInterceptor(SelectedAuthScheme<?> authSchemeBeforeInterceptors,
SignerProperty<T> key, T currentValue) {
T originalValue = authSchemeBeforeInterceptors.authSchemeOption().signerProperty(key);
return !Objects.equals(originalValue, currentValue);
}

/**
* Re-applies interceptor-modified signer properties onto the current auth scheme.
* Called after endpoint resolution, which may have overwritten properties that interceptors set.
*/
public static void applyInterceptorModifiedProperties(SelectedAuthScheme<?> currentScheme,
SelectedAuthScheme<?> authSchemeBeforeInterceptors,
SelectedAuthScheme<?> afterInterceptors,
ExecutionAttributes attrs) {
if (afterInterceptors == null) {
return;
}
doApplyInterceptorModifiedProperties(currentScheme, authSchemeBeforeInterceptors, afterInterceptors, attrs);
}

@SuppressWarnings("unchecked")
private static <T extends Identity> void doApplyInterceptorModifiedProperties(
SelectedAuthScheme<T> currentScheme,
SelectedAuthScheme<?> authSchemeBeforeInterceptors,
SelectedAuthScheme<?> afterInterceptors,
ExecutionAttributes attrs) {

// Start with the current endpoint resolved auth scheme as the base.
AuthSchemeOption.Builder mergedOption = currentScheme.authSchemeOption().toBuilder();
boolean[] changed = {false};

// For each property on the post-interceptor scheme, check if the interceptor changed it.
// If yes, apply it onto the current scheme.
afterInterceptors.authSchemeOption().forEachSignerProperty(new AuthSchemeOption.SignerPropertyConsumer() {
@Override
public <S> void accept(SignerProperty<S> key, S value) {
if (wasModifiedByInterceptor(authSchemeBeforeInterceptors, key, value)) {
mergedOption.putSignerProperty(key, value);
changed[0] = true;
}
}
});

// Only update SELECTED_AUTH_SCHEME if at least one property was re-applied.
if (changed[0]) {
attrs.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME,
new SelectedAuthScheme<>(currentScheme.identity(),
currentScheme.signer(),
mergedOption.build()));
}
}

private static <T extends Identity> SelectedAuthScheme<T> trySelectAuthScheme(
AuthSchemeOption authOption,
AuthScheme<T> authScheme,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,22 @@ public final class SdkInternalExecutionAttribute extends SdkExecutionAttribute {
public static final ExecutionAttribute<SelectedAuthScheme<?>> SELECTED_AUTH_SCHEME =
new ExecutionAttribute<>("SelectedAuthScheme");

/**
* Snapshot of {@link #SELECTED_AUTH_SCHEME} taken before execution interceptors run.
* Used by {@code AuthSchemeResolver#mergePreExistingAuthSchemeProperties} to detect which signer properties
* were explicitly modified by interceptors (and should therefore override the freshly-resolved values).
*/
public static final ExecutionAttribute<SelectedAuthScheme<?>> AUTH_SCHEME_SNAPSHOT_PRE_INTERCEPTORS =
new ExecutionAttribute<>("AuthSchemeSnapshotPreInterceptors");

/**
* Snapshot of {@link #SELECTED_AUTH_SCHEME} taken after interceptors run but before auth scheme resolution.
* Together with {@link #AUTH_SCHEME_SNAPSHOT_PRE_INTERCEPTORS}, this allows detecting which signer properties
* were explicitly modified by interceptors so they can be re-applied after endpoint resolution.
*/
public static final ExecutionAttribute<SelectedAuthScheme<?>> AUTH_SCHEME_SNAPSHOT_POST_INTERCEPTORS =
new ExecutionAttribute<>("AuthSchemeSnapshotPostInterceptors");

/**
* The supported compression algorithms for an operation, and whether the operation is streaming or not.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request, Re
SelectedAuthScheme<? extends Identity> selectedAuthScheme =
AuthSchemeResolver.selectAuthScheme(authOptions, authSchemes, identityProviders, metricCollector);

executionAttributes.putAttribute(SdkInternalExecutionAttribute.AUTH_SCHEME_SNAPSHOT_POST_INTERCEPTORS,
executionAttributes.getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME));

selectedAuthScheme = AuthSchemeResolver.mergePreExistingAuthSchemeProperties(selectedAuthScheme, executionAttributes);

executionAttributes.putAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME, selectedAuthScheme);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.core.ClientEndpointProvider;
import software.amazon.awssdk.core.SdkRequest;
import software.amazon.awssdk.core.SelectedAuthScheme;
import software.amazon.awssdk.core.http.auth.AuthSchemeResolver;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute;
Expand Down Expand Up @@ -70,6 +72,8 @@ public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request, Re
Endpoint endpoint = resolver.resolve(sdkRequest, attrs);
Duration resolveEndpointDuration = Duration.ofNanos(System.nanoTime() - resolveEndpointStart);

reapplyInterceptorModifiedAuthProperties(attrs);

MetricCollector metricCollector = attrs.getAttribute(SdkExecutionAttribute.API_CALL_METRIC_COLLECTOR);
if (metricCollector != null) {
metricCollector.reportMetric(CoreMetric.ENDPOINT_RESOLVE_DURATION, resolveEndpointDuration);
Expand Down Expand Up @@ -140,4 +144,21 @@ private static String combinePath(String clientEndpointPath, String requestPath,
String requestPathWithClientPathRemoved = StringUtils.replaceOnce(requestPath, clientEndpointPath, "");
return SdkHttpUtils.appendUri(resolvedUriPath, requestPathWithClientPathRemoved);
}

private static void reapplyInterceptorModifiedAuthProperties(ExecutionAttributes attrs) {
SelectedAuthScheme<?> currentScheme = attrs.getAttribute(SdkInternalExecutionAttribute.SELECTED_AUTH_SCHEME);
if (currentScheme == null) {
return;
}
SelectedAuthScheme<?> beforeInterceptors =
attrs.getAttribute(SdkInternalExecutionAttribute.AUTH_SCHEME_SNAPSHOT_PRE_INTERCEPTORS);

SelectedAuthScheme<?> afterInterceptors =
attrs.getAttribute(SdkInternalExecutionAttribute.AUTH_SCHEME_SNAPSHOT_POST_INTERCEPTORS);
if (afterInterceptors == null) {
return;
}

AuthSchemeResolver.applyInterceptorModifiedProperties(currentScheme, beforeInterceptors, afterInterceptors, attrs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,7 @@ public void canSetSignerExecutionAttributes_beforeExecution() {
public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes executionAttributes) {
attributeModifications.accept(executionAttributes);
}
},
AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, // Endpoint rules override signing name
AwsSignerExecutionAttribute.SIGNING_REGION, // Endpoint rules override signing region
AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE); // Endpoint rules override double-url-encode
Comment on lines -76 to -78
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we remove those three lines?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously (before our changes), these three properties were already not working in beforeExecution, endpoint rules would overwrite them. That's why they were excluded from the test. But other stages modifyRequest, modifyHttpRequest used to work because they ran after endpoint rules. Now with our changes it works correctly for beforeExecution as well, so removed those exclusions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, could it break users who rely on the buggy behavior?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think so. If the user is explicitly setting those attributes in interceptors, previously we didn't use them, we used the values resolved by endpoint rule. But now with our fix, we will use the values. If they are explicitly setting they are intending to use them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I was thinking about the case where an attribute was set to a wrong value by accident, no one has noticed it because the application is working (SDK not honoring it) and it'll start to fail once we start to honor it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should let it prevent us from fixing it. Just need to document it somewhere... probably changelog entry?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I was thinking about the case where an attribute was set to a wrong value by accident, no one has noticed it because the application is working (SDK not honoring it) and it'll start to fail once we start to honor it

Hmm yeah right, I guess that would be rare. But I'll make sure to document it in changelog.

});
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.lambda.endpoints.LambdaEndpointParams;
import software.amazon.awssdk.services.lambda.endpoints.LambdaEndpointProvider;
import software.amazon.awssdk.services.lambda.endpoints.internal.LambdaResolveEndpointInterceptor;
import software.amazon.awssdk.services.lambda.endpoints.internal.LambdaEndpointResolverUtils;
import software.amazon.awssdk.services.lambda.model.ListFunctionsRequest;
import software.amazon.awssdk.utils.AttributeMap;

Expand Down Expand Up @@ -88,7 +88,7 @@ public void setup() {

@Benchmark
public void resolveEndpoint(Blackhole blackhole) {
LambdaEndpointParams params = LambdaResolveEndpointInterceptor.ruleParams(request, executionAttributes);
LambdaEndpointParams params = LambdaEndpointResolverUtils.ruleParams(request, executionAttributes);
blackhole.consume(provider.resolveEndpoint(params).join());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import software.amazon.awssdk.services.s3.endpoints.S3ClientContextParams;
import software.amazon.awssdk.services.s3.endpoints.S3EndpointParams;
import software.amazon.awssdk.services.s3.endpoints.S3EndpointProvider;
import software.amazon.awssdk.services.s3.endpoints.internal.S3ResolveEndpointInterceptor;
import software.amazon.awssdk.services.s3.endpoints.internal.S3EndpointResolverUtils;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.utils.AttributeMap;

Expand Down Expand Up @@ -121,7 +121,7 @@ public void setup() {

@Benchmark
public void resolveEndpoint(Blackhole blackhole) {
S3EndpointParams params = S3ResolveEndpointInterceptor.ruleParams(request, executionAttributes);
S3EndpointParams params = S3EndpointResolverUtils.ruleParams(request, executionAttributes);
blackhole.consume(provider.resolveEndpoint(params).join());
}

Expand Down
Loading