Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
# ServiceOwners: @miaojiang

# PRLabel: %App Configuration
/sdk/appconfiguration/ @alzimmermsft @Azure/azure-java-sdk
/sdk/appconfiguration/ @mrm9084 @rossgrambo @avanigupta @alzimmermsft @Azure/azure-java-sdk

# ServiceLabel: %App Configuration
# AzureSdkOwners: @mrm9084
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
## 1.9.0-beta.1 (Unreleased)

### Features Added

- Added a pipeline policy to handle query parameters to make sure the keys are always in lower case and in alphabetical order.
- Added audience policy to provide more meaningful error messages for Azure Active Directory authentication failures. The policy detects AAD audience-related errors and provides clear guidance on audience configuration issues.

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.azure.data.appconfiguration.implementation.AzureAppConfigurationImpl;
import com.azure.data.appconfiguration.implementation.ConfigurationClientCredentials;
import com.azure.data.appconfiguration.implementation.ConfigurationCredentialsPolicy;
import com.azure.data.appconfiguration.implementation.AudiencePolicy;
import com.azure.data.appconfiguration.implementation.QueryParamPolicy;
import com.azure.data.appconfiguration.implementation.SyncTokenPolicy;
import com.azure.data.appconfiguration.models.ConfigurationAudience;
Expand Down Expand Up @@ -267,6 +268,9 @@ private HttpPipeline createDefaultHttpPipeline(SyncTokenPolicy syncTokenPolicy,
// Add query parameter reordering policy
policies.add(new QueryParamPolicy());

// Add policy to deal with Audience Exception in new clouds
policies.add(new AudiencePolicy(audience));

policies.addAll(perCallPolicies);
HttpPolicyProviders.addBeforeRetryPolicies(policies);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.data.appconfiguration.implementation;

import com.azure.core.exception.HttpResponseException;
import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpPipelineNextPolicy;
import com.azure.core.http.HttpPipelineNextSyncPolicy;
import com.azure.core.http.HttpResponse;
import com.azure.core.http.policy.HttpPipelinePolicy;
import com.azure.data.appconfiguration.models.ConfigurationAudience;

import reactor.core.publisher.Mono;

/**
* HTTP pipeline policy that handles Azure Active Directory audience-related authentication errors.
* This policy intercepts HTTP responses and provides more meaningful error messages when
* audience configuration issues occur during authentication.
*/
public class AudiencePolicy implements HttpPipelinePolicy {

private static final String NO_AUDIENCE_ERROR_MESSAGE
= "No audience was provided. An audience must be configured to connect to this cloud.";

private static final String INCORRECT_AUDIENCE_ERROR_MESSAGE
= "An incorrect audience was provided. Please update the audience to connect to this cloud.";

private static final String AAD_AUDIENCE_ERROR_CODE = "AADSTS500011";

private final ConfigurationAudience audience;

/**
* Creates a new instance of AudiencePolicy.
*
* @param audience The configuration audience to use for validation. May be null.
*/
public AudiencePolicy(ConfigurationAudience audience) {
this.audience = audience;
}

@Override
public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) {
return next.process().onErrorMap(HttpResponseException.class, this::handleAudienceException);
}

@Override
public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNextSyncPolicy next) {
try {
return next.processSync();
} catch (HttpResponseException ex) {
throw handleAudienceException(ex);
}
}

/**
* Handles audience-related authentication exceptions by providing more meaningful error messages.
*
* @param ex The original HttpResponseException
* @return A new HttpResponseException with improved error message if audience-related, otherwise the original exception
*/
private HttpResponseException handleAudienceException(HttpResponseException ex) {
if (ex.getMessage() != null && ex.getMessage().contains(AAD_AUDIENCE_ERROR_CODE)) {
String message = audience == null ? NO_AUDIENCE_ERROR_MESSAGE : INCORRECT_AUDIENCE_ERROR_MESSAGE;
return new HttpResponseException(message, ex.getResponse(), ex);
}
return ex;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.data.appconfiguration.implementation;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

import com.azure.core.exception.HttpResponseException;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpPipeline;
import com.azure.core.http.HttpPipelineBuilder;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.http.policy.HttpPipelinePolicy;
import com.azure.core.test.SyncAsyncExtension;
import com.azure.core.test.annotation.SyncAsyncTest;
import com.azure.core.test.http.MockHttpResponse;
import com.azure.core.test.http.NoOpHttpClient;
import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationAudience;

import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

/**
* Unit tests for AudiencePolicy
*/
public class AudiencePolicyTest {
private static final String LOCAL_HOST = "http://localhost";
private static final String AAD_AUDIENCE_ERROR_CODE = "AADSTS500011";
private static final String NO_AUDIENCE_ERROR_MESSAGE
= "No audience was provided. An audience must be configured to connect to this cloud.";
private static final String INCORRECT_AUDIENCE_ERROR_MESSAGE
= "An incorrect audience was provided. Please update the audience to connect to this cloud.";

@SyncAsyncTest
public void processWithoutException() {
AudiencePolicy audiencePolicy = new AudiencePolicy(ConfigurationAudience.AZURE_PUBLIC_CLOUD);

HttpPipelinePolicy testPolicy = (context, next) -> {
return next.process();
};

final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient() {
@Override
public Mono<HttpResponse> send(HttpRequest request) {
return Mono.just(new MockHttpResponse(request, 200));
}
}).policies(audiencePolicy, testPolicy).build();

SyncAsyncExtension.execute(() -> sendRequestSync(pipeline), () -> sendRequest(pipeline));
}

@Test
public void processWithNonAudienceException() {
AudiencePolicy audiencePolicy = new AudiencePolicy(ConfigurationAudience.AZURE_PUBLIC_CLOUD);

HttpPipelinePolicy exceptionPolicy = (context, next) -> {
HttpResponseException ex
= new HttpResponseException("Some other error", new MockHttpResponse(context.getHttpRequest(), 401));
return Mono.error(ex);
};

final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient())
.policies(audiencePolicy, exceptionPolicy)
.build();

StepVerifier.create(sendRequest(pipeline))
.expectErrorMatches(throwable -> throwable instanceof HttpResponseException
&& throwable.getMessage().equals("Some other error"))
.verify();

// Test sync version
HttpResponseException thrown = assertThrows(HttpResponseException.class, () -> sendRequestSync(pipeline));
assertEquals("Some other error", thrown.getMessage());
}

@Test
public void processWithAudienceExceptionAndNullAudience() {
AudiencePolicy audiencePolicy = new AudiencePolicy(null);

HttpPipelinePolicy exceptionPolicy = (context, next) -> {
HttpResponseException ex = new HttpResponseException("Error " + AAD_AUDIENCE_ERROR_CODE + " occurred",
new MockHttpResponse(context.getHttpRequest(), 401));
return Mono.error(ex);
};

final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient())
.policies(audiencePolicy, exceptionPolicy)
.build();

StepVerifier.create(sendRequest(pipeline))
.expectErrorMatches(throwable -> throwable instanceof HttpResponseException
&& throwable.getMessage().equals(NO_AUDIENCE_ERROR_MESSAGE))
.verify();

// Test sync version
HttpResponseException thrown = assertThrows(HttpResponseException.class, () -> sendRequestSync(pipeline));
assertEquals(NO_AUDIENCE_ERROR_MESSAGE, thrown.getMessage());
}

@Test
public void processWithAudienceExceptionAndConfiguredAudience() {
AudiencePolicy audiencePolicy = new AudiencePolicy(ConfigurationAudience.AZURE_PUBLIC_CLOUD);

HttpPipelinePolicy exceptionPolicy = (context, next) -> {
HttpResponseException ex = new HttpResponseException("Error " + AAD_AUDIENCE_ERROR_CODE + " occurred",
new MockHttpResponse(context.getHttpRequest(), 401));
return Mono.error(ex);
};

final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new NoOpHttpClient())
.policies(audiencePolicy, exceptionPolicy)
.build();

StepVerifier.create(sendRequest(pipeline))
.expectErrorMatches(throwable -> throwable instanceof HttpResponseException
&& throwable.getMessage().equals(INCORRECT_AUDIENCE_ERROR_MESSAGE))
.verify();

// Test sync version
HttpResponseException thrown = assertThrows(HttpResponseException.class, () -> sendRequestSync(pipeline));
assertEquals(INCORRECT_AUDIENCE_ERROR_MESSAGE, thrown.getMessage());
}

@Test
public void handleAudienceExceptionWithNullMessage() {
AudiencePolicy audiencePolicy = new AudiencePolicy(ConfigurationAudience.AZURE_PUBLIC_CLOUD);

HttpResponseException originalException
= new HttpResponseException(null, new MockHttpResponse(new HttpRequest(HttpMethod.GET, LOCAL_HOST), 401));

// Use reflection to access the private method for testing
try {
java.lang.reflect.Method method
= AudiencePolicy.class.getDeclaredMethod("handleAudienceException", HttpResponseException.class);
method.setAccessible(true);

HttpResponseException result = (HttpResponseException) method.invoke(audiencePolicy, originalException);
assertSame(originalException, result, "Should return original exception when message is null");
} catch (Exception e) {
throw new RuntimeException("Failed to test handleAudienceException with null message", e);
}
}

@Test
public void handleAudienceExceptionWithoutErrorCode() {
AudiencePolicy audiencePolicy = new AudiencePolicy(ConfigurationAudience.AZURE_PUBLIC_CLOUD);

HttpResponseException originalException = new HttpResponseException("Some other error",
new MockHttpResponse(new HttpRequest(HttpMethod.GET, LOCAL_HOST), 401));

// Use reflection to access the private method for testing
try {
java.lang.reflect.Method method
= AudiencePolicy.class.getDeclaredMethod("handleAudienceException", HttpResponseException.class);
method.setAccessible(true);

HttpResponseException result = (HttpResponseException) method.invoke(audiencePolicy, originalException);
assertSame(originalException, result, "Should return original exception when error code is not found");
} catch (Exception e) {
throw new RuntimeException("Failed to test handleAudienceException without error code", e);
}
}

@Test
public void handleAudienceExceptionWithErrorCodeNullAudience() {
AudiencePolicy audiencePolicy = new AudiencePolicy(null);

HttpResponseException originalException
= new HttpResponseException("Error " + AAD_AUDIENCE_ERROR_CODE + " occurred",
new MockHttpResponse(new HttpRequest(HttpMethod.GET, LOCAL_HOST), 401));

// Use reflection to access the private method for testing
try {
java.lang.reflect.Method method
= AudiencePolicy.class.getDeclaredMethod("handleAudienceException", HttpResponseException.class);
method.setAccessible(true);

HttpResponseException result = (HttpResponseException) method.invoke(audiencePolicy, originalException);
assertEquals(NO_AUDIENCE_ERROR_MESSAGE, result.getMessage());
assertSame(originalException.getResponse(), result.getResponse());
} catch (Exception e) {
throw new RuntimeException("Failed to test handleAudienceException with error code and null audience", e);
}
}

@Test
public void handleAudienceExceptionWithErrorCodeConfiguredAudience() {
AudiencePolicy audiencePolicy = new AudiencePolicy(ConfigurationAudience.AZURE_PUBLIC_CLOUD);

HttpResponseException originalException
= new HttpResponseException("Error " + AAD_AUDIENCE_ERROR_CODE + " occurred",
new MockHttpResponse(new HttpRequest(HttpMethod.GET, LOCAL_HOST), 401));

// Use reflection to access the private method for testing
try {
java.lang.reflect.Method method
= AudiencePolicy.class.getDeclaredMethod("handleAudienceException", HttpResponseException.class);
method.setAccessible(true);

HttpResponseException result = (HttpResponseException) method.invoke(audiencePolicy, originalException);
assertEquals(INCORRECT_AUDIENCE_ERROR_MESSAGE, result.getMessage());
assertSame(originalException.getResponse(), result.getResponse());
} catch (Exception e) {
throw new RuntimeException("Failed to test handleAudienceException with error code and configured audience",
e);
}
}

private Mono<HttpResponse> sendRequest(HttpPipeline pipeline) {
return pipeline.send(new HttpRequest(HttpMethod.GET, LOCAL_HOST));
}

private HttpResponse sendRequestSync(HttpPipeline pipeline) {
return pipeline.sendSync(new HttpRequest(HttpMethod.GET, LOCAL_HOST), Context.NONE);
}
}
Loading