diff --git a/eng/jacoco-test-coverage/pom.xml b/eng/jacoco-test-coverage/pom.xml index 7424f244fe62b..314cab44db83a 100644 --- a/eng/jacoco-test-coverage/pom.xml +++ b/eng/jacoco-test-coverage/pom.xml @@ -188,7 +188,7 @@ com.azure azure-cosmos-encryption - 1.13.0-beta.1 + 2.0.0-beta.1 com.azure diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 041c912139880..62996fe3e5ea4 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -97,7 +97,7 @@ com.azure.cosmos.spark:azure-cosmos-spark_3_2-12;1.0.0-beta.1;1.0.0-beta.1 com.azure.cosmos.spark:azure-cosmos-spark_3-1_2-12;4.18.1;4.19.0-beta.1 com.azure.cosmos.spark:azure-cosmos-spark_3-2_2-12;4.18.1;4.19.0-beta.1 com.azure.cosmos.spark:azure-cosmos-spark_3-3_2-12;4.18.1;4.19.0-beta.1 -com.azure:azure-cosmos-encryption;1.12.0;1.13.0-beta.1 +com.azure:azure-cosmos-encryption;1.12.0;2.0.0-beta.1 com.azure:azure-cosmos-test;1.0.0-beta.2;1.0.0-beta.3 com.azure:azure-cosmos-tests;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-data-appconfiguration;1.4.3;1.5.0-beta.1 diff --git a/sdk/cosmos/azure-cosmos-benchmark/pom.xml b/sdk/cosmos/azure-cosmos-benchmark/pom.xml index 85c5a49472bd8..f6c7ef4cc3fdf 100644 --- a/sdk/cosmos/azure-cosmos-benchmark/pom.xml +++ b/sdk/cosmos/azure-cosmos-benchmark/pom.xml @@ -57,7 +57,7 @@ Licensed under the MIT License. com.azure azure-cosmos-encryption - 1.13.0-beta.1 + 2.0.0-beta.1 diff --git a/sdk/cosmos/azure-cosmos-encryption/CHANGELOG.md b/sdk/cosmos/azure-cosmos-encryption/CHANGELOG.md index 5a574d32aac98..bba039889c08b 100644 --- a/sdk/cosmos/azure-cosmos-encryption/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos-encryption/CHANGELOG.md @@ -1,14 +1,12 @@ ## Release History -### 1.13.0-beta.1 (Unreleased) +### 2.0.0-beta.1 (Unreleased) #### Features Added +* Added support for allowing partition key path and id to be part of client encryption policy - See [PR 33648](https://github.com/Azure/azure-sdk-for-java/pull/33648) #### Breaking Changes - -#### Bugs Fixed - -#### Other Changes +* Adds support for ParititonKey and Id encryption, when the PolicyFormatVersion is set to 2 - See [PR 33648](https://github.com/Azure/azure-sdk-for-java/pull/33648) ### 1.12.0 (2023-03-17) diff --git a/sdk/cosmos/azure-cosmos-encryption/pom.xml b/sdk/cosmos/azure-cosmos-encryption/pom.xml index 3f3b1cca5d213..7638446995187 100644 --- a/sdk/cosmos/azure-cosmos-encryption/pom.xml +++ b/sdk/cosmos/azure-cosmos-encryption/pom.xml @@ -13,7 +13,7 @@ Licensed under the MIT License. com.azure azure-cosmos-encryption - 1.13.0-beta.1 + 2.0.0-beta.1 Encryption Plugin for Azure Cosmos DB SDK This Package contains Encryption Plugin for Microsoft Azure Cosmos SDK jar @@ -48,6 +48,7 @@ Licensed under the MIT License. --add-opens com.azure.cosmos.encryption/com.azure.cosmos.encryption.keyprovider=ALL-UNNAMED --add-opens com.azure.cosmos.encryption/com.azure.cosmos.encryption.util=ALL-UNNAMED --add-opens com.azure.cosmos.encryption/com.azure.cosmos.encryption.models=ALL-UNNAMED + --add-opens com.azure.cosmos/com.azure.cosmos.implementation=ALL-UNNAMED diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncClient.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncClient.java index 9de68048575ff..aa42d6639164b 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncClient.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncClient.java @@ -222,7 +222,7 @@ private CosmosContainerProperties getContainerPropertiesWithVersionValidation(Co throw new IllegalArgumentException("Container without client encryption policy cannot be used"); } - if (cosmosContainerResponse.getProperties().getClientEncryptionPolicy().getPolicyFormatVersion() > 1) { + if (cosmosContainerResponse.getProperties().getClientEncryptionPolicy().getPolicyFormatVersion() > 2) { throw new UnsupportedOperationException("This version of the Encryption library cannot be used with this " + "container. Please upgrade to the latest version of the same."); } diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncContainer.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncContainer.java index 353bfc54dc56f..f6a4c84142f0a 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncContainer.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncContainer.java @@ -10,7 +10,11 @@ import com.azure.cosmos.encryption.implementation.Constants; import com.azure.cosmos.encryption.implementation.CosmosResponseFactory; import com.azure.cosmos.encryption.implementation.EncryptionImplementationBridgeHelpers; +import com.azure.cosmos.encryption.implementation.EncryptionProcessor; +import com.azure.cosmos.encryption.implementation.EncryptionSettings; import com.azure.cosmos.encryption.implementation.EncryptionUtils; +import com.azure.cosmos.encryption.implementation.mdesrc.cryptography.MicrosoftDataEncryptionException; +import com.azure.cosmos.encryption.models.SqlQuerySpecWithEncryption; import com.azure.cosmos.implementation.CosmosPagedFluxOptions; import com.azure.cosmos.implementation.HttpConstants; import com.azure.cosmos.implementation.ImplementationBridgeHelpers; @@ -39,22 +43,26 @@ import com.azure.cosmos.models.FeedResponse; import com.azure.cosmos.models.ModelBridgeInternal; import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.PartitionKeyBuilder; import com.azure.cosmos.models.SqlParameter; import com.azure.cosmos.models.SqlQuerySpec; import com.azure.cosmos.util.CosmosPagedFlux; import com.azure.cosmos.util.UtilBridgeInternal; -import com.azure.cosmos.encryption.implementation.EncryptionProcessor; -import com.azure.cosmos.encryption.models.SqlQuerySpecWithEncryption; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; @@ -131,7 +139,7 @@ public Mono> createItem(T item, requestOptions = new CosmosItemRequestOptions(); } byte[] streamPayload = cosmosSerializerToStream(item); - return createItemHelper(streamPayload, null, requestOptions,(Class) item.getClass(), false ); + return createItemHelper(streamPayload, requestOptions,(Class) item.getClass(), false ); } @@ -193,7 +201,93 @@ public Mono> deleteItem(String itemId, PartitionKey partitionKey, CosmosItemRequestOptions requestOptions) { - return container.deleteItem(itemId, partitionKey, requestOptions); + return deleteItemInternal(itemId, partitionKey, requestOptions); + } + + private Mono> deleteItemInternal(String itemId, PartitionKey partitionKey, CosmosItemRequestOptions requestOptions) { + this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync(); + return Mono.just(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(settings -> { + try { + return Mono.zip( + checkAndGetEncryptedId(itemId, settings), + checkAndGetEncryptedPartitionKey(partitionKey, settings) + ).flatMap(encryptedIdPartitionTuple -> container.deleteItem(encryptedIdPartitionTuple.getT1(), encryptedIdPartitionTuple.getT2(), requestOptions)); + } catch (Exception ex) { + return Mono.error(ex); + } + }); + } + + private Mono checkAndGetEncryptedId(String itemId, EncryptionSettings encryptionSettings) + { + if (this.encryptionProcessor.getClientEncryptionPolicy().getIncludedPaths().stream(). + anyMatch(includedPath -> includedPath.getPath().substring(1).equals(Constants.PROPERTY_NAME_ID))) { + return this.getEncryptedItem(encryptionSettings, Constants.PROPERTY_NAME_ID, itemId); + } + return Mono.just(itemId); + } + + private Mono checkAndGetEncryptedPartitionKey(PartitionKey partitionKey, EncryptionSettings encryptionSettings) { + if (encryptionSettings.getPartitionKeyPaths().isEmpty()) { + return Mono.just(partitionKey); + } + + JsonNode partitionKeyNode; + try { + partitionKeyNode = EncryptionUtils.getSimpleObjectMapper().readTree(partitionKey.toString()); + } catch (JsonProcessingException ex) { + return Mono.error(ex); + } + + if (partitionKeyNode.isArray() && partitionKeyNode.size() > 1) { + ArrayNode arrayNode = (ArrayNode) partitionKeyNode; + + return Mono.just(new PartitionKeyBuilder()) + .flatMap(partitionKeyBuilder -> Flux.fromIterable(encryptionSettings.getPartitionKeyPaths()) + .flatMap(path -> { + // case: partition key path is /a/b/c and the client encryption policy has /a in path. + // hence encrypt the partition key value with using its top level path /a since + // /c would have been encrypted in the document using /a's policy. + String partitionKeyPath = path.split("/")[1]; + + String childPartitionKey = arrayNode.elements().next().textValue(); + if (this.encryptionProcessor.getClientEncryptionPolicy().getIncludedPaths().stream(). + anyMatch(includedPath -> includedPath.getPath().substring(1).equals(partitionKeyPath))) { + partitionKeyBuilder.add(childPartitionKey); + return Mono.empty(); + } + return getEncryptedItem(encryptionSettings, partitionKeyPath, childPartitionKey); + }) + .collectList() + .flatMapMany(Flux::fromIterable) + .doOnNext(partitionKeyBuilder::add) + .then(Mono.just(partitionKeyBuilder.build()))); + } else { + return Mono.just(encryptionSettings.getPartitionKeyPaths().get(0)) + .flatMap(path -> { + String partitionKeyPath = path.split("/")[1]; + if (this.encryptionProcessor.getClientEncryptionPolicy().getIncludedPaths().stream(). + noneMatch(includedPath -> includedPath.getPath().substring(1).equals(partitionKeyPath))) { + return Mono.just(partitionKeyNode.elements().next().textValue()); + } + return getEncryptedItem(encryptionSettings, partitionKeyPath, partitionKeyNode.elements().next().textValue()); + }) + .flatMap(encryptedPartitionKey -> Mono.just(new PartitionKey(encryptedPartitionKey))); + } + } + + private Mono getEncryptedItem(EncryptionSettings encryptionSettings, String propertyName, String propertyValue) { + return encryptionSettings + .getEncryptionSettingForPropertyAsync(propertyName, this.encryptionProcessor) + .flatMap(settings -> { + try { + return Mono.just( + this.encryptionProcessor.encryptAndSerializeValue(settings, propertyValue, propertyName)); + } catch (MicrosoftDataEncryptionException ex) { + return Mono.error(ex); + } + }); } /** @@ -225,10 +319,17 @@ public Mono> deleteItem(T item, CosmosItemRequest */ // TODO Make this api public once it is GA in cosmos core library Mono> deleteAllItemsByPartitionKey(PartitionKey partitionKey, CosmosItemRequestOptions requestOptions) { - if (requestOptions == null) { - requestOptions = new CosmosItemRequestOptions(); - } - return container.deleteAllItemsByPartitionKey(partitionKey, requestOptions); + final CosmosItemRequestOptions options = Optional.ofNullable(requestOptions) + .orElse(new CosmosItemRequestOptions()); + + return deleteAllItemsByPartitionKeyInternal(partitionKey, options); + } + + private Mono> deleteAllItemsByPartitionKeyInternal(PartitionKey partitionKey, CosmosItemRequestOptions requestOptions) { + return this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptedSettings -> checkAndGetEncryptedPartitionKey(partitionKey, encryptedSettings)) + .flatMap(encryptedPartitionKey -> container.deleteAllItemsByPartitionKey(encryptedPartitionKey, requestOptions)); } /** @@ -267,7 +368,7 @@ public Mono> upsertItem(T item, CosmosItemRequestOptio } byte[] streamPayload = cosmosSerializerToStream(item); - return upsertItemHelper(streamPayload, null, requestOptions, (Class) item.getClass(), false); + return upsertItemHelper(streamPayload, requestOptions, (Class) item.getClass(), false); } /** @@ -380,11 +481,10 @@ public Mono> readItem(String id, PartitionKey partitionKey, CosmosItemRequestOptions requestOptions, Class classType) { - if (requestOptions == null) { - requestOptions = new CosmosItemRequestOptions(); - } + final CosmosItemRequestOptions options = Optional.ofNullable(requestOptions) + .orElse(new CosmosItemRequestOptions()); - Mono> responseMessageMono = this.readItemHelper(id, partitionKey, requestOptions, false); + Mono> responseMessageMono = this.readItemHelper(id, partitionKey, options, false); return responseMessageMono.publishOn(encryptionScheduler).flatMap(cosmosItemResponse -> setByteArrayContent(cosmosItemResponse, this.encryptionProcessor.decrypt(cosmosItemResponseBuilderAccessor.getByteArrayContent(cosmosItemResponse))) @@ -554,11 +654,10 @@ public Mono> patchItem( checkNotNull(partitionKey, "expected non-null partitionKey for patchItem"); checkNotNull(cosmosPatchOperations, "expected non-null cosmosPatchOperations"); - if (options == null) { - options = new CosmosPatchItemRequestOptions(); - } + final CosmosPatchItemRequestOptions patchOptions = Optional.ofNullable(options) + .orElse(new CosmosPatchItemRequestOptions()); - return patchItemHelper(itemId, partitionKey, cosmosPatchOperations, options, itemType); + return patchItemHelper(itemId, partitionKey, cosmosPatchOperations, patchOptions, itemType); } private Mono> patchItemHelper(String itemId, @@ -623,11 +722,17 @@ private Mono> patchItemInternalHelper(String itemId, boolean isRetry) { setRequestHeaders(requestOptions); - return this.container.patchItem(itemId, partitionKey, encryptedCosmosPatchOperations, requestOptions, itemType).publishOn(encryptionScheduler). - flatMap(cosmosItemResponse -> setByteArrayContent((CosmosItemResponse) cosmosItemResponse, - this.encryptionProcessor.decrypt(cosmosItemResponseBuilderAccessor.getByteArrayContent((CosmosItemResponse) cosmosItemResponse))) - .map(bytes -> this.responseFactory.createItemResponse((CosmosItemResponse) cosmosItemResponse, - itemType))).onErrorResume(exception -> { + return this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptionSettings -> Mono.zip( + checkAndGetEncryptedId(itemId, encryptionSettings), + checkAndGetEncryptedPartitionKey(partitionKey, encryptionSettings)) + .flatMap(encryptedIdPartitionKeyTuple -> + this.container.patchItem(encryptedIdPartitionKeyTuple.getT1(), encryptedIdPartitionKeyTuple.getT2(), encryptedCosmosPatchOperations, requestOptions, itemType).publishOn(encryptionScheduler). + flatMap(cosmosItemResponse -> setByteArrayContent((CosmosItemResponse) cosmosItemResponse, + this.encryptionProcessor.decrypt(cosmosItemResponseBuilderAccessor.getByteArrayContent((CosmosItemResponse) cosmosItemResponse))) + .map(bytes -> this.responseFactory.createItemResponse((CosmosItemResponse) cosmosItemResponse, + itemType))).onErrorResume(exception -> { if (!isRetry && exception instanceof CosmosException) { final CosmosException cosmosException = (CosmosException) exception; if (isIncorrectContainerRid(cosmosException)) { @@ -637,7 +742,7 @@ private Mono> patchItemInternalHelper(String itemId, } } return Mono.error(exception); - }); + }))); } /** @@ -720,10 +825,16 @@ private Mono> readItemHelper(String id, CosmosItemRequestOptions requestOptions, boolean isRetry) { this.setRequestHeaders(requestOptions); - Mono> responseMessageMono = this.container.readItem( - id, - partitionKey, - requestOptions, byte[].class); + Mono> responseMessageMono = this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptionSettings -> Mono.zip( + checkAndGetEncryptedId(id, encryptionSettings), + checkAndGetEncryptedPartitionKey(partitionKey, encryptionSettings)) + .flatMap(encryptedIdPartitionKeyTuple -> + this.container.readItem( + encryptedIdPartitionKeyTuple.getT1(), + encryptedIdPartitionKeyTuple.getT2(), + requestOptions, byte[].class))); return responseMessageMono.onErrorResume(exception -> { if (!isRetry && exception instanceof CosmosException) { final CosmosException cosmosException = (CosmosException) exception; @@ -738,15 +849,13 @@ private Mono> readItemHelper(String id, } private Mono> createItemHelper(byte[] streamPayload, - PartitionKey partitionKey, CosmosItemRequestOptions requestOptions, Class itemClass, boolean isRetry) { this.setRequestHeaders(requestOptions); return this.encryptionProcessor.encrypt(streamPayload) - .flatMap(encryptedPayload -> createItemHelper( + .flatMap(encryptedPayload -> this.container.createItem( encryptedPayload, - partitionKey, requestOptions) .publishOn(encryptionScheduler) .flatMap(cosmosItemResponse -> setByteArrayContent(cosmosItemResponse, @@ -758,7 +867,7 @@ private Mono> createItemHelper(byte[] streamPayload, if (isIncorrectContainerRid(cosmosException)) { this.encryptionProcessor.getIsEncryptionSettingsInitDone().set(false); return this.encryptionProcessor.initializeEncryptionSettingsAsync(true).then - (Mono.defer(() -> createItemHelper(streamPayload, partitionKey, requestOptions, + (Mono.defer(() -> createItemHelper(streamPayload, requestOptions, itemClass, true))); } } @@ -766,24 +875,53 @@ private Mono> createItemHelper(byte[] streamPayload, })); } - private Mono> createItemHelper(byte[] encryptedPayload, - PartitionKey partitionKey, - CosmosItemRequestOptions requestOptions) { - return partitionKey != null - ? this.container.createItem(encryptedPayload, partitionKey, requestOptions) - : this.container.createItem(encryptedPayload, requestOptions); + private Mono> createItemHelper(byte[] streamPayload, + PartitionKey partitionKey, + CosmosItemRequestOptions requestOptions, + Class itemClass, + boolean isRetry) { + this.setRequestHeaders(requestOptions); + AtomicReference encryptedPK = new AtomicReference<>(); + Mono encryptedPayloadMono = + this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptionSettings -> checkAndGetEncryptedPartitionKey(partitionKey, encryptionSettings)) + .flatMap(encryptedPartitionKey -> { + encryptedPK.set(encryptedPartitionKey); + return this.encryptionProcessor.encrypt(streamPayload); + }); + + return encryptedPayloadMono + .flatMap(encryptedPayload -> this.container.createItem( + encryptedPayload, + encryptedPK.get(), + requestOptions) + .publishOn(encryptionScheduler) + .flatMap(cosmosItemResponse -> setByteArrayContent(cosmosItemResponse, + this.encryptionProcessor.decrypt(cosmosItemResponseBuilderAccessor.getByteArrayContent(cosmosItemResponse))) + .map(bytes -> this.responseFactory.createItemResponse(cosmosItemResponse, + itemClass))).onErrorResume(exception -> { + if (!isRetry && exception instanceof CosmosException) { + final CosmosException cosmosException = (CosmosException) exception; + if (isIncorrectContainerRid(cosmosException)) { + this.encryptionProcessor.getIsEncryptionSettingsInitDone().set(false); + return this.encryptionProcessor.initializeEncryptionSettingsAsync(true).then + (Mono.defer(() -> createItemHelper(streamPayload, partitionKey, requestOptions, + itemClass, true))); + } + } + return Mono.error(exception); + })); } private Mono> upsertItemHelper(byte[] streamPayload, - PartitionKey partitionKey, CosmosItemRequestOptions requestOptions, Class itemClass, boolean isRetry) { this.setRequestHeaders(requestOptions); return this.encryptionProcessor.encrypt(streamPayload) - .flatMap(encryptedPayload -> upsertItemHelper( + .flatMap(encryptedPayload -> this.container.upsertItem( encryptedPayload, - partitionKey, requestOptions) .publishOn(encryptionScheduler) .flatMap(cosmosItemResponse -> setByteArrayContent(cosmosItemResponse, @@ -795,7 +933,7 @@ private Mono> upsertItemHelper(byte[] streamPayload, if (isIncorrectContainerRid(cosmosException)) { this.encryptionProcessor.getIsEncryptionSettingsInitDone().set(false); return this.encryptionProcessor.initializeEncryptionSettingsAsync(true).then - (Mono.defer(() -> upsertItemHelper(streamPayload, partitionKey, requestOptions, + (Mono.defer(() -> upsertItemHelper(streamPayload, requestOptions, itemClass, true))); } } @@ -803,12 +941,42 @@ private Mono> upsertItemHelper(byte[] streamPayload, })); } - private Mono> upsertItemHelper(byte[] encryptedPayload, + private Mono> upsertItemHelper(byte[] streamPayload, PartitionKey partitionKey, - CosmosItemRequestOptions requestOptions) { - return partitionKey != null - ? this.container.upsertItem(encryptedPayload, partitionKey, requestOptions) - : this.container.upsertItem(encryptedPayload, requestOptions); + CosmosItemRequestOptions requestOptions, + Class itemClass, + boolean isRetry) { + this.setRequestHeaders(requestOptions); + AtomicReference encryptedPK = new AtomicReference<>(); + Mono encryptedPayloadMono = this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptionSettings -> checkAndGetEncryptedPartitionKey(partitionKey, encryptionSettings)) + .flatMap(encryptedPartitionKey -> { + encryptedPK.set(encryptedPartitionKey); + return this.encryptionProcessor.encrypt(streamPayload); + }); + + return encryptedPayloadMono + .flatMap(encryptedPayload -> this.container.upsertItem( + encryptedPayload, + encryptedPK.get(), + requestOptions) + .publishOn(encryptionScheduler) + .flatMap(cosmosItemResponse -> setByteArrayContent(cosmosItemResponse, + this.encryptionProcessor.decrypt(cosmosItemResponseBuilderAccessor.getByteArrayContent(cosmosItemResponse))) + .map(bytes -> this.responseFactory.createItemResponse(cosmosItemResponse, itemClass))) + .onErrorResume(exception -> { + if (!isRetry && exception instanceof CosmosException) { + final CosmosException cosmosException = (CosmosException) exception; + if (isIncorrectContainerRid(cosmosException)) { + this.encryptionProcessor.getIsEncryptionSettingsInitDone().set(false); + return this.encryptionProcessor.initializeEncryptionSettingsAsync(true).then + (Mono.defer(() -> upsertItemHelper(streamPayload, partitionKey, requestOptions, + itemClass, true))); + } + } + return Mono.error(exception); + })); } private Mono> replaceItemHelper(byte[] streamPayload, @@ -818,11 +986,24 @@ private Mono> replaceItemHelper(byte[] streamPayload, Class itemClass, boolean isRetry) { this.setRequestHeaders(requestOptions); - return this.encryptionProcessor.encrypt(streamPayload) + AtomicReference encryptedPK = new AtomicReference<>(); + AtomicReference encryptedId = new AtomicReference<>(); + Mono encryptedPayloadMono = this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptionSettings -> Mono.zip( + checkAndGetEncryptedId(itemId, encryptionSettings), + checkAndGetEncryptedPartitionKey(partitionKey, encryptionSettings))) + .flatMap(encryptedIdPartitionKeyTuple -> { + encryptedId.set(encryptedIdPartitionKeyTuple.getT1()); + encryptedPK.set(encryptedIdPartitionKeyTuple.getT2()); + return this.encryptionProcessor.encrypt(streamPayload); + }); + + return encryptedPayloadMono .flatMap(encryptedPayload -> this.container.replaceItem( encryptedPayload, - itemId, - partitionKey, + encryptedId.get(), + encryptedPK.get(), requestOptions) .publishOn(encryptionScheduler) .flatMap(cosmosItemResponse -> setByteArrayContent(cosmosItemResponse, @@ -999,49 +1180,66 @@ public Mono executeCosmosBatch(CosmosBatch cosmosBatch) { * transactional batch succeeded. */ public Mono executeCosmosBatch(CosmosBatch cosmosBatch, CosmosBatchRequestOptions requestOptions) { - if (requestOptions == null) { - requestOptions = new CosmosBatchRequestOptions(); - } + final CosmosBatchRequestOptions cosmosBatchRequestOptions = Optional.ofNullable(requestOptions) + .orElse(new CosmosBatchRequestOptions()); List>> monoList = new ArrayList<>(); for (ItemBatchOperation itemBatchOperation : cosmosBatchAccessor.getOperationsInternal(cosmosBatch)) { Mono> itemBatchOperationMono = null; if (itemBatchOperation.getItem() != null) { - ObjectNode objectNode = - EncryptionUtils.getSimpleObjectMapper().valueToTree(itemBatchOperation.getItem()); - itemBatchOperationMono = - encryptionProcessor.encryptObjectNode(objectNode).map(encryptedItem -> { - return new ItemBatchOperation<>( + itemBatchOperationMono = this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptionSettings -> { + try { + Field id = itemBatchOperation.getItem().getClass().getDeclaredField(Constants.PROPERTY_NAME_ID); + id.setAccessible(true); + return Mono.zip( + checkAndGetEncryptedId((String) id.get(itemBatchOperation.getItem()), encryptionSettings), + checkAndGetEncryptedPartitionKey(itemBatchOperation.getPartitionKeyValue(), encryptionSettings)); + } catch (IllegalAccessException | NoSuchFieldException e) { + return Mono.error(e); + } + }) + .flatMap(encryptedIdPartitionKeyTuple -> { + ObjectNode objectNode = + EncryptionUtils.getSimpleObjectMapper().valueToTree(itemBatchOperation.getItem()); + return encryptionProcessor.encryptObjectNode(objectNode).map(encryptedItem -> new ItemBatchOperation<>( itemBatchOperation.getOperationType(), - itemBatchOperation.getId(), - itemBatchOperation.getPartitionKeyValue(), + encryptedIdPartitionKeyTuple.getT1(), + encryptedIdPartitionKeyTuple.getT2(), itemBatchOperation.getRequestOptions(), encryptedItem - ); + )); }); + } else { - itemBatchOperationMono = - Mono.just( + itemBatchOperationMono = this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptionSettings -> { + return Mono.zip( + checkAndGetEncryptedId(itemBatchOperation.getId(), encryptionSettings), + checkAndGetEncryptedPartitionKey(itemBatchOperation.getPartitionKeyValue(), encryptionSettings)); + }) + .flatMap(encryptedIdPartitionKeyTuple -> Mono.just( new ItemBatchOperation<>( itemBatchOperation.getOperationType(), - itemBatchOperation.getId(), - itemBatchOperation.getPartitionKeyValue(), + encryptedIdPartitionKeyTuple.getT1(), + encryptedIdPartitionKeyTuple.getT2(), itemBatchOperation.getRequestOptions(), null - ) - ); + ))); } monoList.add(itemBatchOperationMono); + } Mono>> encryptedOperationListMono = Flux.mergeSequential(monoList).collectList(); - CosmosBatchRequestOptions finalRequestOptions = requestOptions; CosmosBatch encryptedCosmosBatch = CosmosBatch.createCosmosBatch(cosmosBatch.getPartitionKeyValue()); return encryptedOperationListMono.flatMap(itemBatchOperations -> { cosmosBatchAccessor.getOperationsInternal(encryptedCosmosBatch).addAll(itemBatchOperations); - return executeCosmosBatchHelper(encryptedCosmosBatch, finalRequestOptions, false); + return executeCosmosBatchHelper(encryptedCosmosBatch, cosmosBatchRequestOptions, false); }); } @@ -1140,37 +1338,53 @@ public Flux> executeBulkOperati public Flux> executeBulkOperations( Flux operations, CosmosBulkExecutionOptions bulkOptions) { - if (bulkOptions == null) { - bulkOptions = new CosmosBulkExecutionOptions(); - } + final CosmosBulkExecutionOptions cosmosBulkExecutionOptions = Optional.ofNullable(bulkOptions) + .orElse(new CosmosBulkExecutionOptions()); - final CosmosBulkExecutionOptions cosmosBulkExecutionOptions = bulkOptions; Flux operationFlux = operations.flatMap(cosmosItemOperation -> { - Mono cosmosItemOperationMono; + Mono cosmosItemOperationMono = null; if (cosmosItemOperation.getItem() != null) { - ObjectNode objectNode = - EncryptionUtils.getSimpleObjectMapper().valueToTree(cosmosItemOperation.getItem()); - assert cosmosItemOperation instanceof ItemBulkOperation; - cosmosItemOperationMono = - this.encryptionProcessor.encryptObjectNode(objectNode).map(encryptedItem -> new ItemBulkOperation<>( - cosmosItemOperation.getOperationType(), - cosmosItemOperation.getId(), - cosmosItemOperation.getPartitionKeyValue(), - ((ItemBulkOperation) cosmosItemOperation).getRequestOptions(), - encryptedItem, - cosmosItemOperation.getContext() - )); + cosmosItemOperationMono = this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap( encryptionSettings -> { + try { + Field id = cosmosItemOperation.getItem().getClass().getDeclaredField(Constants.PROPERTY_NAME_ID); + id.setAccessible(true); + return Mono.zip( + checkAndGetEncryptedId((String) id.get(cosmosItemOperation.getItem()), encryptionSettings), + checkAndGetEncryptedPartitionKey(cosmosItemOperation.getPartitionKeyValue(), encryptionSettings)); + } catch (IllegalAccessException | NoSuchFieldException e) { + return Mono.error(e); + } + }) + .flatMap(encryptedIdPartitionKeyTuple -> { + ObjectNode objectNode = + EncryptionUtils.getSimpleObjectMapper().valueToTree(cosmosItemOperation.getItem()); + assert cosmosItemOperation instanceof ItemBulkOperation; + return this.encryptionProcessor.encryptObjectNode(objectNode).map(encryptedItem -> new ItemBulkOperation<>( + cosmosItemOperation.getOperationType(), + encryptedIdPartitionKeyTuple.getT1(), + encryptedIdPartitionKeyTuple.getT2(), + ((ItemBulkOperation) cosmosItemOperation).getRequestOptions(), + encryptedItem, + cosmosItemOperation.getContext() + )); + }); } else { - cosmosItemOperationMono = Mono.just( - new ItemBulkOperation<>( - cosmosItemOperation.getOperationType(), - cosmosItemOperation.getId(), - cosmosItemOperation.getPartitionKeyValue(), - ((ItemBulkOperation) cosmosItemOperation).getRequestOptions(), - null, - cosmosItemOperation.getContext() - ) - ); + cosmosItemOperationMono = this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap( encryptionSettings -> Mono.zip( + checkAndGetEncryptedId(cosmosItemOperation.getId() , encryptionSettings), + checkAndGetEncryptedPartitionKey(cosmosItemOperation.getPartitionKeyValue(), encryptionSettings))) + .flatMap(encryptedIdPartitionKeyTuple -> Mono.just( + new ItemBulkOperation<>( + cosmosItemOperation.getOperationType(), + encryptedIdPartitionKeyTuple.getT1(), + encryptedIdPartitionKeyTuple.getT2(), + ((ItemBulkOperation) cosmosItemOperation).getRequestOptions(), + null, + cosmosItemOperation.getContext() + ))); } return cosmosItemOperationMono; }); diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/Constants.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/Constants.java index c06ae1b71f2ca..888752d3959f1 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/Constants.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/Constants.java @@ -15,4 +15,6 @@ public class Constants { public static final String ALLOW_CACHED_READS_HEADER = "x-ms-cosmos-allow-cachedreads"; public static final String DATABASE_RID_HEADER = "x-ms-cosmos-database-rid"; + + public static final String PROPERTY_NAME_ID = "id"; } diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionProcessor.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionProcessor.java index 42032b0f2947f..76cc8094d5626 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionProcessor.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionProcessor.java @@ -22,8 +22,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BinaryNode; import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; @@ -41,6 +43,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -58,6 +61,7 @@ public class EncryptionProcessor { private ClientEncryptionPolicy clientEncryptionPolicy; private String containerRid; private String databaseRid; + private List partitionKeyPaths; private CosmosClientEncryptionKeyProperties cosmosClientEncryptionKeyProperties; private final EncryptionKeyStoreProviderImpl encryptionKeyStoreProviderImpl; private final static ImplementationBridgeHelpers.CosmosContainerPropertiesHelper.CosmosContainerPropertiesAccessor cosmosContainerPropertiesAccessor = ImplementationBridgeHelpers.CosmosContainerPropertiesHelper.getCosmosContainerPropertiesAccessor(); @@ -99,7 +103,15 @@ public Mono initializeEncryptionSettingsAsync(boolean isRetry) { { this.containerRid = cosmosContainerProperties.getResourceId(); this.databaseRid = cosmosContainerPropertiesAccessor.getSelfLink(cosmosContainerProperties).split("/")[1]; + + if (!cosmosContainerProperties.getPartitionKeyDefinition().getPaths().isEmpty()) { + this.partitionKeyPaths = cosmosContainerProperties.getPartitionKeyDefinition().getPaths(); + } else { + this.partitionKeyPaths = new ArrayList<>(); + } + this.encryptionSettings.setDatabaseRid(this.databaseRid); + this.encryptionSettings.setPartitionKeyPaths(partitionKeyPaths); if (cosmosContainerProperties.getClientEncryptionPolicy() == null) { this.isEncryptionSettingsInitDone.set(true); return Mono.empty(); @@ -206,7 +218,7 @@ public Mono initEncryptionSettingsIfNotInitializedAsync() { return Mono.empty(); } - ClientEncryptionPolicy getClientEncryptionPolicy() { + public ClientEncryptionPolicy getClientEncryptionPolicy() { return clientEncryptionPolicy; } @@ -438,16 +450,41 @@ public void encryptAndSerializeProperty(EncryptionSettings encryptionSettings, J } } + public String encryptAndSerializeValue(EncryptionSettings encryptionSettings, String propertyValue, String propertyName) throws MicrosoftDataEncryptionException { + JsonNode propertyValueHolder = toJsonNode(propertyValue.getBytes(StandardCharsets.US_ASCII), TypeMarker.STRING); + if (propertyName.equals(Constants.PROPERTY_NAME_ID)) { + return new String(encryptAndSerializeValue(encryptionSettings, null, propertyValueHolder, propertyName), StandardCharsets.UTF_8); + } else { + return BinaryNode.valueOf(encryptAndSerializeValue(encryptionSettings, null, propertyValueHolder, propertyName)).asText(); + } + } + public byte[] encryptAndSerializeValue(EncryptionSettings encryptionSettings, ObjectNode objectNode, JsonNode propertyValueHolder, String propertyName) throws MicrosoftDataEncryptionException { byte[] cipherText; byte[] cipherTextWithTypeMarker; + if (propertyName.equals(Constants.PROPERTY_NAME_ID)) { + if (propertyValueHolder.getNodeType() != JsonNodeType.STRING) { + throw new IllegalArgumentException("Unsupported argument type. The value to escape has to be string " + + "type. Please refer to https://aka.ms/CosmosClientEncryption for more details."); + } + } Pair typeMarkerPair = toByteArray(propertyValueHolder); cipherText = encryptionSettings.getAeadAes256CbcHmac256EncryptionAlgorithm().encrypt(typeMarkerPair.getRight()); cipherTextWithTypeMarker = new byte[cipherText.length + 1]; cipherTextWithTypeMarker[0] = (byte) typeMarkerPair.getLeft().getValue(); System.arraycopy(cipherText, 0, cipherTextWithTypeMarker, 1, cipherText.length); + + if (propertyName.equals(Constants.PROPERTY_NAME_ID)) { + // case: id does not support '/','\','?','#'. Convert Base64 string to Uri safe string + String base64UriSafeString = convertToBase64UriSafeString(cipherTextWithTypeMarker); + if (objectNode != null && !objectNode.isNull()) { + objectNode.put(propertyName, base64UriSafeString); + } + return base64UriSafeString.getBytes(StandardCharsets.UTF_8); + } + if (objectNode != null && !objectNode.isNull()) { objectNode.put(propertyName, cipherTextWithTypeMarker); } @@ -567,7 +604,14 @@ public JsonNode decryptAndSerializeValue(EncryptionSettings encryptionSettings, JsonNode propertyValueHolder, String propertyName) throws MicrosoftDataEncryptionException, IOException { byte[] cipherText; byte[] cipherTextWithTypeMarker; - cipherTextWithTypeMarker = propertyValueHolder.binaryValue(); + if (propertyName.equals(Constants.PROPERTY_NAME_ID)) { + if (propertyValueHolder.getNodeType() == JsonNodeType.NULL) { + return null; + } + cipherTextWithTypeMarker = convertFromBase64UriSafeString(propertyValueHolder.asText()); + } else { + cipherTextWithTypeMarker = propertyValueHolder.binaryValue(); + } cipherText = new byte[cipherTextWithTypeMarker.length - 1]; System.arraycopy(cipherTextWithTypeMarker, 1, cipherText, 0, cipherTextWithTypeMarker.length - 1); @@ -627,6 +671,17 @@ public static JsonNode toJsonNode(byte[] serializedBytes, TypeMarker typeMarker) throw BridgeInternal.createCosmosException(0, "Invalid or Unsupported Data Type Passed " + typeMarker); } + private String convertToBase64UriSafeString(byte[] bytesToProcess) { + // Base 64 Encoding with URL and Filename Safe Alphabet https://datatracker.ietf.org/doc/html/rfc4648#section-5 + // https://docs.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-item-limits, due to base64 conversion and encryption + // the permissible size of the property will further reduce. + return Base64.getUrlEncoder().encodeToString(bytesToProcess); + } + + private byte[] convertFromBase64UriSafeString(String base64UriSafeString) { + return Base64.getUrlDecoder().decode(base64UriSafeString); + } + public enum TypeMarker { NULL(1), // not used BOOLEAN(2), diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionSettings.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionSettings.java index 8c45d9be3e33e..e25c13be68e07 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionSettings.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionSettings.java @@ -25,6 +25,7 @@ import java.security.InvalidKeyException; import java.time.Duration; import java.time.Instant; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -39,6 +40,7 @@ public final class EncryptionSettings { private EncryptionType encryptionType; private String databaseRid; private CosmosClientEncryptionKeyProperties cosmosClientEncryptionKeyProperties; + private List partitionKeyPaths; private final static EncryptionImplementationBridgeHelpers.CosmosEncryptionAsyncClientHelper.CosmosEncryptionAsyncClientAccessor cosmosEncryptionAsyncClientAccessor = EncryptionImplementationBridgeHelpers.CosmosEncryptionAsyncClientHelper.getCosmosEncryptionAsyncClientAccessor(); @@ -226,6 +228,14 @@ public void setEncryptionSettingForProperty(String propertyName, EncryptionSetti this.encryptionSettingCacheByPropertyName.set(propertyName, cachedEncryptionSettings); } + public List getPartitionKeyPaths() { + return partitionKeyPaths; + } + + public void setPartitionKeyPaths(List partitionKeyPaths) { + this.partitionKeyPaths = partitionKeyPaths; + } + static EncryptionSettings create( EncryptionSettings settingsForKey, EncryptionType encryptionType) throws MicrosoftDataEncryptionException { diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/models/SqlQuerySpecWithEncryption.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/models/SqlQuerySpecWithEncryption.java index b9bfd921c66d4..938d749a18942 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/models/SqlQuerySpecWithEncryption.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/models/SqlQuerySpecWithEncryption.java @@ -4,6 +4,7 @@ package com.azure.cosmos.encryption.models; import com.azure.cosmos.encryption.CosmosEncryptionAsyncContainer; +import com.azure.cosmos.encryption.implementation.Constants; import com.azure.cosmos.encryption.implementation.EncryptionImplementationBridgeHelpers; import com.azure.cosmos.encryption.implementation.EncryptionProcessor; import com.azure.cosmos.encryption.implementation.EncryptionUtils; @@ -17,6 +18,8 @@ import com.fasterxml.jackson.databind.JsonNode; import reactor.core.publisher.Mono; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.HashMap; import java.util.List; @@ -56,11 +59,11 @@ Mono addEncryptionParameterAsync(String path, SqlParameter sqlParameter, List parameters = sqlQuerySpec.getParameters(); if (parameters != null) { return cosmosEncryptionAsyncContainerAccessor.getEncryptionProcessor(cosmosEncryptionAsyncContainer) - .initEncryptionSettingsIfNotInitializedAsync().then(Mono.defer(() -> { - + .initEncryptionSettingsIfNotInitializedAsync().then(Mono.defer(() -> { + String propertyName = path.substring(1); return cosmosEncryptionAsyncContainerAccessor.getEncryptionProcessor(cosmosEncryptionAsyncContainer) .getEncryptionSettings() - .getEncryptionSettingForPropertyAsync(sqlParameter.getName().substring(1), + .getEncryptionSettingForPropertyAsync(propertyName, cosmosEncryptionAsyncContainerAccessor.getEncryptionProcessor(cosmosEncryptionAsyncContainer)).flatMap(encryptionSettings -> { // encryptionSettings. if (encryptionSettings == null) { // property not encrypted. @@ -73,6 +76,12 @@ Mono addEncryptionParameterAsync(String path, SqlParameter sqlParameter, "query because of randomized encryption", path))); } try { + if (propertyName.equals(Constants.PROPERTY_NAME_ID)) { + if (sqlParameter.getValue(Object.class).getClass() != String.class) { + throw new IllegalArgumentException("Unsupported argument type. The value to escape has to be string " + + "type. Please refer to https://aka.ms/CosmosClientEncryption for more details."); + } + } byte[] valueByte = EncryptionUtils.serializeJsonToByteArray(EncryptionUtils.getSimpleObjectMapper(), sqlParameter.getValue(Object.class)); @@ -84,9 +93,20 @@ Mono addEncryptionParameterAsync(String path, SqlParameter sqlParameter, byte[] cipherTextWithTypeMarker = new byte[cipherText.length + 1]; cipherTextWithTypeMarker[0] = (byte) typeMarkerPair.getLeft().getValue(); System.arraycopy(cipherText, 0, cipherTextWithTypeMarker, 1, cipherText.length); - SqlParameter encryptedParameter = new SqlParameter(sqlParameter.getName(), - cipherTextWithTypeMarker); + + SqlParameter encryptedParameter; + if (propertyName.equals(Constants.PROPERTY_NAME_ID)) { + // case: id does not support '/','\','?','#'. Convert Base64 string to Uri safe string + String base64UriSafeString = convertToBase64UriSafeString(cipherTextWithTypeMarker); + encryptedParameter = new SqlParameter(sqlParameter.getName(), + base64UriSafeString.getBytes(StandardCharsets.UTF_8)); + + } else { + encryptedParameter = new SqlParameter(sqlParameter.getName(), + cipherTextWithTypeMarker); + } parameters.add(encryptedParameter); + } catch (MicrosoftDataEncryptionException ex) { return Mono.error(ex); } @@ -98,6 +118,13 @@ Mono addEncryptionParameterAsync(String path, SqlParameter sqlParameter, return Mono.empty(); } + private String convertToBase64UriSafeString(byte[] bytesToProcess) { + // Base 64 Encoding with URL and Filename Safe Alphabet https://datatracker.ietf.org/doc/html/rfc4648#section-5 + // https://docs.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-item-limits, due to base64 conversion and encryption + // the permissible size of the property will further reduce. + return Base64.getUrlEncoder().encodeToString(bytesToProcess); + } + HashMap getEncryptionParamMap() { return encryptionParamMap; } diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/CosmosEncryptionClientCachesTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/CosmosEncryptionClientCachesTest.java index efc84fa304dce..39048a0c9e0c8 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/CosmosEncryptionClientCachesTest.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/CosmosEncryptionClientCachesTest.java @@ -68,7 +68,7 @@ public void before_CosmosItemTest() { CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName(), metadata2).block(); //Create collection with clientEncryptionPolicy - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths()); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths(1), 1); CosmosContainerProperties containerProperties = new CosmosContainerProperties("TestCollForEncryptionCacheTest" , "/mypk"); containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/DotNetCompatibleTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/DotNetCompatibleTest.java index 58fe54893f845..dccd29bab41e1 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/DotNetCompatibleTest.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/DotNetCompatibleTest.java @@ -70,7 +70,7 @@ public void before_CosmosItemTest() throws IOException { cosmosEncryptionAsyncClient.getCosmosEncryptionAsyncDatabase(cosmosAsyncDatabase.getId()); ClientEncryptionPolicy clientEncryptionPolicy = - new ClientEncryptionPolicy(getPaths()); + new ClientEncryptionPolicy(getPaths(2), 2); String containerId = UUID.randomUUID().toString(); CosmosContainerProperties containerProperties = new CosmosContainerProperties(containerId, "/mypk"); containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); @@ -100,12 +100,13 @@ public void createItemEncrypt_readItemDecrypt() throws IOException { this.cosmosEncryptionAsyncContainer.getCosmosAsyncContainer().createItem(dotNetEncryptedPocoJsonNode, partitionKey, requestOptions).block(); + JsonNode dotNetPOCOJsonNode = MAPPER.readTree(new File("src/test/resources/dotnetEncryption/POCO.json")); + partitionKey = new PartitionKey(dotNetPOCOJsonNode.get("mypk").asText()); //reading above saved .net encrypted json via java encryption library EncryptionPojo unencryptedPojo = - this.cosmosEncryptionAsyncContainer.readItem(dotNetEncryptedPocoJsonNode.get("id").asText(), partitionKey + this.cosmosEncryptionAsyncContainer.readItem(dotNetPOCOJsonNode.get("id").asText(), partitionKey , new CosmosItemRequestOptions(), EncryptionPojo.class).block().getItem(); - JsonNode dotNetPOCOJsonNode = MAPPER.readTree(new File("src/test/resources/dotnetEncryption/POCO.json")); EncryptionPojo unencryptedPoco = MAPPER.treeToValue(dotNetPOCOJsonNode, EncryptionPojo.class); //validating java decrypted pojo similar to original .net unencrypted poco diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiCrudTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiCrudTest.java index a5415f557db45..1fbda8eb17f0d 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiCrudTest.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiCrudTest.java @@ -7,8 +7,6 @@ import com.azure.cosmos.CosmosAsyncClient; import com.azure.cosmos.CosmosClientBuilder; import com.azure.cosmos.encryption.models.CosmosEncryptionAlgorithm; -import com.azure.cosmos.encryption.models.CosmosEncryptionType; -import com.azure.cosmos.models.ClientEncryptionIncludedPath; import com.azure.cosmos.models.ClientEncryptionPolicy; import com.azure.cosmos.models.CosmosBatch; import com.azure.cosmos.models.CosmosBatchItemRequestOptions; @@ -24,13 +22,9 @@ import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.CosmosQueryRequestOptions; import com.azure.cosmos.models.EncryptionKeyWrapMetadata; -import com.azure.cosmos.models.FeedResponse; import com.azure.cosmos.models.PartitionKey; -import com.azure.cosmos.models.SqlParameter; import com.azure.cosmos.models.SqlQuerySpec; import com.azure.cosmos.util.CosmosPagedFlux; -import com.azure.cosmos.encryption.implementation.ReflectionUtils; -import com.azure.cosmos.encryption.models.SqlQuerySpecWithEncryption; import io.netty.handler.codec.http.HttpResponseStatus; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -40,7 +34,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -76,8 +69,7 @@ public void before_CosmosItemTest() { cosmosEncryptionAsyncDatabase = getSharedEncryptionDatabase(cosmosEncryptionAsyncClient); cosmosEncryptionAsyncContainer = getSharedEncryptionContainer(cosmosEncryptionAsyncClient); - ClientEncryptionPolicy clientEncryptionWithPolicyFormatVersion2 = new ClientEncryptionPolicy(getPaths()); - ReflectionUtils.setPolicyFormatVersion(clientEncryptionWithPolicyFormatVersion2, 2); + ClientEncryptionPolicy clientEncryptionWithPolicyFormatVersion2 = new ClientEncryptionPolicy(getPaths(2), 2); String containerId = UUID.randomUUID().toString(); CosmosContainerProperties properties = new CosmosContainerProperties(containerId, "/mypk"); properties.setClientEncryptionPolicy(clientEncryptionWithPolicyFormatVersion2); @@ -147,195 +139,6 @@ public void upsertItem_readItem() { validateResponse(properties, readItem); } - @Test(groups = {"encryption"}, timeOut = TIMEOUT) - public void queryItems() { - EncryptionPojo properties = getItem(UUID.randomUUID().toString()); - CosmosItemResponse itemResponse = cosmosEncryptionAsyncContainer.createItem(properties, - new PartitionKey(properties.getMypk()), new CosmosItemRequestOptions()).block(); - assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); - EncryptionPojo responseItem = itemResponse.getItem(); - validateResponse(properties, responseItem); - - String query = String.format("SELECT * from c where c.id = '%s'", properties.getId()); - CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); - - SqlQuerySpec querySpec = new SqlQuerySpec(query); - CosmosPagedFlux feedResponseIterator = - cosmosEncryptionAsyncContainer.queryItems(querySpec, cosmosQueryRequestOptions, EncryptionPojo.class); - List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); - assertThat(feedResponse.size()).isGreaterThanOrEqualTo(1); - for (EncryptionPojo pojo : feedResponse) { - if (pojo.getId().equals(properties.getId())) { - validateResponse(pojo, responseItem); - } - } - } - - @Test(groups = {"encryption"}, timeOut = TIMEOUT) - public void queryItemsAggregate() { - long startTime = Instant.now().getEpochSecond(); - List actualIds = new ArrayList<>(); - EncryptionPojo properties = getItem(UUID.randomUUID().toString()); - cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), - new CosmosItemRequestOptions()).block(); - actualIds.add(properties.getId()); - properties = getItem(UUID.randomUUID().toString()); - cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), - new CosmosItemRequestOptions()).block(); - actualIds.add(properties.getId()); - properties = getItem(UUID.randomUUID().toString()); - cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), - new CosmosItemRequestOptions()).block(); - actualIds.add(properties.getId()); - - // MAX query - String query1 = String.format("Select value max(c._ts) from c"); - CosmosQueryRequestOptions cosmosQueryRequestOptions1 = new CosmosQueryRequestOptions(); - - SqlQuerySpec querySpec1 = new SqlQuerySpec(query1); - CosmosPagedFlux feedResponseIterator1 = - cosmosEncryptionAsyncContainer.queryItems(querySpec1, cosmosQueryRequestOptions1, Integer.class); - List feedResponse1 = feedResponseIterator1.byPage().blockFirst().getResults(); - int timeStamp = feedResponse1.get(0); - long endTime = Instant.now().getEpochSecond(); - - assertThat(timeStamp).isGreaterThanOrEqualTo((int)startTime); - assertThat(timeStamp).isLessThanOrEqualTo((int)endTime); - assertThat(feedResponse1.size()).isEqualTo(1); - - // COUNT query - String query2 = String.format("Select top 1 value count(c) from c order by c._ts"); - CosmosQueryRequestOptions cosmosQueryRequestOptions2 = new CosmosQueryRequestOptions(); - - SqlQuerySpec querySpec2 = new SqlQuerySpec(query2); - CosmosPagedFlux feedResponseIterator2 = - cosmosEncryptionAsyncContainer.queryItems(querySpec2, cosmosQueryRequestOptions2, Integer.class); - List feedResponse2 = feedResponseIterator2.byPage().blockFirst().getResults(); - assertThat(feedResponse2.size()).isEqualTo(1); - - // MAX query for String class type - String query3 = String.format("Select value max(c.sensitiveString) from c"); - CosmosQueryRequestOptions cosmosQueryRequestOptions3 = new CosmosQueryRequestOptions(); - - SqlQuerySpec querySpec3 = new SqlQuerySpec(query3); - CosmosPagedFlux feedResponseIterator3 = - cosmosEncryptionAsyncContainer.queryItems(querySpec3, cosmosQueryRequestOptions3, String.class); - List feedResponse3 = feedResponseIterator3.byPage().blockFirst().getResults(); - assertThat(feedResponse3.size()).isEqualTo(1); - } - - @Test(groups = {"encryption"}, timeOut = TIMEOUT) - public void queryItemsOnEncryptedProperties() { - EncryptionPojo properties = getItem(UUID.randomUUID().toString()); - CosmosItemResponse itemResponse = cosmosEncryptionAsyncContainer.createItem(properties, - new PartitionKey(properties.getMypk()), new CosmosItemRequestOptions()).block(); - assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); - EncryptionPojo responseItem = itemResponse.getItem(); - validateResponse(properties, responseItem); - - String query = String.format("SELECT * FROM c where c.sensitiveString = @sensitiveString and c.nonSensitive =" + - " " + - "@nonSensitive and c.sensitiveLong = @sensitiveLong"); - SqlQuerySpec querySpec = new SqlQuerySpec(query); - SqlParameter parameter1 = new SqlParameter("@nonSensitive", properties.getNonSensitive()); - querySpec.getParameters().add(parameter1); - - SqlParameter parameter2 = new SqlParameter("@sensitiveString", properties.getSensitiveString()); - SqlParameter parameter3 = new SqlParameter("@sensitiveLong", properties.getSensitiveLong()); - SqlQuerySpecWithEncryption sqlQuerySpecWithEncryption = new SqlQuerySpecWithEncryption(querySpec); - sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveString", parameter2); - sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveLong", parameter3); - - CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); - CosmosPagedFlux feedResponseIterator = - cosmosEncryptionAsyncContainer.queryItemsOnEncryptedProperties(sqlQuerySpecWithEncryption, - cosmosQueryRequestOptions, EncryptionPojo.class); - List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); - assertThat(feedResponse.size()).isGreaterThanOrEqualTo(1); - for (EncryptionPojo pojo : feedResponse) { - if (pojo.getId().equals(properties.getId())) { - validateResponse(pojo, responseItem); - } - } - } - - @Test(groups = {"encryption"}, timeOut = TIMEOUT) - public void queryItemsOnRandomizedEncryption() { - EncryptionPojo properties = getItem(UUID.randomUUID().toString()); - CosmosItemResponse itemResponse = cosmosEncryptionAsyncContainer.createItem(properties, - new PartitionKey(properties.getMypk()), new CosmosItemRequestOptions()).block(); - assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); - EncryptionPojo responseItem = itemResponse.getItem(); - validateResponse(properties, responseItem); - - String query = String.format("SELECT * FROM c where c.sensitiveString = @sensitiveString and c.nonSensitive =" + - " " + - "@nonSensitive and c.sensitiveDouble = @sensitiveDouble"); - SqlQuerySpec querySpec = new SqlQuerySpec(query); - SqlParameter parameter1 = new SqlParameter("@nonSensitive", properties.getNonSensitive()); - querySpec.getParameters().add(parameter1); - - SqlParameter parameter2 = new SqlParameter("@sensitiveString", properties.getSensitiveString()); - SqlParameter parameter3 = new SqlParameter("@sensitiveDouble", properties.getSensitiveDouble()); - SqlQuerySpecWithEncryption sqlQuerySpecWithEncryption = new SqlQuerySpecWithEncryption(querySpec); - sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveString", parameter2); - sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveDouble", parameter3); - - CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); - CosmosPagedFlux feedResponseIterator = - cosmosEncryptionAsyncContainer.queryItemsOnEncryptedProperties(sqlQuerySpecWithEncryption, - cosmosQueryRequestOptions, EncryptionPojo.class); - try { - List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); - fail("Query on randomized parameter should fail"); - } catch (IllegalArgumentException ex) { - assertThat(ex.getMessage()).contains("Path /sensitiveDouble cannot be used in the " + - "query because of randomized encryption"); - } - } - - @Test(groups = {"encryption"}, timeOut = TIMEOUT) - public void queryItemsWithContinuationTokenAndPageSize() { - List actualIds = new ArrayList<>(); - EncryptionPojo properties = getItem(UUID.randomUUID().toString()); - cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), - new CosmosItemRequestOptions()).block(); - actualIds.add(properties.getId()); - properties = getItem(UUID.randomUUID().toString()); - cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), - new CosmosItemRequestOptions()).block(); - actualIds.add(properties.getId()); - properties = getItem(UUID.randomUUID().toString()); - cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), - new CosmosItemRequestOptions()).block(); - actualIds.add(properties.getId()); - - String query = String.format("SELECT * from c where c.id in ('%s', '%s', '%s')", actualIds.get(0), - actualIds.get(1), actualIds.get(2)); - CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); - String continuationToken = null; - int pageSize = 1; - - int initialDocumentCount = 3; - int finalDocumentCount = 0; - - CosmosPagedFlux feedResponseIterator = - cosmosEncryptionAsyncContainer.queryItems(query, cosmosQueryRequestOptions, EncryptionPojo.class); - - do { - Iterable> feedResponseIterable = - feedResponseIterator.byPage(continuationToken, 1).toIterable(); - for (FeedResponse fr : feedResponseIterable) { - int resultSize = fr.getResults().size(); - assertThat(resultSize).isEqualTo(pageSize); - finalDocumentCount += fr.getResults().size(); - continuationToken = fr.getContinuationToken(); - } - } while (continuationToken != null); - - assertThat(finalDocumentCount).isEqualTo(initialDocumentCount); - } - @Ignore("Ignoring it temporarily because server always returning policyFormatVersion 0") @Test(groups = {"encryption"}, timeOut = TIMEOUT) public void incompatiblePolicyFormatVersion() { @@ -352,214 +155,6 @@ public void incompatiblePolicyFormatVersion() { } } - @Test(groups = {"encryption"}, timeOut = TIMEOUT) - public void crudQueryStaleCache() { - String databaseId = UUID.randomUUID().toString(); - try { - createNewDatabaseWithClientEncryptionKey(databaseId); - CosmosAsyncClient asyncClient = getClientBuilder().buildAsyncClient(); - KeyEncryptionKeyResolver keyEncryptionKeyResolver = new TestKeyEncryptionKeyResolver(); - CosmosEncryptionAsyncClient cosmosEncryptionAsyncClient = new CosmosEncryptionClientBuilder().cosmosAsyncClient(asyncClient).keyEncryptionKeyResolver( - keyEncryptionKeyResolver).keyEncryptionKeyResolverName("TEST_KEY_RESOLVER").buildAsyncClient(); - CosmosEncryptionAsyncDatabase cosmosEncryptionAsyncDatabase = - cosmosEncryptionAsyncClient.getCosmosEncryptionAsyncDatabase(asyncClient.getDatabase(databaseId)); - - String containerId = UUID.randomUUID().toString(); - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths()); - createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); - CosmosEncryptionAsyncContainer encryptionAsyncContainerOriginal = - cosmosEncryptionAsyncDatabase.getCosmosEncryptionAsyncContainer(containerId); - - EncryptionPojo encryptionPojo = getItem(UUID.randomUUID().toString()); - CosmosItemResponse createResponse = encryptionAsyncContainerOriginal.createItem(encryptionPojo, - new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); - validateResponse(encryptionPojo, createResponse.getItem()); - - String query = String.format("SELECT * from c where c.id = '%s'", encryptionPojo.getId()); - SqlQuerySpec querySpec = new SqlQuerySpec(query); - CosmosPagedFlux feedResponseIterator = - encryptionAsyncContainerOriginal.queryItems(querySpec, null, EncryptionPojo.class); - List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); - - EncryptionPojo readItem = encryptionAsyncContainerOriginal.readItem(encryptionPojo.getId(), - new PartitionKey(encryptionPojo.getMypk()), - new CosmosItemRequestOptions(), EncryptionPojo.class).block().getItem(); - validateResponse(encryptionPojo, readItem); - - //Deleting database and creating database, container again - cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().delete().block(); - createNewDatabaseWithClientEncryptionKey(databaseId); - createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); - - //Validating create on original encryptionAsyncContainer - createResponse = encryptionAsyncContainerOriginal.createItem(encryptionPojo, - new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); - validateResponse(encryptionPojo, createResponse.getItem()); - - //Deleting and creating container - encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); - ClientEncryptionPolicy policyWithOneEncryptionPolicy = new ClientEncryptionPolicy(getPathWithOneEncryptionField()); - createEncryptionContainer(cosmosEncryptionAsyncDatabase, policyWithOneEncryptionPolicy, containerId); - CosmosEncryptionAsyncContainer encryptionAsyncContainerNew = getNewEncryptionContainerProxyObject(cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().getId(), containerId); - encryptionAsyncContainerNew.createItem(encryptionPojo, - new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); - EncryptionPojo pojoWithOneFieldEncrypted = encryptionAsyncContainerNew.getCosmosAsyncContainer().readItem(encryptionPojo.getId(), new PartitionKey(encryptionPojo.getMypk()), - new CosmosItemRequestOptions(), EncryptionPojo.class).block().getItem(); - validateResponseWithOneFieldEncryption(encryptionPojo, pojoWithOneFieldEncrypted); - - //Validating read on original encryptionAsyncContainer - readItem = encryptionAsyncContainerOriginal.readItem(encryptionPojo.getId(), new PartitionKey(encryptionPojo.getMypk()), - new CosmosItemRequestOptions(), EncryptionPojo.class).block().getItem(); - validateResponse(encryptionPojo, readItem); - - //Deleting and creating container - encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); - createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); - - CosmosItemResponse upsertResponse = encryptionAsyncContainerOriginal.upsertItem(encryptionPojo, - new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); - assertThat(upsertResponse.getRequestCharge()).isGreaterThan(0); - EncryptionPojo responseItem = upsertResponse.getItem(); - validateResponse(encryptionPojo, responseItem); - - //Deleting and creating container - encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); - createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); - encryptionAsyncContainerNew = getNewEncryptionContainerProxyObject(cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().getId(), containerId); - encryptionAsyncContainerNew.createItem(encryptionPojo, - new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); - - - CosmosItemResponse replaceResponse = - encryptionAsyncContainerOriginal.replaceItem(encryptionPojo, encryptionPojo.getId(), - new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); - assertThat(upsertResponse.getRequestCharge()).isGreaterThan(0); - responseItem = replaceResponse.getItem(); - validateResponse(encryptionPojo, responseItem); - - // First query fail on core sdk as there will be no pkrange cache, and collection cache have wrong information of collection rid, - // pkrange call will fail will null pointer, therefore querying before deleting the container making sure we have pkrange cache to begin with - encryptionAsyncContainerOriginal.queryItems(querySpec, null, EncryptionPojo.class).byPage().blockFirst().getResults(); - //Deleting and creating container - encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); - createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); - CosmosEncryptionAsyncContainer newEncryptionAsyncContainer = getNewEncryptionContainerProxyObject(cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().getId(), containerId); - - for (int i = 0; i < 10; i++) { - EncryptionPojo pojo = getItem(UUID.randomUUID().toString()); - newEncryptionAsyncContainer.createItem(pojo, - new PartitionKey(pojo.getMypk()), new CosmosItemRequestOptions()).block(); - } - - feedResponseIterator = - encryptionAsyncContainerOriginal.queryItems("Select * from C", null, EncryptionPojo.class); - String continuationToken = null; - int pageSize = 3; - int finalDocumentCount = 0; - do { - Iterable> feedResponseIterable = - feedResponseIterator.byPage(continuationToken, pageSize).toIterable(); - for (FeedResponse fr : feedResponseIterable) { - int resultSize = fr.getResults().size(); - assertThat(resultSize).isLessThanOrEqualTo(pageSize); - finalDocumentCount += fr.getResults().size(); - continuationToken = fr.getContinuationToken(); - } - } while (continuationToken != null); - - assertThat(finalDocumentCount).isEqualTo(10); - - - //Deleting and creating container - encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); - createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); - newEncryptionAsyncContainer = getNewEncryptionContainerProxyObject(cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().getId(), containerId); - - EncryptionPojo encryptionPojoForQueryItemsOnEncryptedProperties = getItem(UUID.randomUUID().toString()); - newEncryptionAsyncContainer.createItem(encryptionPojoForQueryItemsOnEncryptedProperties, - new PartitionKey(encryptionPojoForQueryItemsOnEncryptedProperties.getMypk()), new CosmosItemRequestOptions()).block(); - - query = String.format("SELECT * FROM c where c.sensitiveString = @sensitiveString and c.nonSensitive =" + - " " + - "@nonSensitive and c.sensitiveLong = @sensitiveLong"); - querySpec = new SqlQuerySpec(query); - SqlParameter parameter1 = new SqlParameter("@nonSensitive", encryptionPojoForQueryItemsOnEncryptedProperties.getNonSensitive()); - querySpec.getParameters().add(parameter1); - - SqlParameter parameter2 = new SqlParameter("@sensitiveString", encryptionPojoForQueryItemsOnEncryptedProperties.getSensitiveString()); - SqlParameter parameter3 = new SqlParameter("@sensitiveLong", encryptionPojoForQueryItemsOnEncryptedProperties.getSensitiveLong()); - SqlQuerySpecWithEncryption sqlQuerySpecWithEncryption = new SqlQuerySpecWithEncryption(querySpec); - sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveString", parameter2); - sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveLong", parameter3); - - feedResponseIterator = - encryptionAsyncContainerOriginal.queryItemsOnEncryptedProperties(sqlQuerySpecWithEncryption, - null, EncryptionPojo.class); - feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); - assertThat(feedResponse.size()).isGreaterThanOrEqualTo(1); - for (EncryptionPojo pojo : feedResponse) { - if (pojo.getId().equals(encryptionPojoForQueryItemsOnEncryptedProperties.getId())) { - validateResponse(encryptionPojoForQueryItemsOnEncryptedProperties, pojo); - } - } - - //Deleting and creating container - encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); - createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); - - String itemId= UUID.randomUUID().toString(); - EncryptionPojo createPojo = getItem(itemId); - CosmosBatch cosmosEncryptionBatch = CosmosBatch.createCosmosBatch(new PartitionKey(itemId)); - cosmosEncryptionBatch.createItemOperation(createPojo); - cosmosEncryptionBatch.readItemOperation(itemId); - - CosmosBatchResponse batchResponse = encryptionAsyncContainerOriginal.executeCosmosBatch(cosmosEncryptionBatch).block(); - assertThat(batchResponse.getResults().size()).isEqualTo(2); - validateResponse(createPojo, batchResponse.getResults().get(0).getItem(EncryptionPojo.class)); - validateResponse(createPojo, batchResponse.getResults().get(1).getItem(EncryptionPojo.class)); - - //Deleting and creating container - encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); - createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); - - itemId= UUID.randomUUID().toString(); - createPojo = getItem(itemId); - CosmosItemResponse itemResponse = encryptionAsyncContainerOriginal.createItem(createPojo, - new PartitionKey(createPojo.getMypk()), new CosmosItemRequestOptions()).block(); - - int originalSensitiveInt = createPojo.getSensitiveInt(); - int newSensitiveInt = originalSensitiveInt + 1; - - CosmosPatchOperations cosmosPatchOperations = CosmosPatchOperations.create(); - cosmosPatchOperations.add("/sensitiveString", "patched"); - cosmosPatchOperations.remove("/sensitiveDouble"); - cosmosPatchOperations.replace("/sensitiveInt", newSensitiveInt); - - CosmosItemResponse patchResponse = encryptionAsyncContainerOriginal.patchItem( - createPojo.getId(), - new PartitionKey(createPojo.getMypk()), - cosmosPatchOperations, - new CosmosPatchItemRequestOptions(), - EncryptionPojo.class).block(); - - CosmosItemResponse readResponse = encryptionAsyncContainerOriginal.readItem( - createPojo.getId(), - new PartitionKey(createPojo.getMypk()), - new CosmosPatchItemRequestOptions(), - EncryptionPojo.class).block(); - - validateResponse(patchResponse.getItem(), readResponse.getItem()); - - } finally { - try { - //deleting the database created for this test - this.client.getDatabase(databaseId).delete().block(); - } catch(Exception ex) { - // do nothing as we are clearing database created for this test - } - } - } - @Test(groups = {"encryption"}, timeOut = TIMEOUT) public void invalidDataEncryptionKeyAlgorithm() { try { @@ -911,132 +506,130 @@ private void createItemsAndVerify(List cosmosItemOperations @Test(groups = {"encryption"}, timeOut = TIMEOUT) public void crudOnDifferentOverload() { - List actualProperties = new ArrayList<>(); - // Read item - EncryptionPojo properties = getItem(UUID.randomUUID().toString()); - CosmosItemResponse itemResponse = cosmosEncryptionAsyncContainer.createItem(properties).block(); - assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); - EncryptionPojo responseItem = itemResponse.getItem(); - validateResponse(properties, responseItem); - actualProperties.add(properties); + String databaseId = UUID.randomUUID().toString(); + try { + createNewDatabaseWithClientEncryptionKey(databaseId); + CosmosAsyncClient asyncClient = getClientBuilder().buildAsyncClient(); + KeyEncryptionKeyResolver keyEncryptionKeyResolver = new TestKeyEncryptionKeyResolver(); + CosmosEncryptionAsyncClient cosmosEncryptionAsyncClient = new CosmosEncryptionClientBuilder().cosmosAsyncClient(asyncClient).keyEncryptionKeyResolver( + keyEncryptionKeyResolver).keyEncryptionKeyResolverName("TEST_KEY_RESOLVER").buildAsyncClient(); + CosmosEncryptionAsyncDatabase cosmosEncryptionAsyncDatabase = + cosmosEncryptionAsyncClient.getCosmosEncryptionAsyncDatabase(asyncClient.getDatabase(databaseId)); - properties = getItem(UUID.randomUUID().toString()); - CosmosItemResponse itemResponse1 = cosmosEncryptionAsyncContainer.createItem(properties, new CosmosItemRequestOptions()).block(); - assertThat(itemResponse1.getRequestCharge()).isGreaterThan(0); - EncryptionPojo responseItem1 = itemResponse1.getItem(); - validateResponse(properties, responseItem1); - actualProperties.add(properties); + String containerId = UUID.randomUUID().toString(); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths(1), 1); + createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); + CosmosEncryptionAsyncContainer encryptionAsyncContainerOriginal = + cosmosEncryptionAsyncDatabase.getCosmosEncryptionAsyncContainer(containerId); - //Upsert Item - properties = getItem(UUID.randomUUID().toString()); - CosmosItemResponse upsertResponse1 = cosmosEncryptionAsyncContainer.upsertItem(properties).block(); - assertThat(upsertResponse1.getRequestCharge()).isGreaterThan(0); - EncryptionPojo responseItem2 = upsertResponse1.getItem(); - validateResponse(properties, responseItem2); - actualProperties.add(properties); + List actualProperties = new ArrayList<>(); + // Read item + EncryptionPojo properties = getItem(UUID.randomUUID().toString()); + CosmosItemResponse itemResponse = encryptionAsyncContainerOriginal.createItem(properties).block(); + assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); + EncryptionPojo responseItem = itemResponse.getItem(); + validateResponse(properties, responseItem); + actualProperties.add(properties); + + properties = getItem(UUID.randomUUID().toString()); + CosmosItemResponse itemResponse1 = encryptionAsyncContainerOriginal.createItem(properties, new CosmosItemRequestOptions()).block(); + assertThat(itemResponse1.getRequestCharge()).isGreaterThan(0); + EncryptionPojo responseItem1 = itemResponse1.getItem(); + validateResponse(properties, responseItem1); + actualProperties.add(properties); + + //Upsert Item + properties = getItem(UUID.randomUUID().toString()); + CosmosItemResponse upsertResponse1 = encryptionAsyncContainerOriginal.upsertItem(properties).block(); + assertThat(upsertResponse1.getRequestCharge()).isGreaterThan(0); + EncryptionPojo responseItem2 = upsertResponse1.getItem(); + validateResponse(properties, responseItem2); + actualProperties.add(properties); + + properties = getItem(UUID.randomUUID().toString()); + CosmosItemResponse upsertResponse2 = encryptionAsyncContainerOriginal.upsertItem(properties, new CosmosItemRequestOptions()).block(); + assertThat(upsertResponse2.getRequestCharge()).isGreaterThan(0); + EncryptionPojo responseItem3 = upsertResponse2.getItem(); + validateResponse(properties, responseItem3); + actualProperties.add(properties); + + //Read Item + EncryptionPojo readItem = encryptionAsyncContainerOriginal.readItem(actualProperties.get(0).getId(), + new PartitionKey(actualProperties.get(0).getMypk()), EncryptionPojo.class).block().getItem(); + validateResponse(actualProperties.get(0), readItem); + + //Query Item + String query = String.format("SELECT * from c where c.id = '%s'", actualProperties.get(1).getId()); - properties = getItem(UUID.randomUUID().toString()); - CosmosItemResponse upsertResponse2 = cosmosEncryptionAsyncContainer.upsertItem(properties, new CosmosItemRequestOptions()).block(); - assertThat(upsertResponse2.getRequestCharge()).isGreaterThan(0); - EncryptionPojo responseItem3 = upsertResponse2.getItem(); - validateResponse(properties, responseItem3); - actualProperties.add(properties); - - //Read Item - EncryptionPojo readItem = cosmosEncryptionAsyncContainer.readItem(actualProperties.get(0).getId(), - new PartitionKey(actualProperties.get(0).getMypk()), EncryptionPojo.class).block().getItem(); - validateResponse(actualProperties.get(0), readItem); - - //Query Item - String query = String.format("SELECT * from c where c.id = '%s'", actualProperties.get(1).getId()); - - CosmosPagedFlux feedResponseIterator = - cosmosEncryptionAsyncContainer.queryItems(query, EncryptionPojo.class); - List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); - assertThat(feedResponse.size()).isGreaterThanOrEqualTo(1); - for (EncryptionPojo pojo : feedResponse) { - if (pojo.getId().equals(actualProperties.get(1).getId())) { - validateResponse(pojo, responseItem1); + CosmosPagedFlux feedResponseIterator = + encryptionAsyncContainerOriginal.queryItems(query, EncryptionPojo.class); + List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); + assertThat(feedResponse.size()).isGreaterThanOrEqualTo(1); + for (EncryptionPojo pojo : feedResponse) { + if (pojo.getId().equals(actualProperties.get(1).getId())) { + validateResponse(pojo, responseItem1); + } } - } - CosmosQueryRequestOptions cosmosQueryRequestOptions1 = new CosmosQueryRequestOptions(); + CosmosQueryRequestOptions cosmosQueryRequestOptions1 = new CosmosQueryRequestOptions(); - CosmosPagedFlux feedResponseIterator1 = - cosmosEncryptionAsyncContainer.queryItems(query, cosmosQueryRequestOptions1, EncryptionPojo.class); - List feedResponse1 = feedResponseIterator1.byPage().blockFirst().getResults(); - assertThat(feedResponse1.size()).isGreaterThanOrEqualTo(1); - for (EncryptionPojo pojo : feedResponse1) { - if (pojo.getId().equals(actualProperties.get(1).getId())) { - validateResponse(pojo, responseItem1); + CosmosPagedFlux feedResponseIterator1 = + encryptionAsyncContainerOriginal.queryItems(query, cosmosQueryRequestOptions1, EncryptionPojo.class); + List feedResponse1 = feedResponseIterator1.byPage().blockFirst().getResults(); + assertThat(feedResponse1.size()).isGreaterThanOrEqualTo(1); + for (EncryptionPojo pojo : feedResponse1) { + if (pojo.getId().equals(actualProperties.get(1).getId())) { + validateResponse(pojo, responseItem1); + } } - } - CosmosQueryRequestOptions cosmosQueryRequestOptions2 = new CosmosQueryRequestOptions(); - SqlQuerySpec querySpec = new SqlQuerySpec(query); + CosmosQueryRequestOptions cosmosQueryRequestOptions2 = new CosmosQueryRequestOptions(); + SqlQuerySpec querySpec = new SqlQuerySpec(query); - CosmosPagedFlux feedResponseIterator2 = - cosmosEncryptionAsyncContainer.queryItems(querySpec, cosmosQueryRequestOptions2, EncryptionPojo.class); - List feedResponse2 = feedResponseIterator2.byPage().blockFirst().getResults(); - assertThat(feedResponse2.size()).isGreaterThanOrEqualTo(1); - for (EncryptionPojo pojo : feedResponse2) { - if (pojo.getId().equals(actualProperties.get(1).getId())) { - validateResponse(pojo, responseItem1); + CosmosPagedFlux feedResponseIterator2 = + encryptionAsyncContainerOriginal.queryItems(querySpec, cosmosQueryRequestOptions2, EncryptionPojo.class); + List feedResponse2 = feedResponseIterator2.byPage().blockFirst().getResults(); + assertThat(feedResponse2.size()).isGreaterThanOrEqualTo(1); + for (EncryptionPojo pojo : feedResponse2) { + if (pojo.getId().equals(actualProperties.get(1).getId())) { + validateResponse(pojo, responseItem1); + } } - } - //Replace Item - CosmosItemResponse replaceResponse = - cosmosEncryptionAsyncContainer.replaceItem(actualProperties.get(2), actualProperties.get(2).getId(), - new PartitionKey(actualProperties.get(2).getMypk())).block(); - assertThat(upsertResponse1.getRequestCharge()).isGreaterThan(0); - responseItem = replaceResponse.getItem(); - validateResponse(actualProperties.get(2), responseItem); - - //Delete Item - CosmosItemResponse deleteResponse = cosmosEncryptionAsyncContainer.deleteItem(actualProperties.get(0).getId(), - new PartitionKey(actualProperties.get(0).getMypk())).block(); - assertThat(deleteResponse.getStatusCode()).isEqualTo(204); - - CosmosItemResponse deleteResponse1 = cosmosEncryptionAsyncContainer.deleteItem(actualProperties.get(1).getId(), - new PartitionKey(actualProperties.get(1).getMypk()), new CosmosItemRequestOptions()).block(); - assertThat(deleteResponse1.getStatusCode()).isEqualTo(204); - - CosmosItemResponse deleteResponse2 = cosmosEncryptionAsyncContainer.deleteItem(actualProperties.get(2), - new CosmosItemRequestOptions()).block(); - assertThat(deleteResponse2.getStatusCode()).isEqualTo(204); - - CosmosItemResponse deleteResponse3 = cosmosEncryptionAsyncContainer.deleteAllItemsByPartitionKey(new PartitionKey(actualProperties.get(3).getMypk()), - new CosmosItemRequestOptions()).block(); - assertThat(deleteResponse3.getStatusCode()).isEqualTo(200); - } + //Replace Item + CosmosItemResponse replaceResponse = + encryptionAsyncContainerOriginal.replaceItem(actualProperties.get(2), actualProperties.get(2).getId(), + new PartitionKey(actualProperties.get(2).getMypk())).block(); + assertThat(upsertResponse1.getRequestCharge()).isGreaterThan(0); + responseItem = replaceResponse.getItem(); + validateResponse(actualProperties.get(2), responseItem); - static void validateResponseWithOneFieldEncryption(EncryptionPojo originalItem, EncryptionPojo result) { - assertThat(result.getId()).isEqualTo(originalItem.getId()); - assertThat(result.getNonSensitive()).isEqualTo(originalItem.getNonSensitive()); - assertThat(result.getSensitiveString()).isNotEqualTo(originalItem.getSensitiveString()); - assertThat(result.getSensitiveInt()).isEqualTo(originalItem.getSensitiveInt()); - assertThat(result.getSensitiveFloat()).isEqualTo(originalItem.getSensitiveFloat()); - assertThat(result.getSensitiveLong()).isEqualTo(originalItem.getSensitiveLong()); - assertThat(result.getSensitiveDouble()).isEqualTo(originalItem.getSensitiveDouble()); - assertThat(result.isSensitiveBoolean()).isEqualTo(originalItem.isSensitiveBoolean()); - assertThat(result.getSensitiveIntArray()).isEqualTo(originalItem.getSensitiveIntArray()); - assertThat(result.getSensitiveStringArray()).isEqualTo(originalItem.getSensitiveStringArray()); - assertThat(result.getSensitiveString3DArray()).isEqualTo(originalItem.getSensitiveString3DArray()); - } - - public static List getPathWithOneEncryptionField() { - ClientEncryptionIncludedPath includedPath = new ClientEncryptionIncludedPath(); - includedPath.setClientEncryptionKeyId("key1"); - includedPath.setPath("/sensitiveString"); - includedPath.setEncryptionType(CosmosEncryptionType.DETERMINISTIC.toString()); - includedPath.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); - - List paths = new ArrayList<>(); - paths.add(includedPath); - return paths; - } + //Delete Item + CosmosItemResponse deleteResponse = encryptionAsyncContainerOriginal.deleteItem(actualProperties.get(0).getId(), + new PartitionKey(actualProperties.get(0).getMypk())).block(); + assertThat(deleteResponse.getStatusCode()).isEqualTo(204); + CosmosItemResponse deleteResponse1 = encryptionAsyncContainerOriginal.deleteItem(actualProperties.get(1).getId(), + new PartitionKey(actualProperties.get(1).getMypk()), new CosmosItemRequestOptions()).block(); + assertThat(deleteResponse1.getStatusCode()).isEqualTo(204); + + CosmosItemResponse deleteResponse2 = encryptionAsyncContainerOriginal.deleteItem(actualProperties.get(2), + new CosmosItemRequestOptions()).block(); + assertThat(deleteResponse2.getStatusCode()).isEqualTo(204); + + CosmosItemResponse deleteResponse3 = encryptionAsyncContainerOriginal.deleteAllItemsByPartitionKey(new PartitionKey(actualProperties.get(3).getMypk()), + new CosmosItemRequestOptions()).block(); + assertThat(deleteResponse3.getStatusCode()).isEqualTo(200); + } finally { + try { + //deleting the database created for this test + this.client.getDatabase(databaseId).delete().block(); + } catch (Exception e) { + // do nothing as we are clearing database created for this test + } + } + + } private void createEncryptionContainer(CosmosEncryptionAsyncDatabase cosmosEncryptionAsyncDatabase, ClientEncryptionPolicy clientEncryptionPolicy, String containerId) { @@ -1055,15 +648,4 @@ private void createNewDatabaseWithClientEncryptionKey(String databaseId){ encryptionAsyncDatabase.createClientEncryptionKey("key2", CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName(), metadata2).block(); } - - private CosmosEncryptionAsyncContainer getNewEncryptionContainerProxyObject(String databaseId, String containerId) { - CosmosAsyncClient client = getClientBuilder().buildAsyncClient(); - KeyEncryptionKeyResolver keyEncryptionKeyResolver = new TestKeyEncryptionKeyResolver(); - CosmosEncryptionAsyncClient cosmosEncryptionAsyncClient = new CosmosEncryptionClientBuilder().cosmosAsyncClient(client).keyEncryptionKeyResolver( - keyEncryptionKeyResolver).keyEncryptionKeyResolverName("TEST_KEY_RESOLVER").buildAsyncClient(); - CosmosEncryptionAsyncDatabase cosmosEncryptionAsyncDatabase = - cosmosEncryptionAsyncClient.getCosmosEncryptionAsyncDatabase(client.getDatabase(databaseId)); - CosmosEncryptionAsyncContainer cosmosEncryptionAsyncContainer = cosmosEncryptionAsyncDatabase.getCosmosEncryptionAsyncContainer(containerId); - return cosmosEncryptionAsyncContainer; - } } diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiHierarchicalPkTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiHierarchicalPkTest.java new file mode 100644 index 0000000000000..445812cfb0621 --- /dev/null +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiHierarchicalPkTest.java @@ -0,0 +1,87 @@ +package com.azure.cosmos.encryption; + +import com.azure.core.cryptography.KeyEncryptionKeyResolver; +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.models.ClientEncryptionPolicy; +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosItemRequestOptions; +import com.azure.cosmos.models.CosmosItemResponse; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.PartitionKeyDefinition; +import com.azure.cosmos.models.PartitionKeyDefinitionVersion; +import com.azure.cosmos.models.PartitionKind; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EncryptionAsyncApiHierarchicalPkTest extends TestSuiteBase { + private CosmosAsyncClient client; + private CosmosEncryptionAsyncClient cosmosEncryptionAsyncClient; + + CosmosEncryptionAsyncContainer cosmosEncryptionAsyncContainer; + CosmosEncryptionAsyncDatabase cosmosEncryptionAsyncDatabase; + + @Factory(dataProvider = "clientBuildersWithSessionConsistency") + public EncryptionAsyncApiHierarchicalPkTest(CosmosClientBuilder clientBuilder) { + super(clientBuilder); + } + + @BeforeClass(groups = {"encryption"}, timeOut = SETUP_TIMEOUT) + public void before_CosmosItemTest() { + assertThat(this.client).isNull(); + this.client = getClientBuilder().buildAsyncClient(); + KeyEncryptionKeyResolver keyEncryptionKeyResolver = new TestKeyEncryptionKeyResolver(); + cosmosEncryptionAsyncClient = new CosmosEncryptionClientBuilder().cosmosAsyncClient(this.client).keyEncryptionKeyResolver( + keyEncryptionKeyResolver).keyEncryptionKeyResolverName("TEST_KEY_RESOLVER").buildAsyncClient(); + cosmosEncryptionAsyncDatabase = getSharedEncryptionDatabase(cosmosEncryptionAsyncClient); + cosmosEncryptionAsyncContainer = getSharedEncryptionContainer(cosmosEncryptionAsyncClient); + + PartitionKeyDefinition partitionKeyDefinition = new PartitionKeyDefinition(); + partitionKeyDefinition.setKind(PartitionKind.MULTI_HASH); + partitionKeyDefinition.setVersion(PartitionKeyDefinitionVersion.V2); + List subParitionKeyPaths = new ArrayList<>(); + subParitionKeyPaths.add("/sensitiveNestedPojo"); + subParitionKeyPaths.add("/mypk"); + partitionKeyDefinition.setPaths(subParitionKeyPaths); + + ClientEncryptionPolicy clientEncryptionWithPolicyFormatVersion2 = new ClientEncryptionPolicy(getPaths(2), 2); + String containerId = UUID.randomUUID().toString(); + CosmosContainerProperties properties = getCollectionDefinition(containerId, partitionKeyDefinition); + properties.setClientEncryptionPolicy(clientEncryptionWithPolicyFormatVersion2); + cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().createContainer(properties).block(); + + } + + @AfterClass(groups = {"encryption"}, timeOut = SHUTDOWN_TIMEOUT, alwaysRun = true) + public void afterClass() { + assertThat(this.client).isNotNull(); + this.client.close(); + } + + @Test(groups = {"encryption"}, timeOut = TIMEOUT) + public void crudOnHierarchicalPk() { + EncryptionPojo properties = getItem(UUID.randomUUID().toString()); + CosmosItemResponse itemResponse = cosmosEncryptionAsyncContainer.createItem(properties, + new PartitionKey(properties.getMypk()), new CosmosItemRequestOptions()).block(); + assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); + EncryptionPojo responseItem = itemResponse.getItem(); + validateResponse(properties, responseItem); + + EncryptionPojo readItem = cosmosEncryptionAsyncContainer.readItem(properties.getId(), new PartitionKey(properties.getMypk()), + new CosmosItemRequestOptions(), EncryptionPojo.class).block().getItem(); + validateResponse(properties, readItem); + + // Deleting this item so query max of string in the query_aggregate test passes + cosmosEncryptionAsyncContainer.deleteItem(properties.getId(), new PartitionKey(properties.getMypk())).block(); + } + + +} diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiQueryTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiQueryTest.java new file mode 100644 index 0000000000000..2a431b3e20e07 --- /dev/null +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiQueryTest.java @@ -0,0 +1,530 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.cosmos.encryption; + +import com.azure.core.cryptography.KeyEncryptionKeyResolver; +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.encryption.models.CosmosEncryptionAlgorithm; +import com.azure.cosmos.encryption.models.CosmosEncryptionType; +import com.azure.cosmos.encryption.models.SqlQuerySpecWithEncryption; +import com.azure.cosmos.models.ClientEncryptionIncludedPath; +import com.azure.cosmos.models.ClientEncryptionPolicy; +import com.azure.cosmos.models.CosmosBatch; +import com.azure.cosmos.models.CosmosBatchResponse; +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosItemRequestOptions; +import com.azure.cosmos.models.CosmosItemResponse; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.CosmosQueryRequestOptions; +import com.azure.cosmos.models.EncryptionKeyWrapMetadata; +import com.azure.cosmos.models.FeedResponse; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.cosmos.util.CosmosPagedFlux; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +public class EncryptionAsyncApiQueryTest extends TestSuiteBase { + private CosmosAsyncClient client; + private CosmosEncryptionAsyncClient cosmosEncryptionAsyncClient; + + CosmosEncryptionAsyncContainer cosmosEncryptionAsyncContainer; + CosmosEncryptionAsyncDatabase cosmosEncryptionAsyncDatabase; + + @Factory(dataProvider = "clientBuildersWithSessionConsistency") + public EncryptionAsyncApiQueryTest(CosmosClientBuilder clientBuilder) { + super(clientBuilder); + } + + @BeforeClass(groups = {"encryption"}, timeOut = SETUP_TIMEOUT) + public void before_CosmosItemTest() { + assertThat(this.client).isNull(); + this.client = getClientBuilder().buildAsyncClient(); + KeyEncryptionKeyResolver keyEncryptionKeyResolver = new TestKeyEncryptionKeyResolver(); + cosmosEncryptionAsyncClient = new CosmosEncryptionClientBuilder().cosmosAsyncClient(this.client).keyEncryptionKeyResolver( + keyEncryptionKeyResolver).keyEncryptionKeyResolverName("TEST_KEY_RESOLVER").buildAsyncClient(); + cosmosEncryptionAsyncDatabase = getSharedEncryptionDatabase(cosmosEncryptionAsyncClient); + cosmosEncryptionAsyncContainer = getSharedEncryptionContainer(cosmosEncryptionAsyncClient); + + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths(1), 1); + String containerId = UUID.randomUUID().toString(); + CosmosContainerProperties properties = new CosmosContainerProperties(containerId, "/mypk"); + properties.setClientEncryptionPolicy(clientEncryptionPolicy); + cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().createContainer(properties).block(); + } + + @AfterClass(groups = {"encryption"}, timeOut = SHUTDOWN_TIMEOUT, alwaysRun = true) + public void afterClass() { + assertThat(this.client).isNotNull(); + this.client.close(); + } + + @Test(groups = {"encryption"}, timeOut = TIMEOUT) + public void queryItems() { + EncryptionPojo properties = getItem(UUID.randomUUID().toString()); + CosmosItemResponse itemResponse = cosmosEncryptionAsyncContainer.createItem(properties, + new PartitionKey(properties.getMypk()), new CosmosItemRequestOptions()).block(); + assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); + EncryptionPojo responseItem = itemResponse.getItem(); + validateResponse(properties, responseItem); + + String query = String.format("SELECT * from c where c.id = '%s'", properties.getId()); + CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + + SqlQuerySpec querySpec = new SqlQuerySpec(query); + CosmosPagedFlux feedResponseIterator = + cosmosEncryptionAsyncContainer.queryItems(querySpec, cosmosQueryRequestOptions, EncryptionPojo.class); + List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); + assertThat(feedResponse.size()).isGreaterThanOrEqualTo(1); + for (EncryptionPojo pojo : feedResponse) { + if (pojo.getId().equals(properties.getId())) { + validateResponse(pojo, responseItem); + } + } + } + + @Test(groups = {"encryption"}, timeOut = TIMEOUT) + public void queryItemsAggregate() { + long startTime = Instant.now().getEpochSecond(); + List actualIds = new ArrayList<>(); + EncryptionPojo properties = getItem(UUID.randomUUID().toString()); + cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), + new CosmosItemRequestOptions()).block(); + actualIds.add(properties.getId()); + properties = getItem(UUID.randomUUID().toString()); + cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), + new CosmosItemRequestOptions()).block(); + actualIds.add(properties.getId()); + properties = getItem(UUID.randomUUID().toString()); + cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), + new CosmosItemRequestOptions()).block(); + actualIds.add(properties.getId()); + + // MAX query + String query1 = String.format("Select value max(c._ts) from c"); + CosmosQueryRequestOptions cosmosQueryRequestOptions1 = new CosmosQueryRequestOptions(); + + SqlQuerySpec querySpec1 = new SqlQuerySpec(query1); + CosmosPagedFlux feedResponseIterator1 = + cosmosEncryptionAsyncContainer.queryItems(querySpec1, cosmosQueryRequestOptions1, Integer.class); + List feedResponse1 = feedResponseIterator1.byPage().blockFirst().getResults(); + int timeStamp = feedResponse1.get(0); + long endTime = Instant.now().getEpochSecond(); + + assertThat(timeStamp).isGreaterThanOrEqualTo((int)startTime); + assertThat(timeStamp).isLessThanOrEqualTo((int)endTime); + assertThat(feedResponse1.size()).isEqualTo(1); + + // COUNT query + String query2 = String.format("Select top 1 value count(c) from c order by c._ts"); + CosmosQueryRequestOptions cosmosQueryRequestOptions2 = new CosmosQueryRequestOptions(); + + SqlQuerySpec querySpec2 = new SqlQuerySpec(query2); + CosmosPagedFlux feedResponseIterator2 = + cosmosEncryptionAsyncContainer.queryItems(querySpec2, cosmosQueryRequestOptions2, Integer.class); + List feedResponse2 = feedResponseIterator2.byPage().blockFirst().getResults(); + assertThat(feedResponse2.size()).isEqualTo(1); + + // MAX query for String class type + String query3 = String.format("Select value max(c.sensitiveString) from c"); + CosmosQueryRequestOptions cosmosQueryRequestOptions3 = new CosmosQueryRequestOptions(); + + SqlQuerySpec querySpec3 = new SqlQuerySpec(query3); + CosmosPagedFlux feedResponseIterator3 = + cosmosEncryptionAsyncContainer.queryItems(querySpec3, cosmosQueryRequestOptions3, String.class); + List feedResponse3 = feedResponseIterator3.byPage().blockFirst().getResults(); + assertThat(feedResponse3.size()).isEqualTo(1); + } + + @Test(groups = {"encryption"}, timeOut = TIMEOUT) + public void queryItemsOnEncryptedProperties() { + EncryptionPojo properties = getItem(UUID.randomUUID().toString()); + CosmosItemResponse itemResponse = cosmosEncryptionAsyncContainer.createItem(properties, + new PartitionKey(properties.getMypk()), new CosmosItemRequestOptions()).block(); + assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); + EncryptionPojo responseItem = itemResponse.getItem(); + validateResponse(properties, responseItem); + + String query = String.format("SELECT * FROM c where c.sensitiveString = @sensitiveString and c.nonSensitive =" + + " " + + "@nonSensitive and c.sensitiveLong = @sensitiveLong"); + SqlQuerySpec querySpec = new SqlQuerySpec(query); + SqlParameter parameter1 = new SqlParameter("@nonSensitive", properties.getNonSensitive()); + querySpec.getParameters().add(parameter1); + + SqlParameter parameter2 = new SqlParameter("@sensitiveString", properties.getSensitiveString()); + SqlParameter parameter3 = new SqlParameter("@sensitiveLong", properties.getSensitiveLong()); + SqlQuerySpecWithEncryption sqlQuerySpecWithEncryption = new SqlQuerySpecWithEncryption(querySpec); + sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveString", parameter2); + sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveLong", parameter3); + + CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + CosmosPagedFlux feedResponseIterator = + cosmosEncryptionAsyncContainer.queryItemsOnEncryptedProperties(sqlQuerySpecWithEncryption, + cosmosQueryRequestOptions, EncryptionPojo.class); + List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); + assertThat(feedResponse.size()).isGreaterThanOrEqualTo(1); + for (EncryptionPojo pojo : feedResponse) { + if (pojo.getId().equals(properties.getId())) { + validateResponse(pojo, responseItem); + } + } + } + + @Test(groups = {"encryption"}, timeOut = TIMEOUT) + public void queryItemsOnRandomizedEncryption() { + EncryptionPojo properties = getItem(UUID.randomUUID().toString()); + CosmosItemResponse itemResponse = cosmosEncryptionAsyncContainer.createItem(properties, + new PartitionKey(properties.getMypk()), new CosmosItemRequestOptions()).block(); + assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); + EncryptionPojo responseItem = itemResponse.getItem(); + validateResponse(properties, responseItem); + + String query = String.format("SELECT * FROM c where c.sensitiveString = @sensitiveString and c.nonSensitive =" + + " " + + "@nonSensitive and c.sensitiveDouble = @sensitiveDouble"); + SqlQuerySpec querySpec = new SqlQuerySpec(query); + SqlParameter parameter1 = new SqlParameter("@nonSensitive", properties.getNonSensitive()); + querySpec.getParameters().add(parameter1); + + SqlParameter parameter2 = new SqlParameter("@sensitiveString", properties.getSensitiveString()); + SqlParameter parameter3 = new SqlParameter("@sensitiveDouble", properties.getSensitiveDouble()); + SqlQuerySpecWithEncryption sqlQuerySpecWithEncryption = new SqlQuerySpecWithEncryption(querySpec); + sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveString", parameter2); + sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveDouble", parameter3); + + CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + CosmosPagedFlux feedResponseIterator = + cosmosEncryptionAsyncContainer.queryItemsOnEncryptedProperties(sqlQuerySpecWithEncryption, + cosmosQueryRequestOptions, EncryptionPojo.class); + try { + List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); + fail("Query on randomized parameter should fail"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).contains("Path /sensitiveDouble cannot be used in the " + + "query because of randomized encryption"); + } + } + + @Test(groups = {"encryption"}, timeOut = TIMEOUT) + public void queryItemsWithContinuationTokenAndPageSize() { + List actualIds = new ArrayList<>(); + EncryptionPojo properties = getItem(UUID.randomUUID().toString()); + cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), + new CosmosItemRequestOptions()).block(); + actualIds.add(properties.getId()); + properties = getItem(UUID.randomUUID().toString()); + cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), + new CosmosItemRequestOptions()).block(); + actualIds.add(properties.getId()); + properties = getItem(UUID.randomUUID().toString()); + cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), + new CosmosItemRequestOptions()).block(); + actualIds.add(properties.getId()); + + String query = String.format("SELECT * from c where c.id in ('%s', '%s', '%s')", actualIds.get(0), + actualIds.get(1), actualIds.get(2)); + CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + String continuationToken = null; + int pageSize = 1; + + int initialDocumentCount = 3; + int finalDocumentCount = 0; + + CosmosPagedFlux feedResponseIterator = + cosmosEncryptionAsyncContainer.queryItems(query, cosmosQueryRequestOptions, EncryptionPojo.class); + + do { + Iterable> feedResponseIterable = + feedResponseIterator.byPage(continuationToken, 1).toIterable(); + for (FeedResponse fr : feedResponseIterable) { + int resultSize = fr.getResults().size(); + assertThat(resultSize).isEqualTo(pageSize); + finalDocumentCount += fr.getResults().size(); + continuationToken = fr.getContinuationToken(); + } + } while (continuationToken != null); + + assertThat(finalDocumentCount).isEqualTo(initialDocumentCount); + } + + @Test(groups = {"encryption"}, timeOut = TIMEOUT) + public void crudQueryStaleCache() { + String databaseId = UUID.randomUUID().toString(); + try { + createNewDatabaseWithClientEncryptionKey(databaseId); + CosmosAsyncClient asyncClient = getClientBuilder().buildAsyncClient(); + KeyEncryptionKeyResolver keyEncryptionKeyResolver = new TestKeyEncryptionKeyResolver(); + CosmosEncryptionAsyncClient cosmosEncryptionAsyncClient = new CosmosEncryptionClientBuilder().cosmosAsyncClient(asyncClient).keyEncryptionKeyResolver( + keyEncryptionKeyResolver).keyEncryptionKeyResolverName("TEST_KEY_RESOLVER").buildAsyncClient(); + CosmosEncryptionAsyncDatabase cosmosEncryptionAsyncDatabase = + cosmosEncryptionAsyncClient.getCosmosEncryptionAsyncDatabase(asyncClient.getDatabase(databaseId)); + + String containerId = UUID.randomUUID().toString(); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths(1), 1); + createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); + CosmosEncryptionAsyncContainer encryptionAsyncContainerOriginal = + cosmosEncryptionAsyncDatabase.getCosmosEncryptionAsyncContainer(containerId); + + EncryptionPojo encryptionPojo = getItem(UUID.randomUUID().toString()); + CosmosItemResponse createResponse = encryptionAsyncContainerOriginal.createItem(encryptionPojo, + new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); + validateResponse(encryptionPojo, createResponse.getItem()); + + String query = String.format("SELECT * from c where c.id = '%s'", encryptionPojo.getId()); + SqlQuerySpec querySpec = new SqlQuerySpec(query); + CosmosPagedFlux feedResponseIterator = + encryptionAsyncContainerOriginal.queryItems(querySpec, null, EncryptionPojo.class); + List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); + + EncryptionPojo readItem = encryptionAsyncContainerOriginal.readItem(encryptionPojo.getId(), + new PartitionKey(encryptionPojo.getMypk()), + new CosmosItemRequestOptions(), EncryptionPojo.class).block().getItem(); + validateResponse(encryptionPojo, readItem); + + //Deleting database and creating database, container again + cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().delete().block(); + createNewDatabaseWithClientEncryptionKey(databaseId); + createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); + + //Validating create on original encryptionAsyncContainer + createResponse = encryptionAsyncContainerOriginal.createItem(encryptionPojo, + new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); + validateResponse(encryptionPojo, createResponse.getItem()); + + //Deleting and creating container + encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); + ClientEncryptionPolicy policyWithOneEncryptionPolicy = new ClientEncryptionPolicy(getPathWithOneEncryptionField()); + createEncryptionContainer(cosmosEncryptionAsyncDatabase, policyWithOneEncryptionPolicy, containerId); + CosmosEncryptionAsyncContainer encryptionAsyncContainerNew = getNewEncryptionContainerProxyObject(cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().getId(), containerId); + encryptionAsyncContainerNew.createItem(encryptionPojo, + new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); + EncryptionPojo pojoWithOneFieldEncrypted = encryptionAsyncContainerNew.getCosmosAsyncContainer().readItem(encryptionPojo.getId(), new PartitionKey(encryptionPojo.getMypk()), + new CosmosItemRequestOptions(), EncryptionPojo.class).block().getItem(); + validateResponseWithOneFieldEncryption(encryptionPojo, pojoWithOneFieldEncrypted); + + //Validating read on original encryptionAsyncContainer + readItem = encryptionAsyncContainerOriginal.readItem(encryptionPojo.getId(), new PartitionKey(encryptionPojo.getMypk()), + new CosmosItemRequestOptions(), EncryptionPojo.class).block().getItem(); + validateResponse(encryptionPojo, readItem); + + //Deleting and creating container + encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); + createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); + + CosmosItemResponse upsertResponse = encryptionAsyncContainerOriginal.upsertItem(encryptionPojo, + new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); + assertThat(upsertResponse.getRequestCharge()).isGreaterThan(0); + EncryptionPojo responseItem = upsertResponse.getItem(); + validateResponse(encryptionPojo, responseItem); + + //Deleting and creating container + encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); + createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); + encryptionAsyncContainerNew = getNewEncryptionContainerProxyObject(cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().getId(), containerId); + encryptionAsyncContainerNew.createItem(encryptionPojo, + new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); + + + CosmosItemResponse replaceResponse = + encryptionAsyncContainerOriginal.replaceItem(encryptionPojo, encryptionPojo.getId(), + new PartitionKey(encryptionPojo.getMypk()), new CosmosItemRequestOptions()).block(); + assertThat(upsertResponse.getRequestCharge()).isGreaterThan(0); + responseItem = replaceResponse.getItem(); + validateResponse(encryptionPojo, responseItem); + + // First query fail on core sdk as there will be no pkrange cache, and collection cache have wrong information of collection rid, + // pkrange call will fail will null pointer, therefore querying before deleting the container making sure we have pkrange cache to begin with + encryptionAsyncContainerOriginal.queryItems(querySpec, null, EncryptionPojo.class).byPage().blockFirst().getResults(); + //Deleting and creating container + encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); + createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); + CosmosEncryptionAsyncContainer newEncryptionAsyncContainer = getNewEncryptionContainerProxyObject(cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().getId(), containerId); + + for (int i = 0; i < 10; i++) { + EncryptionPojo pojo = getItem(UUID.randomUUID().toString()); + newEncryptionAsyncContainer.createItem(pojo, + new PartitionKey(pojo.getMypk()), new CosmosItemRequestOptions()).block(); + } + + feedResponseIterator = + encryptionAsyncContainerOriginal.queryItems("Select * from C", null, EncryptionPojo.class); + String continuationToken = null; + int pageSize = 3; + int finalDocumentCount = 0; + do { + Iterable> feedResponseIterable = + feedResponseIterator.byPage(continuationToken, pageSize).toIterable(); + for (FeedResponse fr : feedResponseIterable) { + int resultSize = fr.getResults().size(); + assertThat(resultSize).isLessThanOrEqualTo(pageSize); + finalDocumentCount += fr.getResults().size(); + continuationToken = fr.getContinuationToken(); + } + } while (continuationToken != null); + + assertThat(finalDocumentCount).isEqualTo(10); + + + //Deleting and creating container + encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); + createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); + newEncryptionAsyncContainer = getNewEncryptionContainerProxyObject(cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().getId(), containerId); + + EncryptionPojo encryptionPojoForQueryItemsOnEncryptedProperties = getItem(UUID.randomUUID().toString()); + newEncryptionAsyncContainer.createItem(encryptionPojoForQueryItemsOnEncryptedProperties, + new PartitionKey(encryptionPojoForQueryItemsOnEncryptedProperties.getMypk()), new CosmosItemRequestOptions()).block(); + + query = String.format("SELECT * FROM c where c.sensitiveString = @sensitiveString and c.nonSensitive =" + + " " + + "@nonSensitive and c.sensitiveLong = @sensitiveLong"); + querySpec = new SqlQuerySpec(query); + SqlParameter parameter1 = new SqlParameter("@nonSensitive", encryptionPojoForQueryItemsOnEncryptedProperties.getNonSensitive()); + querySpec.getParameters().add(parameter1); + + SqlParameter parameter2 = new SqlParameter("@sensitiveString", encryptionPojoForQueryItemsOnEncryptedProperties.getSensitiveString()); + SqlParameter parameter3 = new SqlParameter("@sensitiveLong", encryptionPojoForQueryItemsOnEncryptedProperties.getSensitiveLong()); + SqlQuerySpecWithEncryption sqlQuerySpecWithEncryption = new SqlQuerySpecWithEncryption(querySpec); + sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveString", parameter2); + sqlQuerySpecWithEncryption.addEncryptionParameter("/sensitiveLong", parameter3); + + feedResponseIterator = + encryptionAsyncContainerOriginal.queryItemsOnEncryptedProperties(sqlQuerySpecWithEncryption, + null, EncryptionPojo.class); + feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); + assertThat(feedResponse.size()).isGreaterThanOrEqualTo(1); + for (EncryptionPojo pojo : feedResponse) { + if (pojo.getId().equals(encryptionPojoForQueryItemsOnEncryptedProperties.getId())) { + validateResponse(encryptionPojoForQueryItemsOnEncryptedProperties, pojo); + } + } + + //Deleting and creating container + encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); + createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); + + String itemId= UUID.randomUUID().toString(); + EncryptionPojo createPojo = getItem(itemId); + CosmosBatch cosmosEncryptionBatch = CosmosBatch.createCosmosBatch(new PartitionKey(itemId)); + cosmosEncryptionBatch.createItemOperation(createPojo); + cosmosEncryptionBatch.readItemOperation(itemId); + + CosmosBatchResponse batchResponse = encryptionAsyncContainerOriginal.executeCosmosBatch(cosmosEncryptionBatch).block(); + assertThat(batchResponse.getResults().size()).isEqualTo(2); + validateResponse(createPojo, batchResponse.getResults().get(0).getItem(EncryptionPojo.class)); + validateResponse(createPojo, batchResponse.getResults().get(1).getItem(EncryptionPojo.class)); + + //Deleting and creating container + encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); + createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); + + itemId= UUID.randomUUID().toString(); + createPojo = getItem(itemId); + CosmosItemResponse itemResponse = encryptionAsyncContainerOriginal.createItem(createPojo, + new PartitionKey(createPojo.getMypk()), new CosmosItemRequestOptions()).block(); + + int originalSensitiveInt = createPojo.getSensitiveInt(); + int newSensitiveInt = originalSensitiveInt + 1; + + CosmosPatchOperations cosmosPatchOperations = CosmosPatchOperations.create(); + cosmosPatchOperations.add("/sensitiveString", "patched"); + cosmosPatchOperations.remove("/sensitiveDouble"); + cosmosPatchOperations.replace("/sensitiveInt", newSensitiveInt); + + CosmosItemResponse patchResponse = encryptionAsyncContainerOriginal.patchItem( + createPojo.getId(), + new PartitionKey(createPojo.getMypk()), + cosmosPatchOperations, + new CosmosPatchItemRequestOptions(), + EncryptionPojo.class).block(); + + CosmosItemResponse readResponse = encryptionAsyncContainerOriginal.readItem( + createPojo.getId(), + new PartitionKey(createPojo.getMypk()), + new CosmosPatchItemRequestOptions(), + EncryptionPojo.class).block(); + + validateResponse(patchResponse.getItem(), readResponse.getItem()); + + } finally { + try { + //deleting the database created for this test + this.client.getDatabase(databaseId).delete().block(); + } catch(Exception ex) { + // do nothing as we are clearing database created for this test + } + } + } + + static void validateResponseWithOneFieldEncryption(EncryptionPojo originalItem, EncryptionPojo result) { + assertThat(result.getId()).isEqualTo(originalItem.getId()); + assertThat(result.getNonSensitive()).isEqualTo(originalItem.getNonSensitive()); + assertThat(result.getSensitiveString()).isNotEqualTo(originalItem.getSensitiveString()); + assertThat(result.getSensitiveInt()).isEqualTo(originalItem.getSensitiveInt()); + assertThat(result.getSensitiveFloat()).isEqualTo(originalItem.getSensitiveFloat()); + assertThat(result.getSensitiveLong()).isEqualTo(originalItem.getSensitiveLong()); + assertThat(result.getSensitiveDouble()).isEqualTo(originalItem.getSensitiveDouble()); + assertThat(result.isSensitiveBoolean()).isEqualTo(originalItem.isSensitiveBoolean()); + assertThat(result.getSensitiveIntArray()).isEqualTo(originalItem.getSensitiveIntArray()); + assertThat(result.getSensitiveStringArray()).isEqualTo(originalItem.getSensitiveStringArray()); + assertThat(result.getSensitiveString3DArray()).isEqualTo(originalItem.getSensitiveString3DArray()); + } + + public static List getPathWithOneEncryptionField() { + ClientEncryptionIncludedPath includedPath = new ClientEncryptionIncludedPath(); + includedPath.setClientEncryptionKeyId("key1"); + includedPath.setPath("/sensitiveString"); + includedPath.setEncryptionType(CosmosEncryptionType.DETERMINISTIC.toString()); + includedPath.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); + + List paths = new ArrayList<>(); + paths.add(includedPath); + return paths; + } + + private void createEncryptionContainer(CosmosEncryptionAsyncDatabase cosmosEncryptionAsyncDatabase, + ClientEncryptionPolicy clientEncryptionPolicy, + String containerId) { + CosmosContainerProperties properties = new CosmosContainerProperties(containerId, "/mypk"); + properties.setClientEncryptionPolicy(clientEncryptionPolicy); + cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().createContainer(properties).block(); + } + + private void createNewDatabaseWithClientEncryptionKey(String databaseId){ + EncryptionKeyWrapMetadata metadata1 = new EncryptionKeyWrapMetadata("TEST_KEY_RESOLVER", "key1", "tempmetadata1", "RSA-OAEP"); + EncryptionKeyWrapMetadata metadata2 = new EncryptionKeyWrapMetadata("TEST_KEY_RESOLVER", "key2", "tempmetadata2", "RSA-OAEP"); + cosmosEncryptionAsyncClient.getCosmosAsyncClient().createDatabase(databaseId).block(); + CosmosEncryptionAsyncDatabase encryptionAsyncDatabase = cosmosEncryptionAsyncClient.getCosmosEncryptionAsyncDatabase(databaseId); + encryptionAsyncDatabase.createClientEncryptionKey("key1", + CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName(), metadata1).block(); + encryptionAsyncDatabase.createClientEncryptionKey("key2", + CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName(), metadata2).block(); + } + + private CosmosEncryptionAsyncContainer getNewEncryptionContainerProxyObject(String databaseId, String containerId) { + CosmosAsyncClient client = getClientBuilder().buildAsyncClient(); + KeyEncryptionKeyResolver keyEncryptionKeyResolver = new TestKeyEncryptionKeyResolver(); + CosmosEncryptionAsyncClient cosmosEncryptionAsyncClient = new CosmosEncryptionClientBuilder().cosmosAsyncClient(client).keyEncryptionKeyResolver( + keyEncryptionKeyResolver).keyEncryptionKeyResolverName("TEST_KEY_RESOLVER").buildAsyncClient(); + CosmosEncryptionAsyncDatabase cosmosEncryptionAsyncDatabase = + cosmosEncryptionAsyncClient.getCosmosEncryptionAsyncDatabase(client.getDatabase(databaseId)); + CosmosEncryptionAsyncContainer cosmosEncryptionAsyncContainer = cosmosEncryptionAsyncDatabase.getCosmosEncryptionAsyncContainer(containerId); + return cosmosEncryptionAsyncContainer; + } + + +} diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionCosmosEncryptionChangeFeedTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionCosmosEncryptionChangeFeedTest.java index 497487b8deeb8..b0992c85f4d00 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionCosmosEncryptionChangeFeedTest.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionCosmosEncryptionChangeFeedTest.java @@ -264,7 +264,7 @@ private CosmosAsyncContainer createLeaseCollection(int provisionedThroughput) { } private CosmosEncryptionAsyncContainer createFeedCollection() { - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths()); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths(2), 2); String containerId = UUID.randomUUID().toString(); CosmosContainerProperties properties = new CosmosContainerProperties(containerId, "/mypk"); properties.setClientEncryptionPolicy(clientEncryptionPolicy); diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/TestSuiteBase.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/TestSuiteBase.java index c2db3b777d325..def361f00739b 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/TestSuiteBase.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/TestSuiteBase.java @@ -240,7 +240,7 @@ public static void beforeSuite() { SHARED_ENCRYPTION_DATABASE.createClientEncryptionKey("key2", CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName(), metadata2).block(); - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths()); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths(1), 1); String containerId = UUID.randomUUID().toString(); CosmosContainerProperties properties = new CosmosContainerProperties(containerId, "/mypk"); properties.setClientEncryptionPolicy(clientEncryptionPolicy); @@ -567,6 +567,10 @@ static protected CosmosContainerProperties getCollectionDefinition() { return collectionDefinition; } + static protected CosmosContainerProperties getCollectionDefinition(String collectionId, PartitionKeyDefinition partitionKeyDefinition) { + return new CosmosContainerProperties(collectionId, partitionKeyDefinition); + } + static protected CosmosContainerProperties getCollectionDefinitionWithRangeRangeIndexWithIdAsPartitionKey() { return getCollectionDefinitionWithRangeRangeIndex(Collections.singletonList("/id")); } @@ -1213,7 +1217,7 @@ public KeyEncryptionKey buildKeyEncryptionKey(String keyId) { } } - protected static List getPaths() { + protected static List getPaths(int policyFormatVersion) { ClientEncryptionIncludedPath includedPath1 = new ClientEncryptionIncludedPath(); includedPath1.setClientEncryptionKeyId("key1"); includedPath1.setPath("/sensitiveString"); @@ -1292,6 +1296,18 @@ protected static List getPaths() { includedPath13.setEncryptionType(CosmosEncryptionType.DETERMINISTIC.getName()); includedPath13.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); + ClientEncryptionIncludedPath includedPath14 = new ClientEncryptionIncludedPath(); + includedPath14.setClientEncryptionKeyId("key1"); + includedPath14.setPath("/id"); + includedPath14.setEncryptionType(CosmosEncryptionType.DETERMINISTIC.getName()); + includedPath14.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); + + ClientEncryptionIncludedPath includedPath15 = new ClientEncryptionIncludedPath(); + includedPath15.setClientEncryptionKeyId("key1"); + includedPath15.setPath("/pk"); + includedPath15.setEncryptionType(CosmosEncryptionType.DETERMINISTIC.getName()); + includedPath15.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); + List paths = new ArrayList<>(); paths.add(includedPath1); paths.add(includedPath2); @@ -1306,6 +1322,10 @@ protected static List getPaths() { paths.add(includedPath11); paths.add(includedPath12); paths.add(includedPath13); + if (policyFormatVersion == 2) { + paths.add(includedPath14); + paths.add(includedPath15); + } return paths; } diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/resources/dotnetEncryption/EncryptedPOCO.json b/sdk/cosmos/azure-cosmos-encryption/src/test/resources/dotnetEncryption/EncryptedPOCO.json index 448e61486361c..7f1e2684fbcdc 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/resources/dotnetEncryption/EncryptedPOCO.json +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/resources/dotnetEncryption/EncryptedPOCO.json @@ -1,20 +1,20 @@ { - "id": "7d925bf9-d583-4c6d-a49f-137bee7cff91", + "id": "BQF1OMHlXj-zDxKasrXQFwusq5lZf-AjwlS5Z4ZFdtljRO9ljKsPhPdZ4bGsXYbgtO4WrsHdqVy7r_EB101kO7xfjmGMvTOBiRt_M80AcNwiwJ1vFX9-uLudp3ttD-wKyZs=", "mypk": "7d925bf9-d583-4c6d-a49f-137bee7cff91", "nonSensitive": "cf012e6a-3dd1-4f1a-bd93-dc2b7fee5e97", "sensitiveString": "BQE8sgzM2aDfOlOiU9tYsgR5CghB/WZ/jsluf4HoYotfeUBqDBn3hVfx3A+o4m2mm5dEvnJyOR2Sl+NZezsYN71d", "sensitiveInt": "BAF+h8xlYiIqaH9TmN8/mgzX8ulodNhbYPASSPEGIVb+3qTR8J4qo/jZoz/qQdkxkEv8TV/aj0UdPEpGF4EBe+88", - "sensitiveFloat": "AwHBSYf0AqKfOX60Ppn/yj9ycfuFlOWEHGQ230BypHacyeBX/o3jRmlgnRNJuZpr1I9n0pNQl1yCfJkscMUVs8ao", + "sensitiveFloat": "BAFjPNt6v/gmO/v1uZCeTPgT6Tioja+rOzr1I8OK3hjC6tU6eZlXk0TUBVtu6ZkbIMo+cMAxH7IVC5diCnJfkjlS", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", - "sensitiveDouble": "AwHu2zdLESFrjQ7L9KbCenSeEXHOXKjV9d2UCfhNhR/ImYmODXymlKGxapHhzmUabdEp6hKP6ZNIjqJ5xvFD6EYs", + "sensitiveDouble": "AwHMrObWBev4hnUulDL5dvLxybF22O3VzY7//PXyano3Hrl8A967TAp3fdRATdaFHydATozMzgREtbhjJNk1WT1d", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", "sensitiveNestedPojo": { - "id": "BQERuT+l/kKc51OtABiPG3/lu8hpIJNGwUgJVzr71dKJk9wcuuzZbMqtsxj+Ue4B0oY0OYbKCuOYF3wLctg7P0cO", + "id": "BQERuT-l_kKc51OtABiPG3_lu8hpIJNGwUgJVzr71dKJk9wcuuzZbMqtsxj-Ue4B0oY0OYbKCuOYF3wLctg7P0cO", "mypk": "BQERuT+l/kKc51OtABiPG3/lu8hpIJNGwUgJVzr71dKJk9wcuuzZbMqtsxj+Ue4B0oY0OYbKCuOYF3wLctg7P0cO", "nonSensitive": null, "sensitiveString": "BQERuT+l/kKc51OtABiPG3/lu8hpIJNGwUgJVzr71dKJk9wcuuzZbMqtsxj+Ue4B0oY0OYbKCuOYF3wLctg7P0cO", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -82,12 +82,12 @@ "sensitiveChildPojo2DArray": [ [ { - "id": "BQHRkzWrw6w88Rak29a/mWMh57hvictUtZEK0YuYV+LNRuvCBPU74+btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", + "id": "BQHRkzWrw6w88Rak29a_mWMh57hvictUtZEK0YuYV-LNRuvCBPU74-btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", "mypk": null, "nonSensitive": null, "sensitiveString": "BQFfH6UGQgHuCZ1SKb8uDdEEdpjZ9+oFgKXDZGR5awXt5hY7B0D5i+6WQQNp2y2Vwsql32ZhCGEyg2vpew/heajPaIqKEPCCklz0WiVc/lrePg==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -123,12 +123,12 @@ "sensitiveChildPojoList": null }, { - "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C/wEauxKAYYswqUJ6DWd5eiIsFB/bjGViIq5A", + "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C_wEauxKAYYswqUJ6DWd5eiIsFB_bjGViIq5A", "mypk": null, "nonSensitive": null, "sensitiveString": "BQG3BuavLLquGcjrQw50+NLMm/86/+qVs+75b0jVTcfgjdkwXpTyFY7bRb/SSvxb7DYG7/za32PJ9Q5L4t5d3jvluLs2JWe6wb0fzjw5HSVuEA==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -142,12 +142,12 @@ ], [ { - "id": "BQHRkzWrw6w88Rak29a/mWMh57hvictUtZEK0YuYV+LNRuvCBPU74+btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", + "id": "BQHRkzWrw6w88Rak29a_mWMh57hvictUtZEK0YuYV-LNRuvCBPU74-btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", "mypk": null, "nonSensitive": null, "sensitiveString": "BQFfH6UGQgHuCZ1SKb8uDdEEdpjZ9+oFgKXDZGR5awXt5hY7B0D5i+6WQQNp2y2Vwsql32ZhCGEyg2vpew/heajPaIqKEPCCklz0WiVc/lrePg==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -183,12 +183,12 @@ "sensitiveChildPojoList": null }, { - "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C/wEauxKAYYswqUJ6DWd5eiIsFB/bjGViIq5A", + "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C_wEauxKAYYswqUJ6DWd5eiIsFB_bjGViIq5A", "mypk": null, "nonSensitive": null, "sensitiveString": "BQG3BuavLLquGcjrQw50+NLMm/86/+qVs+75b0jVTcfgjdkwXpTyFY7bRb/SSvxb7DYG7/za32PJ9Q5L4t5d3jvluLs2JWe6wb0fzjw5HSVuEA==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -203,12 +203,12 @@ ], "sensitiveChildPojoList": [ { - "id": "BQHRkzWrw6w88Rak29a/mWMh57hvictUtZEK0YuYV+LNRuvCBPU74+btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", + "id": "BQHRkzWrw6w88Rak29a_mWMh57hvictUtZEK0YuYV-LNRuvCBPU74-btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", "mypk": null, "nonSensitive": null, "sensitiveString": "BQFfH6UGQgHuCZ1SKb8uDdEEdpjZ9+oFgKXDZGR5awXt5hY7B0D5i+6WQQNp2y2Vwsql32ZhCGEyg2vpew/heajPaIqKEPCCklz0WiVc/lrePg==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -244,12 +244,12 @@ "sensitiveChildPojoList": null }, { - "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C/wEauxKAYYswqUJ6DWd5eiIsFB/bjGViIq5A", + "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C_wEauxKAYYswqUJ6DWd5eiIsFB_bjGViIq5A", "mypk": null, "nonSensitive": null, "sensitiveString": "BQG3BuavLLquGcjrQw50+NLMm/86/+qVs+75b0jVTcfgjdkwXpTyFY7bRb/SSvxb7DYG7/za32PJ9Q5L4t5d3jvluLs2JWe6wb0fzjw5HSVuEA==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -261,9 +261,9 @@ "sensitiveChildPojoList": null } ], - "_rid": "-dspAMVPSPMBAAAAAAAAAA==", - "_self": "dbs/-dspAA==/colls/-dspAMVPSPM=/docs/-dspAMVPSPMBAAAAAAAAAA==/", - "_etag": "00000000-0000-0000-984f-74dd1e6e01d7", + "_rid": "frBRAOuISWIBAAAAAAAAAA==", + "_self": "dbs/frBRAA==/colls/frBRAOuISWI=/docs/frBRAOuISWIBAAAAAAAAAA==/", + "_etag": "\"01094fbb-0000-0300-0000-63f528970000\"", "_attachments": "attachments/", - "_ts": 1629744506 + "_ts": 1677011095 } diff --git a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosContainerTest.java b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosContainerTest.java index 36ce99c42b75e..d2c6093c3d3ee 100644 --- a/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosContainerTest.java +++ b/sdk/cosmos/azure-cosmos-tests/src/test/java/com/azure/cosmos/CosmosContainerTest.java @@ -119,16 +119,23 @@ public void createContainer_withEncryption() { path1.setClientEncryptionKeyId("containerTestKey1"); ClientEncryptionIncludedPath path2 = new ClientEncryptionIncludedPath(); - path2.setPath("/path2"); + path2.setPath("/mypk"); path2.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); path2.setEncryptionType("Deterministic"); path2.setClientEncryptionKeyId("containerTestKey2"); + ClientEncryptionIncludedPath path3 = new ClientEncryptionIncludedPath(); + path3.setPath("/id"); + path3.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path3.setEncryptionType("Deterministic"); + path3.setClientEncryptionKeyId("containerTestKey2"); + List paths = new ArrayList<>(); paths.add(path1); paths.add(path2); + paths.add(path3); - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 2); containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); CosmosContainerResponse containerResponse = createdDatabase.createContainer(containerProperties); @@ -136,6 +143,110 @@ public void createContainer_withEncryption() { validateContainerResponseWithEncryption(containerProperties, containerResponse, clientEncryptionPolicy); } + @Test(groups = {"emulator"}, timeOut = TIMEOUT) + public void createContainer_withPartitionKeyWrongPolicyFormatVersion() { + String collectionName = UUID.randomUUID().toString(); + CosmosContainerProperties containerProperties = getCollectionDefinition(collectionName); + + ClientEncryptionIncludedPath path1 = new ClientEncryptionIncludedPath(); + path1.setPath("/mypk"); + path1.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path1.setEncryptionType("Deterministic"); + path1.setClientEncryptionKeyId("containerTestKey1"); + + ClientEncryptionIncludedPath path2 = new ClientEncryptionIncludedPath(); + path2.setPath("/path2"); + path2.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path2.setEncryptionType("Deterministic"); + path2.setClientEncryptionKeyId("containerTestKey2"); + + List paths = new ArrayList<>(); + paths.add(path1); + paths.add(path2); + + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 1); + CosmosContainerResponse containerResponse = null; + + //Verify partition key in CosmosContainerProperties constructor with encrypted field. + try { + containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); + containerResponse = createdDatabase.createContainer(containerProperties); + fail("createContainer should fail as mypk which is part of the partition key cannot be encrypted with " + + "PolicyFormatVersion 1."); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be encrypted with PolicyFormatVersion 1. Please use PolicyFormatVersion 2."); + } + + + //Verify for composite key + collectionName = UUID.randomUUID().toString(); + containerProperties = new CosmosContainerProperties(collectionName, "/mypk/mypk1"); + try { + containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); + containerResponse = createdDatabase.createContainer(containerProperties); + fail("createContainer should fail as mypk which is part of the partition key cannot be encrypted with " + + "PolicyFormatVersion 1."); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be encrypted with PolicyFormatVersion 1. Please use PolicyFormatVersion 2."); + } + + + //Verify setPartitionKeyDefinition with encrypted field. + collectionName = UUID.randomUUID().toString(); + containerProperties = new CosmosContainerProperties(collectionName, "/differentKey"); + try { + containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); + PartitionKeyDefinition partitionKeyDefinition = new PartitionKeyDefinition(); + List keyPaths = new ArrayList<>(); + keyPaths.add("/mypk"); + partitionKeyDefinition.setPaths(keyPaths); + containerProperties.setPartitionKeyDefinition(partitionKeyDefinition); + containerResponse = createdDatabase.createContainer(containerProperties); + fail("createContainer should fail as mypk which is part of the partition key cannot be encrypted with " + + "PolicyFormatVersion 1."); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be encrypted with PolicyFormatVersion 1. Please use PolicyFormatVersion 2."); + } + + //This should pass as we check only the first key of the composite key. + collectionName = UUID.randomUUID().toString(); + containerProperties = new CosmosContainerProperties(collectionName, "/mypk1/mypk"); + containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); + containerResponse = createdDatabase.createContainer(containerProperties); + assertThat(containerResponse.getRequestCharge()).isGreaterThan(0); + validateContainerResponseWithEncryption(containerProperties, containerResponse, clientEncryptionPolicy); + } + + @Test(groups = {"emulator"}, timeOut = TIMEOUT) + public void createEncryptionPolicy_withIdWrongPolicyFormatVersion() { + String collectionName = UUID.randomUUID().toString(); + CosmosContainerProperties containerProperties = getCollectionDefinition(collectionName); + + ClientEncryptionIncludedPath path1 = new ClientEncryptionIncludedPath(); + path1.setPath("/id"); + path1.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path1.setEncryptionType("Deterministic"); + path1.setClientEncryptionKeyId("containerTestKey1"); + + ClientEncryptionIncludedPath path2 = new ClientEncryptionIncludedPath(); + path2.setPath("/path2"); + path2.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path2.setEncryptionType("Deterministic"); + path2.setClientEncryptionKeyId("containerTestKey2"); + + List paths = new ArrayList<>(); + paths.add(path1); + paths.add(path2); + + try { + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 1); + fail("clientEncryptionPolicy should fail as id which is part of the partition key cannot be encrypted with " + + "PolicyFormatVersion 1."); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isEqualTo("Path /id cannot be encrypted with policyFormatVersion 1."); + } + } + @Test(groups = {"emulator"}, timeOut = TIMEOUT) public void createContainer_withPartitionKeyInEncryption() { String collectionName = UUID.randomUUID().toString(); @@ -157,18 +268,18 @@ public void createContainer_withPartitionKeyInEncryption() { paths.add(path1); paths.add(path2); - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 2); CosmosContainerResponse containerResponse = null; //Verify partition key in CosmosContainerProperties constructor with encrypted field. try { containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); containerResponse = createdDatabase.createContainer(containerProperties); - fail("createContainer should fail as mypk which is part of the partition key cannot be included in the " + - "ClientEncryptionPolicy."); + fail("createContainer should fail as mypk which is part of the partition key has to be encrypted with " + + "Deterministic type encryption."); } catch (IllegalArgumentException ex) { - assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be included in" + - " the ClientEncryptionPolicy."); + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key has to be encrypted with " + + "Deterministic type Encryption."); } @@ -178,11 +289,11 @@ public void createContainer_withPartitionKeyInEncryption() { try { containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); containerResponse = createdDatabase.createContainer(containerProperties); - fail("createContainer should fail as mypk which is part of the partition key cannot be included in the " + - "ClientEncryptionPolicy."); + fail("createContainer should fail as mypk which is part of the partition key has to be encrypted with " + + "Deterministic type encryption."); } catch (IllegalArgumentException ex) { - assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be included in" + - " the ClientEncryptionPolicy."); + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key has to be encrypted with " + + "Deterministic type Encryption."); } @@ -197,11 +308,11 @@ public void createContainer_withPartitionKeyInEncryption() { partitionKeyDefinition.setPaths(keyPaths); containerProperties.setPartitionKeyDefinition(partitionKeyDefinition); containerResponse = createdDatabase.createContainer(containerProperties); - fail("createContainer should fail as mypk which is part of the partition key cannot be included in the " + - "ClientEncryptionPolicy."); + fail("createContainer should fail as mypk which is part of the partition key has to be encrypted with " + + "Deterministic type encryption."); } catch (IllegalArgumentException ex) { - assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be included in" + - " the ClientEncryptionPolicy."); + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key has to be encrypted with " + + "Deterministic type Encryption."); } //This should pass as we check only the first key of the composite key. @@ -213,6 +324,36 @@ public void createContainer_withPartitionKeyInEncryption() { validateContainerResponseWithEncryption(containerProperties, containerResponse, clientEncryptionPolicy); } + @Test(groups = {"emulator"}, timeOut = TIMEOUT) + public void createEncryptionPolicy_withIdWrongEncryptionType() { + String collectionName = UUID.randomUUID().toString(); + CosmosContainerProperties containerProperties = getCollectionDefinition(collectionName); + + ClientEncryptionIncludedPath path1 = new ClientEncryptionIncludedPath(); + path1.setPath("/id"); + path1.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path1.setEncryptionType("Randomized"); + path1.setClientEncryptionKeyId("containerTestKey1"); + + ClientEncryptionIncludedPath path2 = new ClientEncryptionIncludedPath(); + path2.setPath("/path2"); + path2.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path2.setEncryptionType("Deterministic"); + path2.setClientEncryptionKeyId("containerTestKey2"); + + List paths = new ArrayList<>(); + paths.add(path1); + paths.add(path2); + + try { + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 2); + fail("clientEncryptionPolicy should fail as id which is part of the partition key has to be encrypted with " + + "Deterministic type Encryption."); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isEqualTo("Only deterministic encryption type is supported for path /id."); + } + } + @DataProvider public static Object[][] analyticalTTLProvider() { return new Object[][]{ @@ -594,7 +735,7 @@ public void getFeedRanges_withMultiplePartitions_HashV2() throws Exception { assertFeedRange( feedRanges.get(1), "{\"Range\":{\"min\":\"15555555555555555555555555555555\"," + - "\"max\":\"2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}}"); + "\"max\":\"2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"}}"); assertFeedRange( feedRanges.get(2), "{\"Range\":{\"min\":\"2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"max\":\"FF\"}}"); @@ -678,17 +819,17 @@ public void replace() throws Exception { assertThat(containerResponse.getProperties().getIndexingPolicy().getIndexingMode()).isEqualTo(IndexingMode.CONSISTENT); CosmosContainerResponse replaceResponse = createdDatabase.getContainer(containerProperties.getId()) - .replace(containerResponse.getProperties().setIndexingPolicy( - new IndexingPolicy().setAutomatic(false).setIndexingMode(IndexingMode.NONE))); + .replace(containerResponse.getProperties().setIndexingPolicy( + new IndexingPolicy().setAutomatic(false).setIndexingMode(IndexingMode.NONE))); assertThat(replaceResponse.getProperties().getIndexingPolicy().getIndexingMode()) .isEqualTo(IndexingMode.NONE); assertThat(replaceResponse.getProperties().getIndexingPolicy().isAutomatic()) .isEqualTo(false); replaceResponse = createdDatabase.getContainer(containerProperties.getId()) - .replace(containerResponse.getProperties().setIndexingPolicy( - new IndexingPolicy().setAutomatic(true).setIndexingMode(IndexingMode.CONSISTENT)), - options); + .replace(containerResponse.getProperties().setIndexingPolicy( + new IndexingPolicy().setAutomatic(true).setIndexingMode(IndexingMode.CONSISTENT)), + options); assertThat(replaceResponse.getProperties().getIndexingPolicy().getIndexingMode()) .isEqualTo(IndexingMode.CONSISTENT); assertThat(replaceResponse.getProperties().getIndexingPolicy().isAutomatic()) @@ -711,10 +852,10 @@ public void enableFullFidelityChangeFeedForExistingContainer() throws Exception CosmosContainerResponse replaceResponse = createdDatabase.getContainer(containerProperties.getId()) - .replace(containerResponse - .getProperties() - .setChangeFeedPolicy( - ChangeFeedPolicy.createAllVersionsAndDeletesPolicy(Duration.ofMinutes(4)))); + .replace(containerResponse + .getProperties() + .setChangeFeedPolicy( + ChangeFeedPolicy.createAllVersionsAndDeletesPolicy(Duration.ofMinutes(4)))); assertThat(containerResponse.getProperties()).isNotNull(); assertThat(containerResponse.getProperties().getChangeFeedPolicy()).isNotNull(); assertThat(containerResponse.getProperties().getChangeFeedPolicy().getRetentionDurationForAllVersionsAndDeletesPolicy()) @@ -738,10 +879,10 @@ public void changeFullFidelityChangeFeedRetentionDurationForExistingContainer() CosmosContainerResponse replaceResponse = createdDatabase.getContainer(containerProperties.getId()) - .replace(containerResponse - .getProperties() - .setChangeFeedPolicy( - ChangeFeedPolicy.createAllVersionsAndDeletesPolicy(Duration.ofMinutes(6)))); + .replace(containerResponse + .getProperties() + .setChangeFeedPolicy( + ChangeFeedPolicy.createAllVersionsAndDeletesPolicy(Duration.ofMinutes(6)))); assertThat(containerResponse.getProperties()).isNotNull(); assertThat(containerResponse.getProperties().getChangeFeedPolicy()).isNotNull(); assertThat(containerResponse.getProperties().getChangeFeedPolicy().getRetentionDurationForAllVersionsAndDeletesPolicy()) diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ClientEncryptionPolicy.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ClientEncryptionPolicy.java index 9e58f9da1b991..5ea5efe4ac4e0 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ClientEncryptionPolicy.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ClientEncryptionPolicy.java @@ -7,7 +7,6 @@ import com.azure.cosmos.implementation.JsonSerializable; import com.azure.cosmos.implementation.apachecommons.lang.StringUtils; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.ArrayList; import java.util.List; @@ -35,40 +34,40 @@ public final class ClientEncryptionPolicy { * Constructor. * * @param paths list of path of the item that need encryption along with path-specific settings. + * the PolicyFormatVersion will be set to 1 which is the default value. + * Note: If you need to include partition key or id field paths as part of the ClientEncryptionPolicy, please set PolicyFormatVersion to 2. */ public ClientEncryptionPolicy(List paths) { - this.validateIncludedPaths(paths); - this.includedPaths = paths; this.policyFormatVersion = 1; - } - - /** - * Constructor. - */ - public ClientEncryptionPolicy() { - this.jsonSerializable = new JsonSerializable(); + validateIncludedPaths(paths, policyFormatVersion); + this.includedPaths = paths; } /** * Constructor. * - * @param jsonString the json string that represents the client encryption policy. + * @param paths list of path of the item that need encryption along with path-specific settings. + * @param policyFormatVersion version of the client encryption policy definition. Current supported versions are 1 and 2. Default version is 1. + * Note: If you need to include partition key or id field paths as part of the ClientEncryptionPolicy, please set PolicyFormatVersion to 2. */ - ClientEncryptionPolicy(String jsonString) { - this.jsonSerializable = new JsonSerializable(jsonString); + public ClientEncryptionPolicy(List paths, int policyFormatVersion) { + if (policyFormatVersion > 2 || policyFormatVersion < 1) { + throw new IllegalArgumentException("Supported versions of client encryption policy are 1 and 2."); + } + this.policyFormatVersion = policyFormatVersion; + validateIncludedPaths(paths, policyFormatVersion); + this.includedPaths = paths; } /** * Constructor. - * - * @param objectNode the object node that represents the client encryption policy. */ - ClientEncryptionPolicy(ObjectNode objectNode) { - this.jsonSerializable = new JsonSerializable(objectNode); + public ClientEncryptionPolicy() { + this.jsonSerializable = new JsonSerializable(); } /** - * Gets the list of path of the item that need encryption along with path-specific settings. + * Gets the list of paths of the item that need encryption along with path-specific settings. * @return includedPaths */ public List getIncludedPaths() { @@ -83,28 +82,46 @@ public int getPolicyFormatVersion() { return policyFormatVersion; } - void validatePartitionKeyPathsAreNotEncrypted(List> partitionKeyPathTokens) { + /** + * Ensures that partition key paths specified in the client encryption policy for encryption are encrypted using Deterministic encryption algorithm. + * @param partitionKeyPathTokens Tokens corresponding to validated partition key. + */ + void validatePartitionKeyPathsIfEncrypted(List> partitionKeyPathTokens) { checkNotNull(partitionKeyPathTokens, "partitionKeyPathTokens cannot be null"); - List propertiesToEncrypt = - this.includedPaths.stream().map(clientEncryptionIncludedPath -> clientEncryptionIncludedPath.getPath().substring(1)).collect(Collectors.toList()); + for (List tokensInPath : partitionKeyPathTokens) { checkNotNull(tokensInPath); if (tokensInPath.size() > 0) { String topLevelToken = tokensInPath.get(0); - if (propertiesToEncrypt.contains(topLevelToken)) { - throw new IllegalArgumentException(String.format("Path %s which is part of the partition key " + - "cannot be included" + - " in the ClientEncryptionPolicy.", topLevelToken)); + + // paths in included paths start with "/". Get the ClientEncryptionIncludedPath and validate. + List encrypterPartitionKeyPath = + this.includedPaths.stream().filter(clientEncryptionIncludedPath -> clientEncryptionIncludedPath.getPath().substring(1).equals(topLevelToken)).collect(Collectors.toList()); + + if (encrypterPartitionKeyPath.size() >0) { + if (this.policyFormatVersion < 2) { + throw new IllegalArgumentException(String.format("Path %s which is part of the partition key " + + "cannot be encrypted" + + " with PolicyFormatVersion %s. Please use PolicyFormatVersion 2.", topLevelToken, policyFormatVersion)); + } + + // for the ClientEncryptionIncludedPath found check the encryption type. + if (!encrypterPartitionKeyPath.stream().map(encrypter -> encrypter.getEncryptionType()).findFirst().orElse(null).equals(Constants.Properties.DETERMINISTIC)) { + throw new IllegalArgumentException(String.format("Path %s which is part of the partition key " + + "has to be encrypted" + + " with Deterministic type Encryption.", topLevelToken)); + } + } } } } - private void validateIncludedPaths(List clientEncryptionIncludedPath) { + private static void validateIncludedPaths(List clientEncryptionIncludedPath, int policyFormatVersion) { List includedPathsList = new ArrayList<>(); for (ClientEncryptionIncludedPath path : clientEncryptionIncludedPath) { - this.validateClientEncryptionIncludedPath(path); + validateClientEncryptionIncludedPath(path, policyFormatVersion); if (includedPathsList.contains(path.getPath())) { throw new IllegalArgumentException("Duplicate Path found in clientEncryptionIncludedPath."); } @@ -113,7 +130,7 @@ private void validateIncludedPaths(List clientEncr } } - private void validateClientEncryptionIncludedPath(ClientEncryptionIncludedPath clientEncryptionIncludedPath) { + private static void validateClientEncryptionIncludedPath(ClientEncryptionIncludedPath clientEncryptionIncludedPath, int policyFormatVersion) { if (clientEncryptionIncludedPath == null) { throw new IllegalArgumentException("clientEncryptionIncludedPath is null"); } @@ -123,11 +140,20 @@ private void validateClientEncryptionIncludedPath(ClientEncryptionIncludedPath c } if (clientEncryptionIncludedPath.getPath().charAt(0) != '/' - || clientEncryptionIncludedPath.getPath().lastIndexOf('/') != 0 - || clientEncryptionIncludedPath.getPath().substring(1).equals("id")) { + || clientEncryptionIncludedPath.getPath().lastIndexOf('/') != 0) { throw new IllegalArgumentException("Invalid path " + clientEncryptionIncludedPath.getPath()); } + if (clientEncryptionIncludedPath.getPath().substring(1).equals(Constants.Properties.ID)) { + if (policyFormatVersion < 2) { + throw new IllegalArgumentException(String.format("Path %s cannot be encrypted with policyFormatVersion %s.", clientEncryptionIncludedPath.getPath(), policyFormatVersion)); + } + + if (!clientEncryptionIncludedPath.getEncryptionType().equals(Constants.Properties.DETERMINISTIC)) { + throw new IllegalArgumentException(String.format("Only deterministic encryption type is supported for path %s.", clientEncryptionIncludedPath.getPath())); + } + } + if (StringUtils.isEmpty(clientEncryptionIncludedPath.getClientEncryptionKeyId())) { throw new IllegalArgumentException("clientEncryptionKeyId in clientEncryptionIncludedPath is empty"); } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/CosmosContainerProperties.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/CosmosContainerProperties.java index 78675b6e29ffd..565eb6a367908 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/CosmosContainerProperties.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/CosmosContainerProperties.java @@ -126,7 +126,7 @@ public PartitionKeyDefinition getPartitionKeyDefinition() { public CosmosContainerProperties setPartitionKeyDefinition(PartitionKeyDefinition partitionKeyDefinition) { this.documentCollection.setPartitionKey(partitionKeyDefinition); if (this.getClientEncryptionPolicy() != null) { - this.getClientEncryptionPolicy().validatePartitionKeyPathsAreNotEncrypted(this.getPartitionKeyPathTokensList()); + this.getClientEncryptionPolicy().validatePartitionKeyPathsIfEncrypted(this.getPartitionKeyPathTokensList()); } return this; @@ -315,7 +315,7 @@ public ClientEncryptionPolicy getClientEncryptionPolicy() { */ public CosmosContainerProperties setClientEncryptionPolicy(ClientEncryptionPolicy value) { if (value != null) { - value.validatePartitionKeyPathsAreNotEncrypted(this.getPartitionKeyPathTokensList()); + value.validatePartitionKeyPathsIfEncrypted(this.getPartitionKeyPathTokensList()); } this.documentCollection.setClientEncryptionPolicy(value);