diff --git a/sdk/eventgrid/azure-messaging-eventgrid/CHANGELOG.md b/sdk/eventgrid/azure-messaging-eventgrid/CHANGELOG.md index 1577e08355e04..40ec8100e2d51 100644 --- a/sdk/eventgrid/azure-messaging-eventgrid/CHANGELOG.md +++ b/sdk/eventgrid/azure-messaging-eventgrid/CHANGELOG.md @@ -1,7 +1,8 @@ # Release History ## 4.5.0-beta.1 (Unreleased) - +### Features Added +- Added `EventGridPublisherClientBuilder#credential(TokenCredential credential)` to support Azure Active Directory authentication. ## 4.4.0 (2021-06-09) ### New Features diff --git a/sdk/eventgrid/azure-messaging-eventgrid/README.md b/sdk/eventgrid/azure-messaging-eventgrid/README.md index c5bebb8ff7086..3a06dbe68b343 100644 --- a/sdk/eventgrid/azure-messaging-eventgrid/README.md +++ b/sdk/eventgrid/azure-messaging-eventgrid/README.md @@ -54,8 +54,8 @@ az eventgrid domain create --location --resource-group --resource-group ``` +#### Azure Active Directory (AAD) Token authentication +Azure Event Grid provides integration with Azure Active Directory (Azure AD) for identity-based authentication of requests. +With Azure AD, you can use role-based access control (RBAC) to grant access to your Azure Event Grid resources to users, groups, or applications. +To send events to a topic or domain with a `TokenCredential`, the authenticated identity should have the "EventGrid Data Sender" role assigned. + #### Creating the Client ##### Using endpoint and access key to create the client @@ -138,7 +143,7 @@ limited time, you can use it to create the publisher client: Sync client: ```java -EventGridPublisherClient eventGridEventClient = new EventGridPublisherClientBuilder() +EventGridPublisherClient cloudEventClient = new EventGridPublisherClientBuilder() .endpoint("") .credential(new AzureSasCredential("")) .buildCloudEventPublisherClient(); @@ -152,6 +157,27 @@ EventGridPublisherAsyncClient cloudEventAsyncClient = new EventGridP .buildCloudEventPublisherAsyncClient(); ``` +##### Using endpoint and Azure Active Directory (AAD) token credential to create the client +To use the AAD token credential, include `azure-identity` artifact as a dependency. Refer to +[azure-identity README](https://docs.microsoft.com/java/api/overview/azure/identity-readme) for details. + +Sync client: + +```java +EventGridPublisherClient cloudEventClient = new EventGridPublisherClientBuilder() + .endpoint("") + .credential(new DefaultAzureCredentialBuilder().build()) + .buildCloudEventPublisherClient(); +``` +Async client: + +```java +EventGridPublisherAsyncClient cloudEventClient = new EventGridPublisherClientBuilder() + .endpoint("") + .credential(new DefaultAzureCredentialBuilder().build()) + .buildCloudEventPublisherAsyncClient(); +``` + #### Create a SAS token for other people to send events for a limited period of time If you'd like to give permission to other people to publish events to your Event Grid Topic or Domain for some time, you can create a SAS (**Shared Access Signature**) for them so they can create an `EventGridPublisherClient` like the above to use `AzureSasCredential` diff --git a/sdk/eventgrid/azure-messaging-eventgrid/pom.xml b/sdk/eventgrid/azure-messaging-eventgrid/pom.xml index 579acebc7a288..a7d097543c09b 100644 --- a/sdk/eventgrid/azure-messaging-eventgrid/pom.xml +++ b/sdk/eventgrid/azure-messaging-eventgrid/pom.xml @@ -83,6 +83,12 @@ 12.10.0 test + + com.azure + azure-identity + 1.3.3 + test + org.junit.jupiter junit-jupiter-api diff --git a/sdk/eventgrid/azure-messaging-eventgrid/src/main/java/com/azure/messaging/eventgrid/EventGridPublisherClientBuilder.java b/sdk/eventgrid/azure-messaging-eventgrid/src/main/java/com/azure/messaging/eventgrid/EventGridPublisherClientBuilder.java index 7a7c02ae509ed..2a444a0630cfb 100644 --- a/sdk/eventgrid/azure-messaging-eventgrid/src/main/java/com/azure/messaging/eventgrid/EventGridPublisherClientBuilder.java +++ b/sdk/eventgrid/azure-messaging-eventgrid/src/main/java/com/azure/messaging/eventgrid/EventGridPublisherClientBuilder.java @@ -6,6 +6,7 @@ import com.azure.core.annotation.ServiceClientBuilder; import com.azure.core.credential.AzureKeyCredential; import com.azure.core.credential.AzureSasCredential; +import com.azure.core.credential.TokenCredential; import com.azure.core.http.HttpClient; import com.azure.core.http.HttpHeader; import com.azure.core.http.HttpHeaders; @@ -14,6 +15,7 @@ import com.azure.core.http.policy.AddDatePolicy; import com.azure.core.http.policy.AddHeadersPolicy; import com.azure.core.http.policy.AzureKeyCredentialPolicy; +import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; import com.azure.core.http.policy.HttpLogOptions; import com.azure.core.http.policy.HttpLoggingPolicy; import com.azure.core.http.policy.HttpPipelinePolicy; @@ -53,6 +55,7 @@ public final class EventGridPublisherClientBuilder { private static final String EVENTGRID_PROPERTIES = "azure-messaging-eventgrid.properties"; private static final String NAME = "name"; private static final String VERSION = "version"; + private static final String DEFAULT_EVENTGRID_SCOPE = "https://eventgrid.azure.net/.default"; private final String clientName; @@ -70,6 +73,8 @@ public final class EventGridPublisherClientBuilder { private AzureSasCredential sasToken; + private TokenCredential tokenCredential; + private EventGridServiceVersion serviceVersion; private String endpoint; @@ -131,14 +136,29 @@ private EventGridPublisherAsyncClient buildAsyncClient(Class eventClas httpPipelinePolicies.add(new AddDatePolicy()); - // Using token before key if both are set + final int credentialCount = (sasToken != null ? 1 : 0) + (keyCredential != null ? 1 : 0) + + (tokenCredential != null ? 1 : 0); + if (credentialCount > 1) { + throw logger.logExceptionAsError( + new IllegalStateException("More than 1 credentials are set while building a client. " + + "You should set one and only one credential of type 'TokenCredential', 'AzureSasCredential', " + + "or 'AzureKeyCredential'.")); + } else if (credentialCount == 0) { + throw logger.logExceptionAsError( + new IllegalStateException("Missing credential information while building a client." + + "You should set one and only one credential of type 'TokenCredential', 'AzureSasCredential', " + + "or 'AzureKeyCredential'.")); + } if (sasToken != null) { httpPipelinePolicies.add((context, next) -> { context.getHttpRequest().getHeaders().set(AEG_SAS_TOKEN, sasToken.getSignature()); return next.process(); }); - } else { + } else if (keyCredential != null) { httpPipelinePolicies.add(new AzureKeyCredentialPolicy(AEG_SAS_KEY, keyCredential)); + } else { + httpPipelinePolicies.add(new BearerTokenAuthenticationPolicy(this.tokenCredential, + DEFAULT_EVENTGRID_SCOPE)); } httpPipelinePolicies.addAll(policies); @@ -238,7 +258,7 @@ public EventGridPublisherClientBuilder credential(AzureKeyCredential credential) /** * Set the domain or topic authentication using an already obtained Shared Access Signature token. - * @param credential the token credential to use. + * @param credential the sas credential to use. * * @return the builder itself. */ @@ -247,6 +267,19 @@ public EventGridPublisherClientBuilder credential(AzureSasCredential credential) return this; } + /** + * Set the domain or topic authentication using Azure Activity Directory authentication. + * Refer to azure-identity + * + * @param credential the token credential to use. + * + * @return the builder itself. + */ + public EventGridPublisherClientBuilder credential(TokenCredential credential) { + this.tokenCredential = credential; + return this; + } + /** * Set the domain or topic endpoint. This is the address to publish events to. * It must be the full url of the endpoint instead of just the hostname. diff --git a/sdk/eventgrid/azure-messaging-eventgrid/src/samples/java/com/azure/messaging/eventgrid/samples/ReadmeSamples.java b/sdk/eventgrid/azure-messaging-eventgrid/src/samples/java/com/azure/messaging/eventgrid/samples/ReadmeSamples.java index a80ccb8df5c8d..a81d84e0f875d 100644 --- a/sdk/eventgrid/azure-messaging-eventgrid/src/samples/java/com/azure/messaging/eventgrid/samples/ReadmeSamples.java +++ b/sdk/eventgrid/azure-messaging-eventgrid/src/samples/java/com/azure/messaging/eventgrid/samples/ReadmeSamples.java @@ -10,6 +10,7 @@ import com.azure.core.util.BinaryData; import com.azure.core.util.serializer.TypeReference; import com.azure.core.models.CloudEvent; +import com.azure.identity.DefaultAzureCredentialBuilder; import com.azure.messaging.eventgrid.EventGridEvent; import com.azure.messaging.eventgrid.EventGridPublisherAsyncClient; import com.azure.messaging.eventgrid.EventGridPublisherClient; @@ -99,8 +100,8 @@ public void createCustomEventPublisherAsyncClient() { } public void createPublisherClientWithSas() { - EventGridPublisherClient eventGridEventClient = new EventGridPublisherClientBuilder() - .endpoint("") + EventGridPublisherClient cloudEventClient = new EventGridPublisherClientBuilder() + .endpoint("") .credential(new AzureSasCredential("")) .buildCloudEventPublisherClient(); } @@ -224,4 +225,18 @@ public void systemEventDifferentEventData() { } } } + + public void createPublisherClientWithTokenCredential() { + EventGridPublisherClient cloudEventClient = new EventGridPublisherClientBuilder() + .endpoint("") + .credential(new DefaultAzureCredentialBuilder().build()) + .buildCloudEventPublisherClient(); + } + + public void createPublisherAsyncClientWithTokenCredential() { + EventGridPublisherAsyncClient cloudEventClient = new EventGridPublisherClientBuilder() + .endpoint("") + .credential(new DefaultAzureCredentialBuilder().build()) + .buildCloudEventPublisherAsyncClient(); + } } diff --git a/sdk/eventgrid/azure-messaging-eventgrid/src/test/java/com/azure/messaging/eventgrid/EventGridPublisherClientOnlyTests.java b/sdk/eventgrid/azure-messaging-eventgrid/src/test/java/com/azure/messaging/eventgrid/EventGridPublisherClientOnlyTests.java index 7b11e50ec4f9e..7aa49990e0fe7 100644 --- a/sdk/eventgrid/azure-messaging-eventgrid/src/test/java/com/azure/messaging/eventgrid/EventGridPublisherClientOnlyTests.java +++ b/sdk/eventgrid/azure-messaging-eventgrid/src/test/java/com/azure/messaging/eventgrid/EventGridPublisherClientOnlyTests.java @@ -4,6 +4,7 @@ package com.azure.messaging.eventgrid; import com.azure.core.credential.AzureKeyCredential; +import com.azure.identity.DefaultAzureCredentialBuilder; import org.junit.jupiter.api.Test; import java.io.UnsupportedEncodingException; @@ -15,6 +16,7 @@ import java.time.format.DateTimeFormatter; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class EventGridPublisherClientOnlyTests { @@ -49,4 +51,30 @@ public void testGenerateSas() throws UnsupportedEncodingException { assertTrue(sasToken1.contains(unsignedSas)); } + + @Test + public void createPublisherClientWithNoCredentials() { + final String fullEndpoint = String.format("%s?%s=%s", DUMMY_ENDPOINT, "api-version", + EventGridServiceVersion.getLatest().getVersion()); + + assertThrows(IllegalStateException.class, () -> { + new EventGridPublisherClientBuilder() + .endpoint(fullEndpoint) + .buildCloudEventPublisherClient(); + }); + } + + @Test + public void createPublisherClientWithTwoCredentials() { + final String fullEndpoint = String.format("%s?%s=%s", DUMMY_ENDPOINT, "api-version", + EventGridServiceVersion.getLatest().getVersion()); + + assertThrows(IllegalStateException.class, () -> { + new EventGridPublisherClientBuilder() + .endpoint(fullEndpoint) + .credential(new AzureKeyCredential("FakeCredential")) + .credential(new DefaultAzureCredentialBuilder().build()) + .buildCloudEventPublisherClient(); + }); + } } diff --git a/sdk/eventgrid/azure-messaging-eventgrid/src/test/java/com/azure/messaging/eventgrid/EventGridPublisherClientTests.java b/sdk/eventgrid/azure-messaging-eventgrid/src/test/java/com/azure/messaging/eventgrid/EventGridPublisherClientTests.java index 498c1846d7405..00ff1b41b5395 100644 --- a/sdk/eventgrid/azure-messaging-eventgrid/src/test/java/com/azure/messaging/eventgrid/EventGridPublisherClientTests.java +++ b/sdk/eventgrid/azure-messaging-eventgrid/src/test/java/com/azure/messaging/eventgrid/EventGridPublisherClientTests.java @@ -15,6 +15,8 @@ import com.azure.core.util.Context; import com.azure.core.models.CloudEvent; import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.identity.DefaultAzureCredential; +import com.azure.identity.DefaultAzureCredentialBuilder; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; @@ -160,6 +162,32 @@ public void publishWithSasToken() { .verifyComplete(); } + @Test + @Disabled + public void publishWithTokenCredential() { + DefaultAzureCredential defaultCredential = new DefaultAzureCredentialBuilder().build(); + EventGridPublisherAsyncClient egClient = builder + .credential(defaultCredential) + .endpoint(getEndpoint(CLOUD_ENDPOINT)) + .buildCloudEventPublisherAsyncClient(); + + List events = new ArrayList<>(); + events.add(new CloudEvent("/microsoft/testEvent", "Microsoft.MockPublisher.TestEvent", + BinaryData.fromObject(new HashMap() { + { + put("Field1", "Value1"); + put("Field2", "Value2"); + put("Field3", "Value3"); + } + }), CloudEventDataFormat.JSON, "application/json") + .setSubject("Test") + .setTime(OffsetDateTime.now())); + + StepVerifier.create(egClient.sendEventsWithResponse(events, Context.NONE)) + .expectNextMatches(voidResponse -> voidResponse.getStatusCode() == 200) + .verifyComplete(); + } + @Test public void publishCloudEvents() { EventGridPublisherAsyncClient egClient = builder diff --git a/sdk/eventgrid/azure-messaging-eventgrid/src/test/resources/session-records/publishWithTokenCredential.json b/sdk/eventgrid/azure-messaging-eventgrid/src/test/resources/session-records/publishWithTokenCredential.json new file mode 100644 index 0000000000000..95823bab3c354 --- /dev/null +++ b/sdk/eventgrid/azure-messaging-eventgrid/src/test/resources/session-records/publishWithTokenCredential.json @@ -0,0 +1,23 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://REDACTED.westus2-1.eventgrid-int.azure.net/api/events?api-version=2018-01-01", + "Headers" : { + "User-Agent" : "azsdk-java-UnknownName/UnknownVersion (15.0.2; Windows 10; 10.0)", + "x-ms-client-request-id" : "d9d08daa-f908-4338-ab35-ad6042dc5b9c", + "Content-Type" : "application/cloudevents-batch+json; charset=utf-8" + }, + "Response" : { + "content-length" : "0", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains", + "Server" : "Microsoft-HTTPAPI/2.0", + "api-supported-versions" : "2018-01-01", + "retry-after" : "0", + "StatusCode" : "200", + "x-ms-request-id" : "83028bf1-abe4-49ee-a244-ad5e66335d25", + "Date" : "Fri, 25 Jun 2021 21:19:01 GMT" + }, + "Exception" : null + } ], + "variables" : [ ] +} \ No newline at end of file