diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java index 7c042479ff2..254169ea84c 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java @@ -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()) diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java index ea0d8d6db9b..20f6c81228e 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/http/auth/AuthSchemeResolver.java @@ -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; @@ -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; @@ -125,11 +127,15 @@ public static SelectedAuthScheme 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 SelectedAuthScheme mergePreExistingAuthSchemeProperties( SelectedAuthScheme 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); @@ -137,9 +143,35 @@ public static SelectedAuthScheme mergePreExistingAuthSch 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() { + @Override + public void accept(SignerProperty 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(), @@ -148,6 +180,62 @@ public static SelectedAuthScheme mergePreExistingAuthSch ); } + /** + * Returns true if the given property value differs from what it was before interceptors ran, + * meaning an interceptor explicitly changed it. + */ + private static boolean wasModifiedByInterceptor(SelectedAuthScheme authSchemeBeforeInterceptors, + SignerProperty 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 void doApplyInterceptorModifiedProperties( + SelectedAuthScheme 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 void accept(SignerProperty 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 SelectedAuthScheme trySelectAuthScheme( AuthSchemeOption authOption, AuthScheme authScheme, diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkInternalExecutionAttribute.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkInternalExecutionAttribute.java index 349fc35105a..77daf8558a6 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkInternalExecutionAttribute.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkInternalExecutionAttribute.java @@ -212,6 +212,22 @@ public final class SdkInternalExecutionAttribute extends SdkExecutionAttribute { public static final ExecutionAttribute> 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> 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> AUTH_SCHEME_SNAPSHOT_POST_INTERCEPTORS = + new ExecutionAttribute<>("AuthSchemeSnapshotPostInterceptors"); + /** * The supported compression algorithms for an operation, and whether the operation is streaming or not. */ diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AuthSchemeResolutionStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AuthSchemeResolutionStage.java index 62524e42bd9..0c40b2735c4 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AuthSchemeResolutionStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AuthSchemeResolutionStage.java @@ -77,6 +77,9 @@ public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request, Re SelectedAuthScheme 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); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStage.java index b01dc444fa7..6cfe0e54165 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/EndpointResolutionStage.java @@ -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; @@ -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); @@ -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); + } } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/ExecutionAttributeBackwardsCompatibilityTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/ExecutionAttributeBackwardsCompatibilityTest.java index a4816d22e4d..22b8c7f4efa 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/ExecutionAttributeBackwardsCompatibilityTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/ExecutionAttributeBackwardsCompatibilityTest.java @@ -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 + }); } @Test diff --git a/test/sdk-standard-benchmarks/src/main/java/software/amazon/awssdk/benchmark/endpoints/LambdaEndpointResolverBenchmark.java b/test/sdk-standard-benchmarks/src/main/java/software/amazon/awssdk/benchmark/endpoints/LambdaEndpointResolverBenchmark.java index 78e5a3fbd1a..4efad1b6917 100644 --- a/test/sdk-standard-benchmarks/src/main/java/software/amazon/awssdk/benchmark/endpoints/LambdaEndpointResolverBenchmark.java +++ b/test/sdk-standard-benchmarks/src/main/java/software/amazon/awssdk/benchmark/endpoints/LambdaEndpointResolverBenchmark.java @@ -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; @@ -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()); } diff --git a/test/sdk-standard-benchmarks/src/main/java/software/amazon/awssdk/benchmark/endpoints/S3EndpointResolverBenchmark.java b/test/sdk-standard-benchmarks/src/main/java/software/amazon/awssdk/benchmark/endpoints/S3EndpointResolverBenchmark.java index 9571040f369..240891adcfd 100644 --- a/test/sdk-standard-benchmarks/src/main/java/software/amazon/awssdk/benchmark/endpoints/S3EndpointResolverBenchmark.java +++ b/test/sdk-standard-benchmarks/src/main/java/software/amazon/awssdk/benchmark/endpoints/S3EndpointResolverBenchmark.java @@ -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; @@ -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()); }