Skip to content

Commit

Permalink
Cosmos: preview for AAD support (#12622)
Browse files Browse the repository at this point in the history
* Add initial implementation to pass an AAD token to the backend.

* Address PR comments

* Add AAD authorization test against Cosmos public emulator.
Add implementation for missed cases where authorization token migt be computed.

* update pom related dependency

* update pom dependency

* test updates

* address PR feedback

* address PR feedback

* Bug fixes.

* enable AAD auth in the Cosmos public emulator

* update Cosmos emulator startup switch

* update test case to separate access via different clients

* Address PR feedback.

* Remove constructor which creates unused Cosmos resources.

* use HOST and MASTER_KEY for Cosmos connections; these will default to Cosmos public emulator settings.

* Update test case expectations.

* update Sping related test expectations.

* Update Spring tests expectations and fix couple error cases when passing empty strings for endpoints and master keys.

* Fix for scope resolution

* comment out the test until the CI only failure running public emulator is understood.

* update POM dependencies.

* Fix merge related issue.

* various fixes related to copy/clone of an existing Cosmos client instance.

* update test to account for null values such as key, endpoint or credential properties.
  • Loading branch information
milismsft authored Sep 30, 2020
1 parent f5fa3a4 commit 5bb484f
Show file tree
Hide file tree
Showing 21 changed files with 756 additions and 158 deletions.
2 changes: 1 addition & 1 deletion eng/pipelines/templates/stages/cosmos-sdk-client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ stages:
PreRunSteps:
- template: /eng/common/pipelines/templates/steps/cosmos-emulator.yml
parameters:
StartParameters: '-PartitionCount 50 -Consistency Strong -Timeout 600'
StartParameters: '-EnableAadAuthentication -PartitionCount 50 -Consistency Strong -Timeout 600'
- powershell: |
$Key = 'C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=='
$password = ConvertTo-SecureString -String $Key -Force -AsPlainText
Expand Down
20 changes: 20 additions & 0 deletions sdk/cosmos/azure-cosmos/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,26 @@ Licensed under the MIT License.
<artifactId>azure-core</artifactId>
<version>1.8.1</version> <!-- {x-version-update;com.azure:azure-core;dependency} -->
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.1.2</version> <!-- {x-version-update;com.azure:azure-identity;dependency} -->
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-netty</artifactId>
<version>1.6.1</version> <!-- {x-version-update;com.azure:azure-core-http-netty;dependency} -->
<optional>true</optional>
<exclusions>
<exclusion>
<groupId>com.azure</groupId>
<artifactId>azure-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-afterburner</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.azure.core.annotation.ServiceClient;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.credential.TokenCredential;
import com.azure.core.util.Context;
import com.azure.core.util.tracing.Tracer;
import com.azure.cosmos.implementation.AsyncDocumentClient;
Expand Down Expand Up @@ -55,6 +56,7 @@ public final class CosmosAsyncClient implements Closeable {
private final List<CosmosPermissionProperties> permissions;
private final CosmosAuthorizationTokenResolver cosmosAuthorizationTokenResolver;
private final AzureKeyCredential credential;
private final TokenCredential tokenCredential;
private final boolean sessionCapturingOverride;
private final boolean enableTransportClientSharing;
private final TracerProvider tracerProvider;
Expand All @@ -80,6 +82,7 @@ public final class CosmosAsyncClient implements Closeable {
this.permissions = builder.getPermissions();
this.cosmosAuthorizationTokenResolver = builder.getAuthorizationTokenResolver();
this.credential = builder.getCredential();
this.tokenCredential = builder.getTokenCredential();
this.sessionCapturingOverride = builder.isSessionCapturingOverrideEnabled();
this.enableTransportClientSharing = builder.isConnectionSharingAcrossClientsEnabled();
this.contentResponseOnWriteEnabled = builder.isContentResponseOnWriteEnabled();
Expand All @@ -95,6 +98,7 @@ public final class CosmosAsyncClient implements Closeable {
.withCredential(this.credential)
.withTransportClientSharing(this.enableTransportClientSharing)
.withContentResponseOnWriteEnabled(this.contentResponseOnWriteEnabled)
.withTokenCredential(this.tokenCredential)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.azure.cosmos.implementation.AsyncDocumentClient;
import com.azure.cosmos.implementation.ConnectionPolicy;
import com.azure.cosmos.implementation.Document;
import com.azure.cosmos.implementation.Strings;
import com.azure.cosmos.implementation.Warning;
import com.azure.cosmos.implementation.query.Transformer;
import com.azure.cosmos.models.CosmosQueryRequestOptions;
Expand Down Expand Up @@ -86,16 +87,38 @@ public static ConnectionPolicy getConnectionPolicy(CosmosClientBuilder cosmosCli
@Warning(value = INTERNAL_USE_ONLY_WARNING)
public static CosmosClientBuilder cloneCosmosClientBuilder(CosmosClientBuilder builder) {
CosmosClientBuilder copy = new CosmosClientBuilder();
if (!Strings.isNullOrEmpty(builder.getEndpoint())) {
copy.endpoint(builder.getEndpoint());
}

copy.endpoint(builder.getEndpoint())
.key(builder.getKey())
if (!Strings.isNullOrEmpty(builder.getKey())) {
copy.key(builder.getKey());
}

if (!Strings.isNullOrEmpty(builder.getResourceToken())) {
copy.resourceToken(builder.getResourceToken());
}

if (builder.getCredential() != null) {
copy.credential(builder.getCredential());
}

if (builder.getTokenCredential() != null) {
copy.credential(builder.getTokenCredential());
}

if (builder.getPermissions() != null) {
copy.permissions(builder.getPermissions());
}

if (builder.getAuthorizationTokenResolver() != null) {
copy.authorizationTokenResolver(builder.getAuthorizationTokenResolver());
}

copy
.directMode(builder.getDirectConnectionConfig())
.gatewayMode(builder.getGatewayConnectionConfig())
.consistencyLevel(builder.getConsistencyLevel())
.credential(builder.getCredential())
.permissions(builder.getPermissions())
.authorizationTokenResolver(builder.getAuthorizationTokenResolver())
.resourceToken(builder.getResourceToken())
.contentResponseOnWriteEnabled(builder.isContentResponseOnWriteEnabled())
.userAgentSuffix(builder.getUserAgentSuffix())
.throttlingRetryOptions(builder.getThrottlingRetryOptions())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.azure.core.annotation.ServiceClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.credential.TokenCredential;
import com.azure.cosmos.implementation.Configs;
import com.azure.cosmos.implementation.ConnectionPolicy;
import com.azure.cosmos.implementation.CosmosAuthorizationTokenResolver;
Expand All @@ -12,6 +13,7 @@

import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
* Helper class to build CosmosAsyncClient {@link CosmosAsyncClient} and CosmosClient {@link CosmosClient}
Expand Down Expand Up @@ -80,6 +82,7 @@ public class CosmosClientBuilder {
private Configs configs = new Configs();
private String serviceEndpoint;
private String keyOrResourceToken;
private TokenCredential tokenCredential;
private ConnectionPolicy connectionPolicy;
private GatewayConnectionConfig gatewayConnectionConfig;
private DirectConnectionConfig directConnectionConfig;
Expand Down Expand Up @@ -199,7 +202,12 @@ CosmosAuthorizationTokenResolver getAuthorizationTokenResolver() {
*/
CosmosClientBuilder authorizationTokenResolver(
CosmosAuthorizationTokenResolver cosmosAuthorizationTokenResolver) {
this.cosmosAuthorizationTokenResolver = cosmosAuthorizationTokenResolver;
this.cosmosAuthorizationTokenResolver = Objects.requireNonNull(cosmosAuthorizationTokenResolver,
"'cosmosAuthorizationTokenResolver' cannot be null.");
this.keyOrResourceToken = null;
this.credential = null;
this.permissions = null;
this.tokenCredential = null;
return this;
}

Expand All @@ -219,7 +227,7 @@ String getEndpoint() {
* @return current Builder
*/
public CosmosClientBuilder endpoint(String endpoint) {
this.serviceEndpoint = endpoint;
this.serviceEndpoint = Objects.requireNonNull(endpoint, "'endpoint' cannot be null.");
return this;
}

Expand All @@ -241,12 +249,16 @@ String getKey() {
* @return current Builder.
*/
public CosmosClientBuilder key(String key) {
this.keyOrResourceToken = key;
this.keyOrResourceToken = Objects.requireNonNull(key, "'key' cannot be null.");
this.cosmosAuthorizationTokenResolver = null;
this.credential = null;
this.permissions = null;
this.tokenCredential = null;
return this;
}

/**
* Sets a resource token used to perform authentication
* Gets a resource token used to perform authentication
* for accessing resource.
*
* @return the resourceToken
Expand All @@ -263,7 +275,37 @@ String getResourceToken() {
* @return current Builder.
*/
public CosmosClientBuilder resourceToken(String resourceToken) {
this.keyOrResourceToken = resourceToken;
this.keyOrResourceToken = Objects.requireNonNull(resourceToken, "'resourceToken' cannot be null.");
this.cosmosAuthorizationTokenResolver = null;
this.credential = null;
this.permissions = null;
this.tokenCredential = null;
return this;
}

/**
* Gets a token credential instance used to perform authentication
* for accessing resource.
*
* @return the token credential.
*/
TokenCredential getTokenCredential() {
return tokenCredential;
}

/**
* Sets the {@link TokenCredential} used to authorize requests sent to the service.
*
* @param credential {@link TokenCredential}.
* @return the updated CosmosClientBuilder
* @throws NullPointerException If {@code credential} is {@code null}.
*/
public CosmosClientBuilder credential(TokenCredential credential) {
this.tokenCredential = Objects.requireNonNull(credential, "'credential' cannot be null.");
this.keyOrResourceToken = null;
this.cosmosAuthorizationTokenResolver = null;
this.credential = null;
this.permissions = null;
return this;
}

Expand All @@ -285,7 +327,11 @@ List<CosmosPermissionProperties> getPermissions() {
* @return current Builder.
*/
public CosmosClientBuilder permissions(List<CosmosPermissionProperties> permissions) {
this.permissions = permissions;
this.permissions = Objects.requireNonNull(permissions, "'permissions' cannot be null.");
this.keyOrResourceToken = null;
this.cosmosAuthorizationTokenResolver = null;
this.credential = null;
this.tokenCredential = null;
return this;
}

Expand Down Expand Up @@ -338,7 +384,11 @@ AzureKeyCredential getCredential() {
* @return current cosmosClientBuilder
*/
public CosmosClientBuilder credential(AzureKeyCredential credential) {
this.credential = credential;
this.credential = Objects.requireNonNull(credential, "'cosmosKeyCredential' cannot be null.");
this.keyOrResourceToken = null;
this.cosmosAuthorizationTokenResolver = null;
this.permissions = null;
this.tokenCredential = null;
return this;
}

Expand Down Expand Up @@ -689,7 +739,8 @@ private void validateConfig() {
ifThrowIllegalArgException(this.serviceEndpoint == null,
"cannot buildAsyncClient client without service endpoint");
ifThrowIllegalArgException(
this.keyOrResourceToken == null && (permissions == null || permissions.isEmpty()) && this.credential == null,
this.keyOrResourceToken == null && (permissions == null || permissions.isEmpty())
&& this.credential == null && this.tokenCredential == null,
"cannot buildAsyncClient client without any one of key, resource token, permissions, and "
+ "azure key credential");
ifThrowIllegalArgException(credential != null && StringUtils.isEmpty(credential.getKey()),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.cosmos.implementation;

import com.azure.core.credential.SimpleTokenCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

/**
* This class is used internally and act as a helper in authorization of
* AAD tokens and its supporting method.
*
*/
public class AadTokenAuthorizationHelper {
public static final String AAD_AUTH_SCHEMA_TYPE_SEGMENT = "type";
public static final String AAD_AUTH_VERSION_SEGMENT = "ver";
public static final String AAD_AUTH_SIGNATURE_SEGMENT = "sig";
public static final String AAD_AUTH_SCHEMA_TYPE_VALUE = "aad";
public static final String AAD_AUTH_VERSION_VALUE = "1.0";
public static final String AAD_AUTH_TOKEN_COSMOS_SCOPE = "https://cosmos.azure.com/.default";
private static final String AUTH_PREFIX =
AAD_AUTH_SCHEMA_TYPE_SEGMENT + "=" + AAD_AUTH_SCHEMA_TYPE_VALUE
+ "&"
+ AAD_AUTH_VERSION_SEGMENT + "=" + AAD_AUTH_VERSION_VALUE
+ "&"
+ AAD_AUTH_SIGNATURE_SEGMENT + "=";
private static final Logger logger = LoggerFactory.getLogger(AadTokenAuthorizationHelper.class);

/**
* This method will try to fetch the AAD token to access the resource and add it to the request headers.

*
* @param request the request headers.
* @param simpleTokenCache token cache that supports caching a token and refreshing it.
* @return the request headers with authorization header updated.
*/
public static Mono<RxDocumentServiceRequest> populateAuthorizationHeader(RxDocumentServiceRequest request, SimpleTokenCache simpleTokenCache) {
if (request == null || request.getHeaders() == null) {
throw new IllegalArgumentException("request");
}
if (simpleTokenCache == null) {
throw new IllegalArgumentException("simpleTokenCache");
}

return getAuthorizationToken(simpleTokenCache)
.map(authorization -> {
request.getHeaders().put(HttpConstants.HttpHeaders.AUTHORIZATION, authorization);
return request;
});
}

public static Mono<String> getAuthorizationToken(SimpleTokenCache simpleTokenCache) {
return simpleTokenCache.getToken()
.map(accessToken -> {
String authorization;
String authorizationPayload = AUTH_PREFIX + accessToken.getToken();

try {
authorization = URLEncoder.encode(authorizationPayload, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Failed to encode authorization token.", e);
}

return authorization;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package com.azure.cosmos.implementation;

import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.credential.TokenCredential;
import com.azure.cosmos.ConsistencyLevel;
import com.azure.cosmos.implementation.apachecommons.lang.StringUtils;
import com.azure.cosmos.models.CosmosItemIdentity;
Expand Down Expand Up @@ -72,6 +73,7 @@ class Builder {
URI serviceEndpoint;
CosmosAuthorizationTokenResolver cosmosAuthorizationTokenResolver;
AzureKeyCredential credential;
TokenCredential tokenCredential;
boolean sessionCapturingOverride;
boolean transportClientSharing;
boolean contentResponseOnWriteEnabled;
Expand Down Expand Up @@ -172,6 +174,17 @@ public Builder withTokenResolver(CosmosAuthorizationTokenResolver cosmosAuthoriz
return this;
}

/**
* This method will accept functional interface TokenCredential which helps in generation authorization
* token per request. AsyncDocumentClient can be successfully initialized with this API without passing any MasterKey, ResourceToken or PermissionFeed.
* @param tokenCredential the token credential
* @return current Builder.
*/
public Builder withTokenCredential(TokenCredential tokenCredential) {
this.tokenCredential = tokenCredential;
return this;
}

private void ifThrowIllegalArgException(boolean value, String error) {
if (value) {
throw new IllegalArgumentException(error);
Expand All @@ -180,10 +193,10 @@ private void ifThrowIllegalArgException(boolean value, String error) {

public AsyncDocumentClient build() {

ifThrowIllegalArgException(this.serviceEndpoint == null, "cannot buildAsyncClient client without service endpoint");
ifThrowIllegalArgException(this.serviceEndpoint == null || StringUtils.isEmpty(this.serviceEndpoint.toString()), "cannot buildAsyncClient client without service endpoint");
ifThrowIllegalArgException(
this.masterKeyOrResourceToken == null && (permissionFeed == null || permissionFeed.isEmpty())
&& this.credential == null,
&& this.credential == null && this.tokenCredential == null,
"cannot buildAsyncClient client without any one of masterKey, " +
"resource token, permissionFeed and azure key credential");
ifThrowIllegalArgException(credential != null && StringUtils.isEmpty(credential.getKey()),
Expand All @@ -197,6 +210,7 @@ public AsyncDocumentClient build() {
configs,
cosmosAuthorizationTokenResolver,
credential,
tokenCredential,
sessionCapturingOverride,
transportClientSharing,
contentResponseOnWriteEnabled);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ public enum AuthorizationTokenType {
SystemReadOnly,
SystemReadWrite,
SystemAll,
ResourceToken
ResourceToken,
AadToken
}
Loading

0 comments on commit 5bb484f

Please sign in to comment.