Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Client Encryption: Removes retry behavior for point operations during client encryption policy change. #2648

Merged
merged 58 commits into from
Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
0f15769
Patch retry post policy refresh and refactoring.
kr-santosh Aug 3, 2021
fd69e86
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Aug 4, 2021
33ebde4
Update EncryptionContainer.cs
kr-santosh Aug 4, 2021
6fd6d1c
Update MdeEncryptionTests.cs
kr-santosh Aug 6, 2021
7607b97
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Aug 19, 2021
5196d60
Support policy refresh during delete operation.
kr-santosh Aug 19, 2021
a286881
Merge branch 'users/sakulk/bugfixes2' of https://github.com/Azure/azu…
kr-santosh Aug 19, 2021
d2af66f
Update EncryptionSettings.cs
kr-santosh Aug 24, 2021
92d09d0
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Aug 24, 2021
825c465
Update MdeEncryptionTests.cs
kr-santosh Aug 26, 2021
d25c3ac
Update EncryptionContainer.cs
kr-santosh Sep 3, 2021
afd75dd
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Sep 3, 2021
20c90f0
Update MdeEncryptionTests.cs
kr-santosh Sep 9, 2021
8415cd6
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Sep 9, 2021
0abdd93
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Sep 13, 2021
ce95fc6
Update MdeEncryptionTests.cs
kr-santosh Sep 15, 2021
c179d49
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Sep 22, 2021
b888aa1
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Sep 27, 2021
2f3f906
Update EncryptionContainer.cs
kr-santosh Sep 27, 2021
fcbe1f4
Update EncryptionFeedIterator.cs
kr-santosh Sep 27, 2021
172388b
Removed the retry logic, we not just update the settings and throw ex…
kr-santosh Sep 27, 2021
45193f8
Updated exception message.
kr-santosh Sep 27, 2021
409630e
Update EncryptionTransactionalBatch.cs
kr-santosh Sep 27, 2021
458b0f5
Update MdeEncryptionTests.cs
kr-santosh Sep 27, 2021
f8bc05f
Update MdeEncryptionTests.cs
kr-santosh Sep 27, 2021
2674ba2
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Sep 28, 2021
1bd0c80
Update EncryptionSettingForProperty.cs
kr-santosh Sep 28, 2021
63222a5
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Sep 29, 2021
a1cfd32
Update EncryptionSettingForProperty.cs
kr-santosh Sep 29, 2021
ab8fc10
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Sep 30, 2021
3968bef
Fixes as per review request.
kr-santosh Sep 30, 2021
44b4c3b
Update MdeEncryptionTests.cs
kr-santosh Sep 30, 2021
4cdefa0
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Oct 4, 2021
9d3a8e7
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Oct 18, 2021
260ece5
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Oct 25, 2021
e4ab2ac
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Oct 26, 2021
10417a6
Update MdeEncryptionTests.cs
kr-santosh Oct 26, 2021
89f947b
Update EncryptionContainer.cs
kr-santosh Oct 26, 2021
cf1a4e5
Merge branch 'master' into users/sakulk/bugfixes2
j82w Oct 27, 2021
7271635
Update EncryptionProcessor.cs
kr-santosh Oct 27, 2021
79361af
refactor, static creation of serializers..
kr-santosh Oct 27, 2021
e40d72a
Added change logs to custom pkg, bumped up package version.
kr-santosh Oct 28, 2021
5e73fe5
Update changelog.md
kr-santosh Oct 28, 2021
0e84171
Update changelog.md
kr-santosh Oct 28, 2021
41fc38c
updates.
kr-santosh Oct 28, 2021
6e3cca2
Revert "updates."
kr-santosh Oct 29, 2021
e9cd6db
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Oct 29, 2021
42678f5
Revert "Update changelog.md"
kr-santosh Oct 29, 2021
b4c4c93
Reverting changes w.r.t to custom package.
kr-santosh Oct 29, 2021
1323b6d
Reverting changes w.r.t custom package.
kr-santosh Oct 29, 2021
0397bd9
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Nov 9, 2021
8f01564
Pass diagnostics in exception.
kr-santosh Nov 10, 2021
1993aac
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Dec 7, 2021
5f12290
throw EncryptionCosmosException instead of InvalidOperationException …
kr-santosh Dec 7, 2021
69f6908
Update MdeEncryptionTests.cs
kr-santosh Dec 7, 2021
63f89e2
Merge branch 'master' into users/sakulk/bugfixes2
kr-santosh Dec 14, 2021
9f35ab9
Update EncryptionContainer.cs
kr-santosh Dec 14, 2021
405838b
Merge branch 'users/sakulk/bugfixes2' of https://github.com/Azure/azu…
kr-santosh Dec 14, 2021
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
183 changes: 152 additions & 31 deletions Microsoft.Azure.Cosmos.Encryption/src/EncryptionContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -644,28 +644,14 @@ public async override Task<ResponseMessage> PatchItemStreamAsync(
throw new ArgumentNullException(nameof(patchOperations));
}

EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(
obsoleteEncryptionSettings: null,
cancellationToken: cancellationToken);

CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(requestOptions);
using (diagnosticsContext.CreateScope("PatchItem"))
{
List<PatchOperation> encryptedPatchOperations = await this.EncryptPatchOperationsAsync(
patchOperations,
encryptionSettings,
cancellationToken);

ResponseMessage responseMessage = await this.container.PatchItemStreamAsync(
ResponseMessage responseMessage = await this.PatchItemHelperAsync(
id,
partitionKey,
encryptedPatchOperations,
patchOperations,
requestOptions,
cancellationToken);

responseMessage.Content = await EncryptionProcessor.DecryptAsync(
responseMessage.Content,
encryptionSettings,
diagnosticsContext,
cancellationToken);

Expand Down Expand Up @@ -985,9 +971,7 @@ private async Task<ResponseMessage> CreateItemHelperAsync(
// The idea is to have the container Rid cached and sent out as part of RequestOptions with Container Rid set in "x-ms-cosmos-intended-collection-rid" header.
// So when the container being referenced here gets recreated we would end up with a stale encryption settings and container Rid and this would result in BadRequest( and a substatus 1024).
// This would allow us to refresh the encryption settings and Container Rid, on the premise that the container recreated could possibly be configured with a new encryption policy.
if (!isRetry &&
responseMessage.StatusCode == HttpStatusCode.BadRequest &&
string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus))
if (!isRetry && CheckIfRequestNeedsARetryPostPolicyRefresh(responseMessage))
{
// Even though the streamPayload position is expected to be 0,
// because for MemoryStream we just use the underlying buffer to send over the wire rather than using the Stream APIs
Expand Down Expand Up @@ -1056,9 +1040,7 @@ private async Task<ResponseMessage> ReadItemHelperAsync(
clonedRequestOptions,
cancellationToken);

if (!isRetry &&
responseMessage.StatusCode == HttpStatusCode.BadRequest &&
string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus))
if (!isRetry && CheckIfRequestNeedsARetryPostPolicyRefresh(responseMessage))
{
// get the latest encryption settings.
await this.GetOrUpdateEncryptionSettingsFromCacheAsync(
Expand Down Expand Up @@ -1131,9 +1113,7 @@ private async Task<ResponseMessage> ReplaceItemHelperAsync(
clonedRequestOptions,
cancellationToken);

if (!isRetry &&
responseMessage.StatusCode == HttpStatusCode.BadRequest &&
string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus))
if (!isRetry && CheckIfRequestNeedsARetryPostPolicyRefresh(responseMessage))
{
streamPayload.Position = 0;
streamPayload = await this.DecryptStreamPayloadAndUpdateEncryptionSettingsAsync(
Expand Down Expand Up @@ -1206,9 +1186,7 @@ private async Task<ResponseMessage> UpsertItemHelperAsync(
clonedRequestOptions,
cancellationToken);

if (!isRetry &&
responseMessage.StatusCode == HttpStatusCode.BadRequest &&
string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus))
if (!isRetry && CheckIfRequestNeedsARetryPostPolicyRefresh(responseMessage))
{
streamPayload.Position = 0;
streamPayload = await this.DecryptStreamPayloadAndUpdateEncryptionSettingsAsync(
Expand All @@ -1235,6 +1213,151 @@ private async Task<ResponseMessage> UpsertItemHelperAsync(
return responseMessage;
}

private async Task<ResponseMessage> PatchItemHelperAsync(
string id,
PartitionKey partitionKey,
IReadOnlyList<PatchOperation> patchOperations,
PatchItemRequestOptions requestOptions,
CosmosDiagnosticsContext diagnosticsContext,
CancellationToken cancellationToken,
bool isRetry = false)
{
EncryptionSettings encryptionSettings = await this.GetOrUpdateEncryptionSettingsFromCacheAsync(
obsoleteEncryptionSettings: null,
cancellationToken: cancellationToken);

PatchItemRequestOptions clonedRequestOptions = requestOptions;

if (!isRetry)
{
if (requestOptions != null)
{
clonedRequestOptions = (PatchItemRequestOptions)requestOptions.ShallowCopy();
}
else
{
clonedRequestOptions = new PatchItemRequestOptions();
}
}

encryptionSettings.SetRequestHeaders(clonedRequestOptions);

List<PatchOperation> encryptedPatchOperations = await this.EncryptPatchItemsAsync(
patchOperations,
encryptionSettings,
cancellationToken);

ResponseMessage responseMessage = await this.container.PatchItemStreamAsync(
id,
partitionKey,
encryptedPatchOperations,
clonedRequestOptions,
cancellationToken);

if (!isRetry && CheckIfRequestNeedsARetryPostPolicyRefresh(responseMessage))
{
// get the latest encryption settings.
await this.GetOrUpdateEncryptionSettingsFromCacheAsync(
obsoleteEncryptionSettings: encryptionSettings,
cancellationToken: cancellationToken);

return await this.PatchItemHelperAsync(
id,
partitionKey,
patchOperations,
clonedRequestOptions,
diagnosticsContext,
cancellationToken,
isRetry: true);
}

responseMessage.Content = await EncryptionProcessor.DecryptAsync(
responseMessage.Content,
encryptionSettings,
diagnosticsContext,
cancellationToken);

return responseMessage;
}

private async Task<List<PatchOperation>> EncryptPatchItemsAsync(
kr-santosh marked this conversation as resolved.
Show resolved Hide resolved
IReadOnlyList<PatchOperation> patchOperations,
EncryptionSettings encryptionSettings,
CancellationToken cancellationToken = default)
{
List<PatchOperation> encryptedPatchOperations = new List<PatchOperation>(patchOperations.Count);

foreach (PatchOperation patchOperation in patchOperations)
{
if (patchOperation.OperationType == PatchOperationType.Remove)
{
encryptedPatchOperations.Add(patchOperation);
continue;
}

if (string.IsNullOrWhiteSpace(patchOperation.Path) || patchOperation.Path[0] != '/')
{
throw new ArgumentException($"Invalid path '{patchOperation.Path}'.");
}

// get the top level path's encryption setting.
EncryptionSettingForProperty settingforProperty = encryptionSettings.GetEncryptionSettingForProperty(
patchOperation.Path.Split('/')[1]);

// non-encrypted path
if (settingforProperty == null)
{
encryptedPatchOperations.Add(patchOperation);
continue;
}
else if (patchOperation.OperationType == PatchOperationType.Increment)
{
throw new InvalidOperationException($"Increment patch operation is not allowed for encrypted path '{patchOperation.Path}'.");
}

if (!patchOperation.TrySerializeValueParameter(this.CosmosSerializer, out Stream valueParam))
{
throw new ArgumentException($"Cannot serialize value parameter for operation: {patchOperation.OperationType}, path: {patchOperation.Path}.");
}

Stream encryptedPropertyValue = await EncryptionProcessor.EncryptValueStreamAsync(
valueParam,
settingforProperty,
cancellationToken);

switch (patchOperation.OperationType)
{
case PatchOperationType.Add:
encryptedPatchOperations.Add(PatchOperation.Add(patchOperation.Path, encryptedPropertyValue));
break;

case PatchOperationType.Replace:
encryptedPatchOperations.Add(PatchOperation.Replace(patchOperation.Path, encryptedPropertyValue));
break;

case PatchOperationType.Set:
encryptedPatchOperations.Add(PatchOperation.Set(patchOperation.Path, encryptedPropertyValue));
break;

default:
throw new NotSupportedException(nameof(patchOperation.OperationType));
}
}

return encryptedPatchOperations;
}

internal static bool CheckIfRequestNeedsARetryPostPolicyRefresh(ResponseMessage responseMessage)
{
if (responseMessage.StatusCode == HttpStatusCode.BadRequest &&
string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus))
{
return true;
}

return false;
}

/// <summary>
/// This method takes in an encrypted Stream payload.
/// The streamPayload is decrypted with the same policy which was used to encrypt and and then the original plain stream payload is
Expand Down Expand Up @@ -1329,9 +1452,7 @@ private async Task<ResponseMessage> ReadManyItemsHelperAsync(
clonedRequestOptions,
cancellationToken);

if (!isRetry &&
responseMessage.StatusCode == HttpStatusCode.BadRequest &&
string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus))
if (!isRetry && CheckIfRequestNeedsARetryPostPolicyRefresh(responseMessage))
{
// get the latest encryption settings.
await this.GetOrUpdateEncryptionSettingsFromCacheAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ public override async Task<ResponseMessage> ReadNextAsync(CancellationToken canc
ResponseMessage responseMessage = await this.feedIterator.ReadNextAsync(cancellationToken);

// check for Bad Request and Wrong RID intended and update the cached RID and Client Encryption Policy.
if (responseMessage.StatusCode == HttpStatusCode.BadRequest
&& string.Equals(responseMessage.Headers.Get(Constants.SubStatusHeader), Constants.IncorrectContainerRidSubStatus))
if (EncryptionContainer.CheckIfRequestNeedsARetryPostPolicyRefresh(responseMessage))
{
await this.encryptionContainer.GetOrUpdateEncryptionSettingsFromCacheAsync(
obsoleteEncryptionSettings: encryptionSettings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,123 @@ await MdeEncryptionTests.ValidateQueryResultsAsync(
testDoc);
}

[TestMethod]
public async Task EncryptionValidatePolicyRefreshPostContainerDeletePatch()
{
Collection<ClientEncryptionIncludedPath> paths = new Collection<ClientEncryptionIncludedPath>()
{
new ClientEncryptionIncludedPath()
{
Path = "/Sensitive_StringFormat",
ClientEncryptionKeyId = "key1",
EncryptionType = "Deterministic",
EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256",
},

new ClientEncryptionIncludedPath()
{
Path = "/Sensitive_NestedObjectFormatL1",
ClientEncryptionKeyId = "key1",
EncryptionType = "Deterministic",
EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256",
},
};

ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths);

ContainerProperties containerProperties = new ContainerProperties(Guid.NewGuid().ToString(), "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy };

Container encryptionContainerToDelete = await database.CreateContainerAsync(containerProperties, 400);
await encryptionContainerToDelete.InitializeEncryptionAsync();

CosmosClient otherClient = TestCommon.CreateCosmosClient(builder => builder
.Build());

CosmosClient otherEncryptionClient = otherClient.WithEncryption(new TestEncryptionKeyStoreProvider());
Database otherDatabase = otherEncryptionClient.GetDatabase(MdeEncryptionTests.database.Id);

Container otherEncryptionContainer = otherDatabase.GetContainer(encryptionContainerToDelete.Id);

await MdeEncryptionTests.MdeCreateItemAsync(otherEncryptionContainer);
kr-santosh marked this conversation as resolved.
Show resolved Hide resolved

// Client 1 Deletes the Container referenced in Client 2 and Recreate with different policy
using (await database.GetContainer(encryptionContainerToDelete.Id).DeleteContainerStreamAsync())
{ }

paths = new Collection<ClientEncryptionIncludedPath>()
{
new ClientEncryptionIncludedPath()
{
Path = "/Sensitive_IntArray",
ClientEncryptionKeyId = "key1",
EncryptionType = "Deterministic",
EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256",
},

new ClientEncryptionIncludedPath()
{
Path = "/Sensitive_DecimalFormat",
ClientEncryptionKeyId = "key2",
EncryptionType = "Deterministic",
EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256",
},

new ClientEncryptionIncludedPath()
{
Path = "/Sensitive_FloatFormat",
ClientEncryptionKeyId = "key1",
EncryptionType = "Deterministic",
EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256",
},

new ClientEncryptionIncludedPath()
{
Path = "/Sensitive_ArrayFormat",
ClientEncryptionKeyId = "key2",
EncryptionType = "Deterministic",
EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256",
},
};

clientEncryptionPolicy = new ClientEncryptionPolicy(paths);

containerProperties = new ContainerProperties(encryptionContainerToDelete.Id, "/PK") { ClientEncryptionPolicy = clientEncryptionPolicy };

ContainerResponse containerResponse = await database.CreateContainerAsync(containerProperties, 400);

TestDoc docPostPatching = await MdeEncryptionTests.MdeCreateItemAsync(encryptionContainerToDelete);


kr-santosh marked this conversation as resolved.
Show resolved Hide resolved
// request should fail and pick up new policy.
docPostPatching.NonSensitive = Guid.NewGuid().ToString();
docPostPatching.NonSensitiveInt++;
docPostPatching.Sensitive_StringFormat = Guid.NewGuid().ToString();
docPostPatching.Sensitive_DateFormat = new DateTime(2020, 02, 02);
docPostPatching.Sensitive_DecimalFormat = 11.11m;
docPostPatching.Sensitive_IntArray[1] = 19877;
docPostPatching.Sensitive_IntMultiDimArray[1, 0] = 19877;
docPostPatching.Sensitive_IntFormat = 2020;
docPostPatching.Sensitive_BoolFormat = false;
docPostPatching.Sensitive_FloatFormat = 2020.20f;

// Maximum 10 operations at a time (current limit)
List<PatchOperation> patchOperations = new List<PatchOperation>
{
PatchOperation.Increment("/NonSensitiveInt", 1),
PatchOperation.Replace("/NonSensitive", docPostPatching.NonSensitive),
PatchOperation.Replace("/Sensitive_StringFormat", docPostPatching.Sensitive_StringFormat),
PatchOperation.Replace("/Sensitive_DateFormat", docPostPatching.Sensitive_DateFormat),
PatchOperation.Replace("/Sensitive_DecimalFormat", docPostPatching.Sensitive_DecimalFormat),
PatchOperation.Set("/Sensitive_IntArray/1", docPostPatching.Sensitive_IntArray[1]),
PatchOperation.Set("/Sensitive_IntMultiDimArray/1/0", docPostPatching.Sensitive_IntMultiDimArray[1,0]),
kr-santosh marked this conversation as resolved.
Show resolved Hide resolved
PatchOperation.Replace("/Sensitive_IntFormat", docPostPatching.Sensitive_IntFormat),
PatchOperation.Replace("/Sensitive_BoolFormat", docPostPatching.Sensitive_BoolFormat),
PatchOperation.Replace("/Sensitive_FloatFormat", docPostPatching.Sensitive_FloatFormat),
};

await MdeEncryptionTests.MdePatchItemAsync(otherEncryptionContainer, patchOperations, docPostPatching, HttpStatusCode.OK);
kr-santosh marked this conversation as resolved.
Show resolved Hide resolved
}

[TestMethod]
public async Task EncryptionValidatePolicyRefreshPostDatabaseDelete()
{
Expand Down