Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] To be able to rotate subscription key #7596

Merged
merged 15 commits into from
Jan 28, 2020
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 @@ -245,11 +245,12 @@
<!-- Checkstyle rules should not check files in generated-test-sources -->
<suppress checks="[a-zA-Z0-9]*" files="[/\\]generated-test-sources[/\\]"/>

<!-- Allows the SasTokenCredentialPolicy in Implementation -->
<!-- Allows the HttpPipelinePolicy derived class in Implementation folder -->
<suppress checks="com.azure.tools.checkstyle.checks.HttpPipelinePolicy" files="com.azure.storage.common.implementation.policy.SasTokenCredentialPolicy.java"/>
<suppress checks="com.azure.tools.checkstyle.checks.HttpPipelinePolicy" files="com.azure.security.keyvault.secrets.implementation.KeyVaultCredentialPolicy.java"/>
<suppress checks="com.azure.tools.checkstyle.checks.HttpPipelinePolicy" files="com.azure.security.keyvault.certificates.implementation.KeyVaultCredentialPolicy.java"/>
<suppress checks="com.azure.tools.checkstyle.checks.HttpPipelinePolicy" files="com.azure.security.keyvault.keys.implementation.KeyVaultCredentialPolicy.java"/>
<suppress checks="com.azure.tools.checkstyle.checks.HttpPipelinePolicy" files="com.azure.ai.textanalytics.implementation.SubscriptionKeyCredentialPolicy.java"/>

<!-- Empty while loop waiting for Reactor stream completion -->
<suppress checks="EmptyBlock" files="com.azure.storage.blob.batch.BlobBatch.java"/>
Expand Down
60 changes: 39 additions & 21 deletions sdk/textanalytics/azure-ai-textanalytics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ cognitive services.
```

Use the key as the credential parameter to authenticate the client:
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L45-L48 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L44-L47 -->
```java
TextAnalyticsClient textAnalyticsClient = new TextAnalyticsClientBuilder()
.subscriptionKey(SUBSCRIPTION_KEY)
.endpoint(ENDPOINT)
.subscriptionKey(new TextAnalyticsApiKeyCredential("{subscription_key}"))
.endpoint("{endpoint}")
.buildClient();
```

Expand All @@ -100,10 +100,10 @@ cognitive services.
AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET

Use the returned token credential to authenticate the client:
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L65-L68 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L64-L67 -->
```java
TextAnalyticsAsyncClient textAnalyticsClient = new TextAnalyticsClientBuilder()
.endpoint(ENDPOINT)
.endpoint("{endpoint}")
.credential(new DefaultAzureCredentialBuilder().build())
.buildAsyncClient();
```
Expand All @@ -114,14 +114,27 @@ analyze sentiment, recognize entities, detect language, and extract key phrases
To create a client object, you will need the cognitive services or text analytics endpoint to
your resource and a subscription key that allows you access:

<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L45-L48 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L44-L47 -->
```java
TextAnalyticsClient textAnalyticsClient = new TextAnalyticsClientBuilder()
.subscriptionKey(SUBSCRIPTION_KEY)
.endpoint(ENDPOINT)
.subscriptionKey(new TextAnalyticsApiKeyCredential("{subscription_key}"))
.endpoint("{endpoint}")
.buildClient();
```

#### Rotate existing subscription key
The Azure Text Analytics client library provide a way to rotate the existing subscription key.

<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L174-L180 -->
```java
TextAnalyticsApiKeyCredential credential = new TextAnalyticsApiKeyCredential("{expired_subscription_key}");
TextAnalyticsClient textAnalyticsClient = new TextAnalyticsClientBuilder()
.subscriptionKey(credential)
.endpoint("{endpoint}")
.buildClient();

credential.updateCredential("{new_subscription_key}");
```
## Key concepts

### Text Input
Expand Down Expand Up @@ -186,23 +199,23 @@ The following sections provide several code snippets covering some of the most c
Text analytics support both synchronous and asynchronous client creation by using
`TextAnalyticsClientBuilder`,

<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L45-L48 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L44-L47 -->
``` java
TextAnalyticsClient textAnalyticsClient = new TextAnalyticsClientBuilder()
.subscriptionKey(SUBSCRIPTION_KEY)
.endpoint(ENDPOINT)
.subscriptionKey(new TextAnalyticsApiKeyCredential("{subscription_key}"))
.endpoint("{endpoint}")
.buildClient();
```
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L55-L58 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L54-L57 -->
``` java
TextAnalyticsAsyncClient textAnalyticsClient = new TextAnalyticsClientBuilder()
.subscriptionKey(SUBSCRIPTION_KEY)
.endpoint(ENDPOINT)
.subscriptionKey(new TextAnalyticsApiKeyCredential("{subscription_key}"))
.endpoint("{endpoint}")
.buildAsyncClient();
```

### Detect language
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L75-L82 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L74-L81 -->
```java
String inputText = "Bonjour tout le monde";

Expand All @@ -215,7 +228,7 @@ for (DetectedLanguage detectedLanguage : textAnalyticsClient.detectLanguage(inpu
```

### Recognize entity
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L89-L98 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L88-L97 -->
```java
String text = "Satya Nadella is the CEO of Microsoft";

Expand All @@ -230,7 +243,7 @@ for (NamedEntity entity : textAnalyticsClient.recognizeEntities(text).getNamedEn
```

### Recognize PII(Personally Identifiable Information) entity
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L105-L114 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L104-L113 -->
```java
String text = "My SSN is 555-55-5555";

Expand All @@ -245,7 +258,7 @@ for (NamedEntity entity : textAnalyticsClient.recognizePiiEntities(text).getName
```

### Recognize linked entity
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L121-L128 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L120-L127 -->

```java
String text = "Old Faithful is a geyser at Yellowstone Park.";
Expand All @@ -258,7 +271,7 @@ for (LinkedEntity linkedEntity : textAnalyticsClient.recognizeLinkedEntities(tex
}
```
### Extract key phrases
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L135-L139 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L134-L138 -->
```java
String text = "My cat might need to see a veterinarian.";

Expand All @@ -268,7 +281,7 @@ for (String keyPhrase : textAnalyticsClient.extractKeyPhrases(text).getKeyPhrase
```

### Analyze sentiment
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L146-L152 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L145-L151 -->
```java
String text = "The hotel was dark and unclean.";

Expand All @@ -285,8 +298,13 @@ Text Analytics clients raise exceptions. For example, if you try to detect the l
document IDs, `400` error is return that indicating bad request. In the following code snippet, the error is handled
gracefully by catching the exception and display the additional information about the error.

<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L164-L168 -->
<!-- embedme ./src/samples/java/com/azure/ai/textanalytics/ReadmeSamples.java#L158-L167 -->
```java
List<DetectLanguageInput> inputs = Arrays.asList(
new DetectLanguageInput("1", "This is written in English.", "us"),
new DetectLanguageInput("2", "Este es un document escrito en Español.", "es")
);

try {
textAnalyticsClient.detectBatchLanguages(inputs);
} catch (HttpResponseException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

package com.azure.ai.textanalytics;

import com.azure.ai.textanalytics.implementation.SubscriptionKeyCredentialPolicy;
import com.azure.ai.textanalytics.implementation.TextAnalyticsClientImpl;
import com.azure.ai.textanalytics.implementation.TextAnalyticsClientImplBuilder;
import com.azure.ai.textanalytics.models.TextAnalyticsApiKeyCredential;
import com.azure.ai.textanalytics.models.TextAnalyticsClientOptions;
import com.azure.core.annotation.ServiceClientBuilder;
import com.azure.core.credential.TokenCredential;
Expand Down Expand Up @@ -42,7 +44,8 @@
*
* <p>
* The client needs the service endpoint of the Azure Text Analytics to access the resource service.
* {@link #subscriptionKey(String) subscriptionKey(String)} or
* {@link #subscriptionKey(TextAnalyticsApiKeyCredential)
* subscriptionKey(TextAnalyticsApiKeyCredential)} or
* {@link #credential(TokenCredential) credential(TokenCredential)} give the builder access credential.
* </p>
*
Expand Down Expand Up @@ -74,7 +77,6 @@ public final class TextAnalyticsClientBuilder {
private static final String CONTENT_TYPE_HEADER_VALUE = "application/json";
private static final String ACCEPT_HEADER = "Accept";
private static final String TEXT_ANALYTICS_PROPERTIES = "azure-ai-textanalytics.properties";
private static final String OCP_APIM_SUBSCRIPTION_KEY = "Ocp-Apim-Subscription-Key";
private static final String NAME = "name";
private static final String VERSION = "version";
private static final RetryPolicy DEFAULT_RETRY_POLICY = new RetryPolicy("retry-after-ms", ChronoUnit.MILLIS);
Expand All @@ -87,7 +89,7 @@ public final class TextAnalyticsClientBuilder {
private final String clientVersion;

private String endpoint;
private String subscriptionKey;
private TextAnalyticsApiKeyCredential credential;
private TokenCredential tokenCredential;
private HttpClient httpClient;
private HttpLogOptions httpLogOptions;
Expand Down Expand Up @@ -126,7 +128,7 @@ public TextAnalyticsClientBuilder() {
*
* @return A TextAnalyticsClient with the options set from the builder.
* @throws NullPointerException if {@link #endpoint(String) endpoint} or
* {@link #subscriptionKey(String) subscriptionKey} has not been set.
* {@link #subscriptionKey(TextAnalyticsApiKeyCredential) subscriptionKey} has not been set.
* @throws IllegalArgumentException if {@link #endpoint(String) endpoint} cannot be parsed into a valid URL.
*/
public TextAnalyticsClient buildClient() {
Expand All @@ -146,7 +148,7 @@ public TextAnalyticsClient buildClient() {
*
* @return A TextAnalyticsAsyncClient with the options set from the builder.
* @throws NullPointerException if {@link #endpoint(String) endpoint} or
* {@link #subscriptionKey(String) subscriptionKey} has not been set.
* {@link #subscriptionKey(TextAnalyticsApiKeyCredential) subscriptionKey} has not been set.
* @throws IllegalArgumentException if {@link #endpoint(String) endpoint} cannot be parsed into a valid URL.
*/
public TextAnalyticsAsyncClient buildAsyncClient() {
Expand All @@ -170,8 +172,8 @@ public TextAnalyticsAsyncClient buildAsyncClient() {
if (tokenCredential != null) {
// User token based policy
policies.add(new BearerTokenAuthenticationPolicy(tokenCredential, DEFAULT_SCOPE));
} else if (subscriptionKey != null) {
headers.put(OCP_APIM_SUBSCRIPTION_KEY, subscriptionKey);
} else if (credential != null) {
policies.add(new SubscriptionKeyCredentialPolicy(credential));
} else {
// Throw exception that credential and tokenCredential cannot be null
throw logger.logExceptionAsError(
Expand Down Expand Up @@ -235,21 +237,28 @@ public TextAnalyticsClientBuilder endpoint(String endpoint) {
} catch (MalformedURLException ex) {
throw logger.logExceptionAsWarning(new IllegalArgumentException("'endpoint' must be a valid URL", ex));
}
this.endpoint = endpoint;

if (endpoint.endsWith("/")) {
this.endpoint = endpoint.substring(0, endpoint.length() - 1);
} else {
this.endpoint = endpoint;
}

return this;
}

/**
* Sets the credential to use when authenticating HTTP requests for this TextAnalyticsClientBuilder.
*
* @param subscriptionKey subscription key
* @param subscriptionKeyCredential subscription key credential
*
* @return The updated TextAnalyticsClientBuilder object.
* @throws NullPointerException If {@code subscriptionKey} is {@code null}
* @throws NullPointerException If {@code subscriptionKeyCredential} is {@code null}
*/
public TextAnalyticsClientBuilder subscriptionKey(String subscriptionKey) {
Objects.requireNonNull(subscriptionKey, "'subscriptionKey' cannot be null.");
this.subscriptionKey = subscriptionKey;
public TextAnalyticsClientBuilder subscriptionKey(
TextAnalyticsApiKeyCredential subscriptionKeyCredential) {
Objects.requireNonNull(subscriptionKeyCredential, "'subscriptionKeyCredential' cannot be null.");
this.credential = subscriptionKeyCredential;
return this;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.ai.textanalytics.implementation;

import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpPipelineNextPolicy;
import com.azure.core.http.HttpResponse;
import com.azure.core.http.policy.HttpPipelinePolicy;
import com.azure.ai.textanalytics.models.TextAnalyticsApiKeyCredential;
import reactor.core.publisher.Mono;

/**
* Policy that adds the {@link TextAnalyticsApiKeyCredential} into the request's `Ocp-Apim-Subscription-Key`
* header.
*/
public final class SubscriptionKeyCredentialPolicy implements HttpPipelinePolicy {
private static final String OCP_APIM_SUBSCRIPTION_KEY = "Ocp-Apim-Subscription-Key";
private final TextAnalyticsApiKeyCredential credential;

/**
* Creates a {@link SubscriptionKeyCredentialPolicy} pipeline policy that adds the
* {@link TextAnalyticsApiKeyCredential} into the request's `Ocp-Apim-Subscription-Key` header.
*
* @param credential the {@link TextAnalyticsApiKeyCredential} credential used to create the policy.
*/
public SubscriptionKeyCredentialPolicy(TextAnalyticsApiKeyCredential credential) {
this.credential = credential;
}

@Override
public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) {
context.getHttpRequest().setHeader(OCP_APIM_SUBSCRIPTION_KEY, credential.getSubscriptionKey());
return next.process();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.ai.textanalytics.models;

import java.util.Objects;

/**
* Subscription key credential that shared across cognitive services, or restrict to single service.
*
* <p>Be able to rotate an existing subscription key</p>
* {@codesnippet com.azure.ai.textanalytics.models.TextAnalyticsApiKeyCredential}
*
*/
public final class TextAnalyticsApiKeyCredential {
private volatile String subscriptionKey;

/**
* Creates a {@link TextAnalyticsApiKeyCredential} model that describes subscription key for
* authentication.
*
* @param subscriptionKey the subscription key for authentication
*/
public TextAnalyticsApiKeyCredential(String subscriptionKey) {
this.subscriptionKey = Objects.requireNonNull(subscriptionKey, "`subscriptionKey` cannot be null.");
}

/**
* Get the subscription key.
*
* @return the subscription key
*/
public String getSubscriptionKey() {
return this.subscriptionKey;
}

/**
* Set the subscription key.
*
* @param subscriptionKey the subscription key for authentication
*/
public void updateCredential(String subscriptionKey) {
this.subscriptionKey = Objects.requireNonNull(subscriptionKey, "`subscriptionKey` cannot be null.");
}
}
Loading