Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

package com.azure.cosmos;

import com.azure.cosmos.implementation.DefaultCosmosItemSerializer;
import com.azure.cosmos.implementation.ImplementationBridgeHelpers;
import com.azure.cosmos.implementation.TestConfigurations;
import com.azure.cosmos.models.CosmosBatch;
Expand All @@ -28,6 +29,7 @@
import com.azure.cosmos.models.FeedResponse;
import com.azure.cosmos.models.PartitionKey;
import com.azure.cosmos.rx.TestSuiteBase;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
Expand Down Expand Up @@ -272,6 +274,42 @@ public void pointOperationsAndQueryWithObjectNode(CosmosItemSerializer requestLe
ObjectNode.class);
}

@Test(groups = { "fast", "emulator" }, dataProvider = "testConfigs_requestLevelSerializer", timeOut = TIMEOUT * 1000000)
public void replaceItemRespectsCustomObjectMapper(CosmosItemSerializer ignored) {
ObjectMapper customMapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
CosmosItemSerializer customSerializer = new DefaultCosmosItemSerializer(customMapper);
CosmosItemRequestOptions options = new CosmosItemRequestOptions()
.setCustomItemSerializer(customSerializer);

String id = UUID.randomUUID().toString();
TestDocumentWithNullableField doc = new TestDocumentWithNullableField();
doc.id = id;
doc.mypk = id;
doc.someValue = "hello";
doc.nullableField = null;

container.createItem(doc, new PartitionKey(id), options);

ObjectNode rawAfterCreate = container
.readItem(id, new PartitionKey(id), options, ObjectNode.class)
.getItem();
assertThat(rawAfterCreate.has("nullableField"))
.as("createItem should respect NON_NULL and omit null fields")
.isFalse();

doc.someValue = "updated";
container.replaceItem(doc, id, new PartitionKey(id), options);

ObjectNode rawAfterReplace = container
.readItem(id, new PartitionKey(id), options, ObjectNode.class)
.getItem();
assertThat(rawAfterReplace.get("someValue").asText()).isEqualTo("updated");
assertThat(rawAfterReplace.has("nullableField"))
.as("replaceItem should respect NON_NULL and omit null fields")
.isFalse();
}

private <T> void runPointOperationAndQueryTestCase(
T doc,
String id,
Expand Down Expand Up @@ -878,6 +916,13 @@ private static void assertSameDocument(TestDocument doc, TestDocument deserializ
}
}

private static class TestDocumentWithNullableField {
public String id;
public String mypk;
public String someValue;
public String nullableField;
}

private static class TestDocumentWrappedInEnvelope {
public String id;

Expand Down
1 change: 1 addition & 0 deletions sdk/cosmos/azure-cosmos/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#### Bugs Fixed
* Fixed Remote Code Execution (RCE) vulnerability (CWE-502) by replacing Java deserialization with JSON-based serialization in `CosmosClientMetadataCachesSnapshot`, `AsyncCache`, and `DocumentCollection`. The metadata cache snapshot now uses Jackson for serialization/deserialization, eliminating the entire class of Java deserialization attacks. - [PR 47971](https://github.com/Azure/azure-sdk-for-java/pull/47971)
* Fixed availability strategy for Gateway V2 (thin client) by ensuring `RegionalRoutingContext` identity is based only on the immutable gateway endpoint. - See [PR 48432](https://github.com/Azure/azure-sdk-for-java/pull/48432)
* Fixed an issue where `replaceItem` bypassed the `customItemSerializer`, serialising POJOs with the SDK's internal `ObjectMapper` instead of the user-configured one. - See [PR 48529](https://github.com/Azure/azure-sdk-for-java/pull/48529)

#### Other Changes
* Added `appendUserAgentSuffix` method to `AsyncDocumentClient` to allow downstream libraries to append to the user agent after client construction. - See [PR 48505](https://github.com/Azure/azure-sdk-for-java/pull/48505)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,12 +386,12 @@ private static void mergeDiagnostics(

private <T> Mono<CosmosItemResponse<T>> replaceItemWithTrackingId(Class<T> itemType,
String itemId,
Document doc,
Object item,
RequestOptions requestOptions,
String trackingId) {

checkNotNull(trackingId, "Argument 'trackingId' must not be null.");
return replaceItemInternalCore(itemType, itemId, doc, requestOptions, trackingId)
return replaceItemInternalCore(itemType, itemId, item, requestOptions, trackingId)
.onErrorResume(throwable -> {
Throwable error = throwable instanceof CompletionException ? throwable.getCause() : throwable;

Expand Down Expand Up @@ -1737,15 +1737,14 @@ public <T> Mono<CosmosItemResponse<T>> replaceItem(T item, String itemId, Partit
public <T> Mono<CosmosItemResponse<T>> replaceItem(
T item, String itemId, PartitionKey partitionKey,
CosmosItemRequestOptions options) {
Document doc = InternalObjectNode.fromObject(item);
if (options == null) {
options = new CosmosItemRequestOptions();
}
ModelBridgeInternal.setPartitionKey(options, partitionKey);
@SuppressWarnings("unchecked")
Class<T> itemType = (Class<T>) item.getClass();
final CosmosItemRequestOptions requestOptions = options;
return withContext(context -> replaceItemInternal(itemType, itemId, doc, requestOptions, context));
return withContext(context -> replaceItemInternal(itemType, itemId, item, requestOptions, context));
}

/**
Expand Down Expand Up @@ -2228,15 +2227,15 @@ private Mono<CosmosItemResponse<Object>> deleteAllItemsByPartitionKeyInternal(
private <T> Mono<CosmosItemResponse<T>> replaceItemInternalCore(
Class<T> itemType,
String itemId,
Document doc,
Object item,
RequestOptions requestOptions,
String trackingId) {

requestOptions.setTrackingId(trackingId);

return this.getDatabase()
.getDocClientWrapper()
.replaceDocument(getItemLink(itemId), doc, requestOptions)
.replaceDocument(getItemLink(itemId), item, requestOptions)
.map(response -> itemResponseAccessor.createCosmosItemResponse(response, itemType, requestOptions.getEffectiveItemSerializer()))
.single();
}
Expand All @@ -2261,7 +2260,7 @@ private RequestOptions getEffectiveOptions(
private <T> Mono<CosmosItemResponse<T>> replaceItemInternal(
Class<T> itemType,
String itemId,
Document doc,
Object item,
CosmosItemRequestOptions options,
Context context) {

Expand All @@ -2280,9 +2279,9 @@ private <T> Mono<CosmosItemResponse<T>> replaceItemInternal(
String trackingId = null;
if (nonIdempotentWriteRetryPolicy.isEnabled() && nonIdempotentWriteRetryPolicy.useTrackingIdProperty()) {
trackingId = UUIDs.nonBlockingRandomUUID().toString();
responseMono = this.replaceItemWithTrackingId(itemType, itemId, doc, requestOptions, trackingId);
responseMono = this.replaceItemWithTrackingId(itemType, itemId, item, requestOptions, trackingId);
} else {
responseMono = this.replaceItemInternalCore(itemType, itemId, doc, requestOptions, null);
responseMono = this.replaceItemInternalCore(itemType, itemId, item, requestOptions, null);
}

CosmosAsyncClient client = database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3277,12 +3277,23 @@ private Mono<ResourceResponse<Document>> replaceDocumentInternal(
throw new IllegalArgumentException("document");
}

Document typedDocument = Document.fromObject(document, options.getEffectiveItemSerializer());
Document typedDocument;
boolean itemAlreadySerialized = false;

if (document instanceof Document) {
typedDocument = (Document) document;
} else if (document instanceof byte[]) {
typedDocument = new Document((byte[]) document);
} else {
typedDocument = Document.fromObject(document, options.getEffectiveItemSerializer());
itemAlreadySerialized = true;
}

return this.replaceDocumentInternal(
documentLink,
typedDocument,
options,
itemAlreadySerialized,
retryPolicyInstance,
clientContextOverride,
requestReference,
Expand Down Expand Up @@ -3358,6 +3369,7 @@ private Mono<ResourceResponse<Document>> replaceDocumentInternal(
document.getSelfLink(),
document,
options,
false,
retryPolicyInstance,
clientContextOverride,
requestReference,
Expand All @@ -3373,6 +3385,7 @@ private Mono<ResourceResponse<Document>> replaceDocumentInternal(
String documentLink,
Document document,
RequestOptions options,
boolean itemAlreadySerialized,
DocumentClientRetryPolicy retryPolicyInstance,
DiagnosticsClientContext clientContextOverride,
AtomicReference<RxDocumentServiceRequest> requestReference,
Expand All @@ -3396,7 +3409,13 @@ private Mono<ResourceResponse<Document>> replaceDocumentInternal(
}
}

ByteBuffer content = document.serializeJsonToByteBuffer(options.getEffectiveItemSerializer(), onAfterSerialization, false);
// When the item was already serialized via Document.fromObject with the effective
// serializer, use DEFAULT_SERIALIZER to convert the Document's propertyBag to bytes
// without re-applying the custom serializer (which would double-serialize).
CosmosItemSerializer serializerForContent = itemAlreadySerialized
? CosmosItemSerializer.DEFAULT_SERIALIZER
: options.getEffectiveItemSerializer();
ByteBuffer content = document.serializeJsonToByteBuffer(serializerForContent, onAfterSerialization, false);
Instant serializationEndTime = Instant.now();
SerializationDiagnosticsContext.SerializationDiagnostics serializationDiagnostics =
new SerializationDiagnosticsContext.SerializationDiagnostics(
Expand Down
Loading