From ca108c1017c7bd6e4d00d9779e229f3c24916dde Mon Sep 17 00:00:00 2001 From: DmitryLukyanov Date: Thu, 15 Dec 2022 22:58:35 +0400 Subject: [PATCH 1/6] CSHARP-4255: Fix bug and some tests. --- .../Encryption/ClientEncryption.cs | 28 ++-- .../BsonValueEquivalencyComparer.cs | 17 ++- .../Encryption/ClientEncryptionTests.cs | 142 +++++++++++++++++- .../prose-tests/ClientEncryptionProseTests.cs | 5 +- 4 files changed, 167 insertions(+), 25 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/ClientEncryption.cs b/src/MongoDB.Driver/Encryption/ClientEncryption.cs index 7dfadcb36b5..a3826e06abb 100644 --- a/src/MongoDB.Driver/Encryption/ClientEncryption.cs +++ b/src/MongoDB.Driver/Encryption/ClientEncryption.cs @@ -82,7 +82,8 @@ public Task AddAlternateKeyNameAsync(Guid id, string alternateKeyN /// /// Create encrypted collection. /// - /// The collection namespace. + /// The database. + /// The collectionName. /// The create collection options. /// The kms provider. /// The datakey options. @@ -90,28 +91,28 @@ public Task AddAlternateKeyNameAsync(Guid id, string alternateKeyN /// /// if EncryptionFields contains a keyId with a null value, a data key will be automatically generated and assigned to keyId value. /// - public void CreateEncryptedCollection(CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default) + public void CreateEncryptedCollection(IMongoDatabase database, string collectionName, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default) { - Ensure.IsNotNull(collectionNamespace, nameof(collectionNamespace)); + Ensure.IsNotNull(database, nameof(database)); + Ensure.IsNotNull(collectionName, nameof(collectionName)); Ensure.IsNotNull(createCollectionOptions, nameof(createCollectionOptions)); Ensure.IsNotNull(dataKeyOptions, nameof(dataKeyOptions)); Ensure.IsNotNull(kmsProvider, nameof(kmsProvider)); - foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(collectionNamespace, createCollectionOptions.EncryptedFields)) + foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(new CollectionNamespace(database.DatabaseNamespace.DatabaseName, collectionName), createCollectionOptions.EncryptedFields)) { var dataKey = CreateDataKey(kmsProvider, dataKeyOptions, cancellationToken); EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey); } - var database = _libMongoCryptController.KeyVaultClient.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName); - - database.CreateCollection(collectionNamespace.CollectionName, createCollectionOptions, cancellationToken); + database.CreateCollection(collectionName, createCollectionOptions, cancellationToken); } /// /// Create encrypted collection. /// - /// The collection namespace. + /// The database. + /// The collectionName. /// The create collection options. /// The kms provider. /// The datakey options. @@ -119,22 +120,21 @@ public void CreateEncryptedCollection(CollectionNamespace collectio /// /// if EncryptionFields contains a keyId with a null value, a data key will be automatically generated and assigned to keyId value. /// - public async Task CreateEncryptedCollectionAsync(CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default) + public async Task CreateEncryptedCollectionAsync(IMongoDatabase database, string collectionName, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default) { - Ensure.IsNotNull(collectionNamespace, nameof(collectionNamespace)); + Ensure.IsNotNull(database, nameof(database)); + Ensure.IsNotNull(collectionName, nameof(collectionName)); Ensure.IsNotNull(createCollectionOptions, nameof(createCollectionOptions)); Ensure.IsNotNull(dataKeyOptions, nameof(dataKeyOptions)); Ensure.IsNotNull(kmsProvider, nameof(kmsProvider)); - foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(collectionNamespace, createCollectionOptions.EncryptedFields)) + foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(new CollectionNamespace(database.DatabaseNamespace.DatabaseName, collectionName), createCollectionOptions.EncryptedFields)) { var dataKey = await CreateDataKeyAsync(kmsProvider, dataKeyOptions, cancellationToken).ConfigureAwait(false); EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey); } - var database = _libMongoCryptController.KeyVaultClient.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName); - - await database.CreateCollectionAsync(collectionNamespace.CollectionName, createCollectionOptions, cancellationToken).ConfigureAwait(false); + await database.CreateCollectionAsync(collectionName, createCollectionOptions, cancellationToken).ConfigureAwait(false); } /// diff --git a/tests/MongoDB.Bson.TestHelpers/BsonValueEquivalencyComparer.cs b/tests/MongoDB.Bson.TestHelpers/BsonValueEquivalencyComparer.cs index 692a1e1e927..b15a42168d9 100644 --- a/tests/MongoDB.Bson.TestHelpers/BsonValueEquivalencyComparer.cs +++ b/tests/MongoDB.Bson.TestHelpers/BsonValueEquivalencyComparer.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using System; using System.Collections.Generic; namespace MongoDB.Bson.TestHelpers @@ -22,15 +23,17 @@ public class BsonValueEquivalencyComparer : IEqualityComparer #region static public static BsonValueEquivalencyComparer Instance { get; } = new BsonValueEquivalencyComparer(); - public static bool Compare(BsonValue a, BsonValue b) + public static bool Compare(BsonValue a, BsonValue b, Action massageAction = null) { + massageAction?.Invoke(a, b); + if (a.BsonType == BsonType.Document && b.BsonType == BsonType.Document) { - return CompareDocuments((BsonDocument)a, (BsonDocument)b); + return CompareDocuments((BsonDocument)a, (BsonDocument)b, massageAction); } else if (a.BsonType == BsonType.Array && b.BsonType == BsonType.Array) { - return CompareArrays((BsonArray)a, (BsonArray)b); + return CompareArrays((BsonArray)a, (BsonArray)b, massageAction); } else if (a.BsonType == b.BsonType) { @@ -50,7 +53,7 @@ public static bool Compare(BsonValue a, BsonValue b) } } - private static bool CompareArrays(BsonArray a, BsonArray b) + private static bool CompareArrays(BsonArray a, BsonArray b, Action massageAction = null) { if (a.Count != b.Count) { @@ -59,7 +62,7 @@ private static bool CompareArrays(BsonArray a, BsonArray b) for (var i = 0; i < a.Count; i++) { - if (!Compare(a[i], b[i])) + if (!Compare(a[i], b[i], massageAction)) { return false; } @@ -68,7 +71,7 @@ private static bool CompareArrays(BsonArray a, BsonArray b) return true; } - private static bool CompareDocuments(BsonDocument a, BsonDocument b) + private static bool CompareDocuments(BsonDocument a, BsonDocument b, Action massageAction = null) { if (a.ElementCount != b.ElementCount) { @@ -83,7 +86,7 @@ private static bool CompareDocuments(BsonDocument a, BsonDocument b) return false; } - if (!Compare(aElement.Value, bElement.Value)) + if (!Compare(aElement.Value, bElement.Value, massageAction)) { return false; } diff --git a/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs b/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs index 70b9607ba3b..caec3a9be28 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs @@ -25,6 +25,9 @@ using MongoDB.Driver.Tests.Specifications.client_side_encryption; using MongoDB.Libmongocrypt; using Xunit; +using Moq; +using System.Collections.Generic; +using System.Threading; namespace MongoDB.Driver.Tests.Encryption { @@ -64,6 +67,141 @@ public async Task CreateDataKey_should_correctly_handle_input_arguments() } } + [Fact] + public async Task CreateEncryptedCollection_should_handle_input_arguments() + { + const string kmsProvider = "local"; + const string collectionName = "collName"; + var createCollectionOptions = new CreateCollectionOptions(); + var database = Mock.Of(); + + var dataKeyOptions = new DataKeyOptions(); + + using (var subject = CreateSubject()) + { + ShouldBeArgumentException(Record.Exception(() => subject.CreateEncryptedCollection(database: null, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedParamName: "database"); + ShouldBeArgumentException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database: null, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedParamName: "database"); + + ShouldBeArgumentException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName: null, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedParamName: "collectionName"); + ShouldBeArgumentException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName: null, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedParamName: "collectionName"); + + ShouldBeArgumentException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName: collectionName, createCollectionOptions: null, kmsProvider, dataKeyOptions)), expectedParamName: "createCollectionOptions"); + ShouldBeArgumentException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions: null, kmsProvider, dataKeyOptions)), expectedParamName: "createCollectionOptions"); + + ShouldBeArgumentException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName: collectionName, createCollectionOptions, kmsProvider: null, dataKeyOptions)), expectedParamName: "kmsProvider"); + ShouldBeArgumentException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider: null, dataKeyOptions)), expectedParamName: "kmsProvider"); + + ShouldBeArgumentException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName: collectionName, createCollectionOptions, kmsProvider, dataKeyOptions: null)), expectedParamName: "dataKeyOptions"); + ShouldBeArgumentException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions: null)), expectedParamName: "dataKeyOptions"); + } + } + + [Fact] + public async Task CreateEncryptedCollection_should_handle_save_generated_key_when_second_key_failed() + { + const string kmsProvider = "local"; + const string collectionName = "collName"; + const string encryptedFieldsStr = "{ fields : [{ keyId : null }, { keyId : null }] }"; + var database = Mock.Of(d => d.DatabaseNamespace == new DatabaseNamespace("db")); + + var dataKeyOptions = new DataKeyOptions(); + + var mockCollection = new Mock>(); + mockCollection + .SetupSequence(c => c.InsertOne(It.IsAny(), It.IsAny(), It.IsAny())) + .Pass() + .Throws(new Exception("test")); + mockCollection + .SetupSequence(c => c.InsertOneAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask) + .Throws(new Exception("test")); + var mockDatabase = new Mock(); + mockDatabase.Setup(c => c.GetCollection(It.IsAny(), It.IsAny())).Returns(mockCollection.Object); + var client = new Mock(); + client.Setup(c => c.GetDatabase(It.IsAny(), It.IsAny())).Returns(mockDatabase.Object); + + using (var subject = CreateSubject(client.Object)) + { + var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = BsonDocument.Parse(encryptedFieldsStr) }; + var exception = Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)); + AssertResults(exception.InnerException, createCollectionOptions); + + createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = BsonDocument.Parse(encryptedFieldsStr) }; + exception = await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)); + AssertResults(exception.InnerException, createCollectionOptions); + } + + void AssertResults(Exception ex, CreateCollectionOptions createCollectionOptions) + { + ex.Should().BeOfType().Which.Message.Should().Be("test"); + var fields = createCollectionOptions.EncryptedFields["fields"].AsBsonArray; + fields[0].AsBsonDocument["keyId"].Should().BeOfType(); // pass + /* + - If generating `D` resulted in an error `E`, the entire + `CreateEncryptedCollection` must now fail with error `E`. Return the + partially-formed `EF'` with the error so that the caller may know what + datakeys have already been created by the helper. + */ + fields[1].AsBsonDocument["keyId"].Should().BeOfType(); // throw + } + } + + [Theory] + [InlineData(null, "There are no encrypted fields defined for the collection.")] + [InlineData("{}", "{}")] + [InlineData("{ a : 1 }", "{ a : 1 }")] + [InlineData("{ fields : { } }", "{ fields: { } }")] + [InlineData("{ fields : [] }", "{ fields: [] }")] + [InlineData("{ fields : [{ a : 1 }] }", "{ fields: [{ a : 1 }] }")] + [InlineData("{ fields : [{ keyId : 1 }] }", "{ fields: [{ keyId : 1 }] }")] + [InlineData("{ fields : [{ keyId : null }] }", "{ fields: [{ keyId : '#binary_generated#' }] }")] + [InlineData("{ fields : [{ keyId : null }, { keyId : null }] }", "{ fields: [{ keyId : '#binary_generated#' }, { keyId : '#binary_generated#' }] }")] + [InlineData("{ fields : [{ keyId : 3 }, { keyId : null }] }", "{ fields: [{ keyId : 3 }, { keyId : '#binary_generated#' }] }")] + public async Task CreateEncryptedCollection_should_handle_various_encryptedFields(string encryptedFieldsStr, string expectedResult) + { + const string kmsProvider = "local"; + const string collectionName = "collName"; + var database = Mock.Of(d => d.DatabaseNamespace == new DatabaseNamespace("db")); + + var dataKeyOptions = new DataKeyOptions(); + + using (var subject = CreateSubject()) + { + if (BsonDocument.TryParse(expectedResult, out var encryptedFields)) + { + var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; + subject.CreateEncryptedCollection(database, collectionName: collectionName, createCollectionOptions, kmsProvider: kmsProvider, dataKeyOptions); + createCollectionOptions.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone()); + + createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; + await subject.CreateEncryptedCollectionAsync(database, collectionName: collectionName, createCollectionOptions, kmsProvider: kmsProvider, dataKeyOptions); + createCollectionOptions.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone()); + } + else + { + var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; + AssertInvalidOperationException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName: collectionName, createCollectionOptions, kmsProvider: kmsProvider, dataKeyOptions)), expectedResult); + + createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; + AssertInvalidOperationException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName: collectionName, createCollectionOptions, kmsProvider: kmsProvider, dataKeyOptions)), expectedResult); + } + } + + void AssertInvalidOperationException(Exception ex, string message) => ex.Should().BeOfType().Which.Message.Should().Be(message); + } + + private class EncryptedFieldsComparer : IEqualityComparer + { + public bool Equals(BsonDocument x, BsonDocument y) => BsonValueEquivalencyComparer.Compare(x, y, (a, b) => + { + if (a is BsonDocument aDocument && aDocument.TryGetValue("keyId", out var aKeyId) && aKeyId.IsBsonBinaryData && + b is BsonDocument bDocument && bDocument.TryGetValue("keyId", out var bKeyId) && bKeyId == "#binary_generated#") + { + bDocument["keyId"] = aDocument["keyId"]; + } + }); + public int GetHashCode(BsonDocument obj) => obj.GetHashCode(); + } [Fact] public void CryptClient_should_be_initialized() @@ -167,10 +305,10 @@ public async Task RewrapManyDataKey_should_correctly_handle_input_arguments() } // private methods - private ClientEncryption CreateSubject() + private ClientEncryption CreateSubject(IMongoClient client = null) { var clientEncryptionOptions = new ClientEncryptionOptions( - DriverTestConfiguration.Client, + client ?? DriverTestConfiguration.Client, __keyVaultCollectionNamespace, kmsProviders: EncryptionTestHelper.GetKmsProviders(filter: "local")); diff --git a/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs b/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs index 4610051a939..0435ae3d031 100644 --- a/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs +++ b/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs @@ -2339,14 +2339,15 @@ private IMongoCollection CreateEncryptedCollection(IMongoClient cl private IMongoCollection CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, bool async) { var datakeyOptions = CreateDataKeyOptions(kmsProvider); + var database = client.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName); if (async) { - clientEncryption.CreateEncryptedCollectionAsync(collectionNamespace, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default).GetAwaiter().GetResult(); + clientEncryption.CreateEncryptedCollectionAsync(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default).GetAwaiter().GetResult(); } else { - clientEncryption.CreateEncryptedCollection(collectionNamespace, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default); + clientEncryption.CreateEncryptedCollection(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default); } return client.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName).GetCollection(collectionNamespace.CollectionName); From 28cf23d15b992632ee8c6bc4960261e6efe5024b Mon Sep 17 00:00:00 2001 From: DmitryLukyanov Date: Mon, 16 Jan 2023 18:10:30 +0400 Subject: [PATCH 2/6] Code review. --- src/MongoDB.Driver/CreateCollectionOptions.cs | 52 +++++++++++++++++ .../Encryption/ClientEncryption.cs | 50 +++++++++++++---- .../CreateEncryptedCollectionResult.cs | 38 +++++++++++++ ...ongoEncryptionCreateCollectionException.cs | 56 +++++++++++++++++++ .../Encryption/MongoEncryptionException.cs | 1 - .../Encryption/ClientEncryptionTests.cs | 52 ++++++++++------- .../prose-tests/ClientEncryptionProseTests.cs | 4 +- 7 files changed, 218 insertions(+), 35 deletions(-) create mode 100644 src/MongoDB.Driver/Encryption/CreateEncryptedCollectionResult.cs create mode 100644 src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs diff --git a/src/MongoDB.Driver/CreateCollectionOptions.cs b/src/MongoDB.Driver/CreateCollectionOptions.cs index 1b3cbb5929f..e2233deb3ed 100644 --- a/src/MongoDB.Driver/CreateCollectionOptions.cs +++ b/src/MongoDB.Driver/CreateCollectionOptions.cs @@ -196,6 +196,29 @@ public DocumentValidationLevel? ValidationLevel get { return _validationLevel; } set { _validationLevel = value; } } + + internal virtual CreateCollectionOptions Clone() + { + var clone = new CreateCollectionOptions(); + clone._autoIndexId = _autoIndexId; + clone._capped = _capped; + clone._changeStreamPreAndPostImagesOptions = _changeStreamPreAndPostImagesOptions; + clone._collation = _collation; + clone._encryptedFields = _encryptedFields; + clone._expireAfter = _expireAfter; + clone._indexOptionDefaults = _indexOptionDefaults; + clone._maxDocuments = _maxDocuments; + clone._maxSize = _maxSize; + clone._noPadding = _noPadding; + clone._serializerRegistry = _serializerRegistry; + clone._storageEngine = _storageEngine; + clone._timeSeriesOptions = _timeSeriesOptions; + clone._usePowerOf2Sizes = _usePowerOf2Sizes; + clone._validationAction = _validationAction; + clone._validationLevel = _validationLevel; + + return clone; + } } /// @@ -282,5 +305,34 @@ public FilterDefinition Validator get { return _validator; } set { _validator = value; } } + + internal override CreateCollectionOptions Clone() + { + var clone = new CreateCollectionOptions(); +#pragma warning disable CS0618 // Type or member is obsolete + clone.AutoIndexId = base.AutoIndexId; +#pragma warning restore CS0618 // Type or member is obsolete + clone.Capped = base.Capped; + clone.ChangeStreamPreAndPostImagesOptions = base.ChangeStreamPreAndPostImagesOptions; + clone.Collation = base.Collation; + clone.EncryptedFields = base.EncryptedFields; + clone.ExpireAfter = base.ExpireAfter; + clone.IndexOptionDefaults = base.IndexOptionDefaults; + clone.MaxDocuments = base.MaxDocuments; + clone.MaxSize = base.MaxSize; + clone.NoPadding = base.NoPadding; + clone.SerializerRegistry = base.SerializerRegistry; + clone.StorageEngine = base.StorageEngine; + clone.TimeSeriesOptions = base.TimeSeriesOptions; + clone.UsePowerOf2Sizes = base.UsePowerOf2Sizes; + clone.ValidationAction = base.ValidationAction; + clone.ValidationLevel = base.ValidationLevel; + + clone._clusteredIndex = _clusteredIndex; + clone._documentSerializer = _documentSerializer; + clone._validator = _validator; + + return clone; + } } } diff --git a/src/MongoDB.Driver/Encryption/ClientEncryption.cs b/src/MongoDB.Driver/Encryption/ClientEncryption.cs index a3826e06abb..ef436a282a7 100644 --- a/src/MongoDB.Driver/Encryption/ClientEncryption.cs +++ b/src/MongoDB.Driver/Encryption/ClientEncryption.cs @@ -83,15 +83,16 @@ public Task AddAlternateKeyNameAsync(Guid id, string alternateKeyN /// Create encrypted collection. /// /// The database. - /// The collectionName. + /// The collection name. /// The create collection options. /// The kms provider. /// The datakey options. /// The cancellation token. + /// The operation result. /// /// if EncryptionFields contains a keyId with a null value, a data key will be automatically generated and assigned to keyId value. /// - public void CreateEncryptedCollection(IMongoDatabase database, string collectionName, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default) + public CreateEncryptedCollectionResult CreateEncryptedCollection(IMongoDatabase database, string collectionName, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default) { Ensure.IsNotNull(database, nameof(database)); Ensure.IsNotNull(collectionName, nameof(collectionName)); @@ -99,28 +100,41 @@ public void CreateEncryptedCollection(IMongoDatabase database, string collection Ensure.IsNotNull(dataKeyOptions, nameof(dataKeyOptions)); Ensure.IsNotNull(kmsProvider, nameof(kmsProvider)); - foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(new CollectionNamespace(database.DatabaseNamespace.DatabaseName, collectionName), createCollectionOptions.EncryptedFields)) + var encryptedFields = createCollectionOptions.EncryptedFields?.DeepClone()?.AsBsonDocument; + try { - var dataKey = CreateDataKey(kmsProvider, dataKeyOptions, cancellationToken); - EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey); + foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(new CollectionNamespace(database.DatabaseNamespace.DatabaseName, collectionName), encryptedFields)) + { + var dataKey = CreateDataKey(kmsProvider, dataKeyOptions, cancellationToken); + EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey); + } + + var effectiveCreateEncryptionOptions = createCollectionOptions.Clone(); + effectiveCreateEncryptionOptions.EncryptedFields = encryptedFields; + database.CreateCollection(collectionName, effectiveCreateEncryptionOptions, cancellationToken); + } + catch (Exception ex) + { + throw new MongoEncryptionCreateCollectionException(ex, encryptedFields); } - database.CreateCollection(collectionName, createCollectionOptions, cancellationToken); + return new CreateEncryptedCollectionResult(encryptedFields); } /// /// Create encrypted collection. /// /// The database. - /// The collectionName. + /// The collection name. /// The create collection options. /// The kms provider. /// The datakey options. /// The cancellation token. + /// The operation result. /// /// if EncryptionFields contains a keyId with a null value, a data key will be automatically generated and assigned to keyId value. /// - public async Task CreateEncryptedCollectionAsync(IMongoDatabase database, string collectionName, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default) + public async Task CreateEncryptedCollectionAsync(IMongoDatabase database, string collectionName, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default) { Ensure.IsNotNull(database, nameof(database)); Ensure.IsNotNull(collectionName, nameof(collectionName)); @@ -128,13 +142,25 @@ public async Task CreateEncryptedCollectionAsync(IMongoDatabase database, string Ensure.IsNotNull(dataKeyOptions, nameof(dataKeyOptions)); Ensure.IsNotNull(kmsProvider, nameof(kmsProvider)); - foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(new CollectionNamespace(database.DatabaseNamespace.DatabaseName, collectionName), createCollectionOptions.EncryptedFields)) + var encryptedFields = createCollectionOptions.EncryptedFields?.DeepClone()?.AsBsonDocument; + try + { + foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(new CollectionNamespace(database.DatabaseNamespace.DatabaseName, collectionName), encryptedFields)) + { + var dataKey = await CreateDataKeyAsync(kmsProvider, dataKeyOptions, cancellationToken).ConfigureAwait(false); + EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey); + } + + var effectiveCreateEncryptionOptions = createCollectionOptions.Clone(); + effectiveCreateEncryptionOptions.EncryptedFields = encryptedFields; + await database.CreateCollectionAsync(collectionName, effectiveCreateEncryptionOptions, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) { - var dataKey = await CreateDataKeyAsync(kmsProvider, dataKeyOptions, cancellationToken).ConfigureAwait(false); - EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey); + throw new MongoEncryptionCreateCollectionException(ex, encryptedFields); } - await database.CreateCollectionAsync(collectionName, createCollectionOptions, cancellationToken).ConfigureAwait(false); + return new CreateEncryptedCollectionResult(encryptedFields); } /// diff --git a/src/MongoDB.Driver/Encryption/CreateEncryptedCollectionResult.cs b/src/MongoDB.Driver/Encryption/CreateEncryptedCollectionResult.cs new file mode 100644 index 00000000000..02b36108e97 --- /dev/null +++ b/src/MongoDB.Driver/Encryption/CreateEncryptedCollectionResult.cs @@ -0,0 +1,38 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using MongoDB.Bson; + +namespace MongoDB.Driver.Encryption +{ + /// + /// Represents the result of a create encrypted collection. + /// + public class CreateEncryptedCollectionResult + { + private readonly BsonDocument _encryptedFields; + + /// + /// Initializes a new instance of the class. + /// + /// The encrypted fields document. + public CreateEncryptedCollectionResult(BsonDocument encryptedFields) => _encryptedFields = encryptedFields; + + /// + /// The encrypted fields document. + /// + public BsonDocument EncryptedFields => _encryptedFields; + } +} diff --git a/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs b/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs new file mode 100644 index 00000000000..09532eef51b --- /dev/null +++ b/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs @@ -0,0 +1,56 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Runtime.Serialization; +using MongoDB.Bson; + +namespace MongoDB.Driver.Encryption +{ + /// + /// Represents an encryption exception. + /// + [Serializable] + public class MongoEncryptionCreateCollectionException : MongoEncryptionException + { + private readonly BsonDocument _encryptedFields; + + /// + /// Initializes a new instance of the class. + /// + /// The inner exception. + /// The encrypted fields. + public MongoEncryptionCreateCollectionException(Exception innerException, BsonDocument encryptedFields) + : base(innerException) + { + _encryptedFields = encryptedFields; + } + + /// + /// Initializes a new instance of the class (this overload used by deserialization). + /// + /// The SerializationInfo. + /// The StreamingContext. + protected MongoEncryptionCreateCollectionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// The encrypted fields. + /// + public BsonDocument EncryptedFields => _encryptedFields; + } +} diff --git a/src/MongoDB.Driver/Encryption/MongoEncryptionException.cs b/src/MongoDB.Driver/Encryption/MongoEncryptionException.cs index 5e264539c07..78b6ca522db 100644 --- a/src/MongoDB.Driver/Encryption/MongoEncryptionException.cs +++ b/src/MongoDB.Driver/Encryption/MongoEncryptionException.cs @@ -15,7 +15,6 @@ using System; using System.Runtime.Serialization; -using MongoDB.Driver.Core.Misc; namespace MongoDB.Driver.Encryption { diff --git a/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs b/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs index caec3a9be28..36dbe2d63e3 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs @@ -124,17 +124,22 @@ public async Task CreateEncryptedCollection_should_handle_save_generated_key_whe { var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = BsonDocument.Parse(encryptedFieldsStr) }; var exception = Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)); - AssertResults(exception.InnerException, createCollectionOptions); + AssertResults(exception, createCollectionOptions); createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = BsonDocument.Parse(encryptedFieldsStr) }; exception = await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)); - AssertResults(exception.InnerException, createCollectionOptions); + AssertResults(exception, createCollectionOptions); } void AssertResults(Exception ex, CreateCollectionOptions createCollectionOptions) { - ex.Should().BeOfType().Which.Message.Should().Be("test"); - var fields = createCollectionOptions.EncryptedFields["fields"].AsBsonArray; + var createCollectionException = ex.Should().BeOfType().Subject; + createCollectionException + .InnerException + .Should().BeOfType().Subject.InnerException + .Should().BeOfType().Which.Message + .Should().Be("test"); + var fields = createCollectionException.EncryptedFields["fields"].AsBsonArray; fields[0].AsBsonDocument["keyId"].Should().BeOfType(); // pass /* - If generating `D` resulted in an error `E`, the entire @@ -170,36 +175,43 @@ public async Task CreateEncryptedCollection_should_handle_various_encryptedField if (BsonDocument.TryParse(expectedResult, out var encryptedFields)) { var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; - subject.CreateEncryptedCollection(database, collectionName: collectionName, createCollectionOptions, kmsProvider: kmsProvider, dataKeyOptions); - createCollectionOptions.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone()); + var effectiveEncryptedFields = subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions); + effectiveEncryptedFields.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone()); createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; - await subject.CreateEncryptedCollectionAsync(database, collectionName: collectionName, createCollectionOptions, kmsProvider: kmsProvider, dataKeyOptions); - createCollectionOptions.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone()); + effectiveEncryptedFields = await subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions); + effectiveEncryptedFields.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone()); } else { var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; - AssertInvalidOperationException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName: collectionName, createCollectionOptions, kmsProvider: kmsProvider, dataKeyOptions)), expectedResult); + AssertInvalidOperationException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedResult); createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; - AssertInvalidOperationException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName: collectionName, createCollectionOptions, kmsProvider: kmsProvider, dataKeyOptions)), expectedResult); + AssertInvalidOperationException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedResult); } } - void AssertInvalidOperationException(Exception ex, string message) => ex.Should().BeOfType().Which.Message.Should().Be(message); + void AssertInvalidOperationException(Exception ex, string message) => + ex + .Should().BeOfType().Subject.InnerException + .Should().BeOfType().Which.Message.Should().Be(message); } - private class EncryptedFieldsComparer : IEqualityComparer + private sealed class EncryptedFieldsComparer : IEqualityComparer { - public bool Equals(BsonDocument x, BsonDocument y) => BsonValueEquivalencyComparer.Compare(x, y, (a, b) => - { - if (a is BsonDocument aDocument && aDocument.TryGetValue("keyId", out var aKeyId) && aKeyId.IsBsonBinaryData && - b is BsonDocument bDocument && bDocument.TryGetValue("keyId", out var bKeyId) && bKeyId == "#binary_generated#") - { - bDocument["keyId"] = aDocument["keyId"]; - } - }); + public bool Equals(BsonDocument x, BsonDocument y) => + BsonValueEquivalencyComparer.Compare( + x, y, + massageAction: (a, b) => + { + if (a is BsonDocument aDocument && aDocument.TryGetValue("keyId", out var aKeyId) && aKeyId.IsBsonBinaryData && + b is BsonDocument bDocument && bDocument.TryGetValue("keyId", out var bKeyId) && bKeyId == "#binary_generated#") + { + bDocument["keyId"] = aDocument["keyId"]; + } + }); + public int GetHashCode(BsonDocument obj) => obj.GetHashCode(); } diff --git a/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs b/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs index 0435ae3d031..e5c4ecad392 100644 --- a/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs +++ b/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs @@ -2343,11 +2343,11 @@ private IMongoCollection CreateEncryptedCollection(IMongoClient cl if (async) { - clientEncryption.CreateEncryptedCollectionAsync(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default).GetAwaiter().GetResult(); + _ = clientEncryption.CreateEncryptedCollectionAsync(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default).GetAwaiter().GetResult(); } else { - clientEncryption.CreateEncryptedCollection(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default); + _ = clientEncryption.CreateEncryptedCollection(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default); } return client.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName).GetCollection(collectionNamespace.CollectionName); From 4029fd38ed55942f6bc8536ac9653d1b7245e4c7 Mon Sep 17 00:00:00 2001 From: DmitryLukyanov Date: Mon, 16 Jan 2023 18:50:33 +0400 Subject: [PATCH 3/6] Add test. --- ...ongoEncryptionCreateCollectionException.cs | 13 +++++ ...ncryptionCreateCollectionExceptionTests.cs | 47 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs diff --git a/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs b/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs index 09532eef51b..90955257f5a 100644 --- a/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs +++ b/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs @@ -46,11 +46,24 @@ public MongoEncryptionCreateCollectionException(Exception innerException, BsonDo protected MongoEncryptionCreateCollectionException(SerializationInfo info, StreamingContext context) : base(info, context) { + _encryptedFields = (BsonDocument)info.GetValue("_encryptedFields", typeof(BsonDocument)); } /// /// The encrypted fields. /// public BsonDocument EncryptedFields => _encryptedFields; + + // public methods + /// + /// Gets the object data. + /// + /// The information. + /// The context. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("_encryptedFields", _encryptedFields); + } } } diff --git a/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs b/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs new file mode 100644 index 00000000000..3223eb3a7a9 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs @@ -0,0 +1,47 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Driver.Encryption; +using Xunit; + +namespace MongoDB.Driver.Tests.Encryption +{ + public class MongoEncryptionCreateCollectionExceptionTests + { + [Fact] + public void Serialization_should_work() + { + var subject = new MongoEncryptionCreateCollectionException(new Exception("inner"), new BsonDocument("value", 1)); + + var formatter = new BinaryFormatter(); + using (var stream = new MemoryStream()) + { +#pragma warning disable SYSLIB0011 // BinaryFormatter serialization is obsolete + formatter.Serialize(stream, subject); + stream.Position = 0; + var rehydrated = (MongoEncryptionCreateCollectionException)formatter.Deserialize(stream); +#pragma warning restore SYSLIB0011 // BinaryFormatter serialization is obsolete + + rehydrated.InnerException.Message.Should().Be(subject.InnerException.Message); + rehydrated.EncryptedFields.Should().Be(subject.EncryptedFields).And.Should().NotBeNull(); + } + } + } +} From 308349b19a58712a28e43534bc264991c61a8a79 Mon Sep 17 00:00:00 2001 From: DmitryLukyanov Date: Tue, 17 Jan 2023 00:12:48 +0400 Subject: [PATCH 4/6] Code review. --- .../Encryption/CreateEncryptedCollectionResult.cs | 2 +- .../MongoEncryptionCreateCollectionException.cs | 4 ++-- .../Encryption/ClientEncryptionTests.cs | 15 ++++++--------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CreateEncryptedCollectionResult.cs b/src/MongoDB.Driver/Encryption/CreateEncryptedCollectionResult.cs index 02b36108e97..30476cf8f0a 100644 --- a/src/MongoDB.Driver/Encryption/CreateEncryptedCollectionResult.cs +++ b/src/MongoDB.Driver/Encryption/CreateEncryptedCollectionResult.cs @@ -20,7 +20,7 @@ namespace MongoDB.Driver.Encryption /// /// Represents the result of a create encrypted collection. /// - public class CreateEncryptedCollectionResult + public sealed class CreateEncryptedCollectionResult { private readonly BsonDocument _encryptedFields; diff --git a/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs b/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs index 90955257f5a..9a581233538 100644 --- a/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs +++ b/src/MongoDB.Driver/Encryption/MongoEncryptionCreateCollectionException.cs @@ -46,7 +46,7 @@ public MongoEncryptionCreateCollectionException(Exception innerException, BsonDo protected MongoEncryptionCreateCollectionException(SerializationInfo info, StreamingContext context) : base(info, context) { - _encryptedFields = (BsonDocument)info.GetValue("_encryptedFields", typeof(BsonDocument)); + _encryptedFields = (BsonDocument)info.GetValue(nameof(_encryptedFields), typeof(BsonDocument)); } /// @@ -63,7 +63,7 @@ protected MongoEncryptionCreateCollectionException(SerializationInfo info, Strea public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); - info.AddValue("_encryptedFields", _encryptedFields); + info.AddValue(nameof(_encryptedFields), _encryptedFields); } } } diff --git a/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs b/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs index 36dbe2d63e3..9265565102f 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs @@ -172,22 +172,19 @@ public async Task CreateEncryptedCollection_should_handle_various_encryptedField using (var subject = CreateSubject()) { + var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; + if (BsonDocument.TryParse(expectedResult, out var encryptedFields)) { - var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; - var effectiveEncryptedFields = subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions); - effectiveEncryptedFields.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone()); + var createCollectionResult = subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions); + createCollectionResult.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone()); - createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; - effectiveEncryptedFields = await subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions); - effectiveEncryptedFields.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone()); + createCollectionResult = await subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions); + createCollectionResult.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone()); } else { - var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; AssertInvalidOperationException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedResult); - - createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null }; AssertInvalidOperationException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedResult); } } From e7b9fc1a66b61d4043208276d010f9e93ffd69f1 Mon Sep 17 00:00:00 2001 From: DmitryLukyanov Date: Tue, 17 Jan 2023 01:06:55 +0400 Subject: [PATCH 5/6] Code review. --- src/MongoDB.Driver/CreateCollectionOptions.cs | 94 +++++++++---------- .../Encryption/ClientEncryptionTests.cs | 1 - 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/src/MongoDB.Driver/CreateCollectionOptions.cs b/src/MongoDB.Driver/CreateCollectionOptions.cs index e2233deb3ed..7a79c9cd3b7 100644 --- a/src/MongoDB.Driver/CreateCollectionOptions.cs +++ b/src/MongoDB.Driver/CreateCollectionOptions.cs @@ -197,28 +197,26 @@ public DocumentValidationLevel? ValidationLevel set { _validationLevel = value; } } - internal virtual CreateCollectionOptions Clone() - { - var clone = new CreateCollectionOptions(); - clone._autoIndexId = _autoIndexId; - clone._capped = _capped; - clone._changeStreamPreAndPostImagesOptions = _changeStreamPreAndPostImagesOptions; - clone._collation = _collation; - clone._encryptedFields = _encryptedFields; - clone._expireAfter = _expireAfter; - clone._indexOptionDefaults = _indexOptionDefaults; - clone._maxDocuments = _maxDocuments; - clone._maxSize = _maxSize; - clone._noPadding = _noPadding; - clone._serializerRegistry = _serializerRegistry; - clone._storageEngine = _storageEngine; - clone._timeSeriesOptions = _timeSeriesOptions; - clone._usePowerOf2Sizes = _usePowerOf2Sizes; - clone._validationAction = _validationAction; - clone._validationLevel = _validationLevel; - - return clone; - } + internal virtual CreateCollectionOptions Clone() => + new CreateCollectionOptions + { + _autoIndexId = _autoIndexId, + _capped = _capped, + _changeStreamPreAndPostImagesOptions = _changeStreamPreAndPostImagesOptions, + _collation = _collation, + _encryptedFields = _encryptedFields, + _expireAfter = _expireAfter, + _indexOptionDefaults = _indexOptionDefaults, + _maxDocuments = _maxDocuments, + _maxSize = _maxSize, + _noPadding = _noPadding, + _serializerRegistry = _serializerRegistry, + _storageEngine = _storageEngine, + _timeSeriesOptions = _timeSeriesOptions, + _usePowerOf2Sizes = _usePowerOf2Sizes, + _validationAction = _validationAction, + _validationLevel = _validationLevel + }; } /// @@ -306,33 +304,31 @@ public FilterDefinition Validator set { _validator = value; } } - internal override CreateCollectionOptions Clone() - { - var clone = new CreateCollectionOptions(); -#pragma warning disable CS0618 // Type or member is obsolete - clone.AutoIndexId = base.AutoIndexId; -#pragma warning restore CS0618 // Type or member is obsolete - clone.Capped = base.Capped; - clone.ChangeStreamPreAndPostImagesOptions = base.ChangeStreamPreAndPostImagesOptions; - clone.Collation = base.Collation; - clone.EncryptedFields = base.EncryptedFields; - clone.ExpireAfter = base.ExpireAfter; - clone.IndexOptionDefaults = base.IndexOptionDefaults; - clone.MaxDocuments = base.MaxDocuments; - clone.MaxSize = base.MaxSize; - clone.NoPadding = base.NoPadding; - clone.SerializerRegistry = base.SerializerRegistry; - clone.StorageEngine = base.StorageEngine; - clone.TimeSeriesOptions = base.TimeSeriesOptions; - clone.UsePowerOf2Sizes = base.UsePowerOf2Sizes; - clone.ValidationAction = base.ValidationAction; - clone.ValidationLevel = base.ValidationLevel; - - clone._clusteredIndex = _clusteredIndex; - clone._documentSerializer = _documentSerializer; - clone._validator = _validator; + internal override CreateCollectionOptions Clone() => + new CreateCollectionOptions + { + #pragma warning disable CS0618 // Type or member is obsolete + AutoIndexId = base.AutoIndexId, + #pragma warning restore CS0618 // Type or member is obsolete + Capped = base.Capped, + ChangeStreamPreAndPostImagesOptions = base.ChangeStreamPreAndPostImagesOptions, + Collation = base.Collation, + EncryptedFields = base.EncryptedFields, + ExpireAfter = base.ExpireAfter, + IndexOptionDefaults = base.IndexOptionDefaults, + MaxDocuments = base.MaxDocuments, + MaxSize = base.MaxSize, + NoPadding = base.NoPadding, + SerializerRegistry = base.SerializerRegistry, + StorageEngine = base.StorageEngine, + TimeSeriesOptions = base.TimeSeriesOptions, + UsePowerOf2Sizes = base.UsePowerOf2Sizes, + ValidationAction = base.ValidationAction, + ValidationLevel = base.ValidationLevel, - return clone; - } + _clusteredIndex = _clusteredIndex, + _documentSerializer = _documentSerializer, + _validator = _validator + }; } } diff --git a/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs b/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs index 9265565102f..fe3170485bf 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs @@ -126,7 +126,6 @@ public async Task CreateEncryptedCollection_should_handle_save_generated_key_whe var exception = Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)); AssertResults(exception, createCollectionOptions); - createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = BsonDocument.Parse(encryptedFieldsStr) }; exception = await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)); AssertResults(exception, createCollectionOptions); } From 1e78cdbe35b543c0bb39148f72e1039b3019f6bd Mon Sep 17 00:00:00 2001 From: DmitryLukyanov Date: Tue, 17 Jan 2023 03:16:09 +0400 Subject: [PATCH 6/6] Fixing tests. --- .../prose-tests/ClientEncryptionProseTests.cs | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs b/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs index e5c4ecad392..ab254d459ae 100644 --- a/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs +++ b/tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs @@ -119,7 +119,7 @@ void RunTestCase(int testCase) { case 1: // Case 1: Simple Creation and Validation { - var collection = CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, encryptedFields, kmsProvider, async); + var collection = CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, encryptedFields, kmsProvider, async, out _); var exception = Record.Exception(() => Insert(collection, async, new BsonDocument("ssn", "123-45-6789"))); exception.Should().BeOfType>().Which.Message.Should().Contain("Document failed validation"); @@ -127,24 +127,28 @@ void RunTestCase(int testCase) break; case 2: // Case 2: Missing ``encryptedFields`` { - var exception = Record.Exception(() => CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, encryptedFields: null, kmsProvider, async)); + var exception = Record.Exception(() => CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, encryptedFields: null, kmsProvider, async, out _)); - exception.Should().BeOfType().Which.Message.Should().Contain("There are no encrypted fields defined for the collection.") ; + exception + .Should().BeOfType().Which.InnerException + .Should().BeOfType().Which.Message.Should().Contain("There are no encrypted fields defined for the collection.") ; } break; case 3: // Case 3: Invalid ``keyId`` { var effectiveEncryptedFields = encryptedFields.DeepClone(); effectiveEncryptedFields["fields"].AsBsonArray[0].AsBsonDocument["keyId"] = false; - var exception = Record.Exception(() => CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, effectiveEncryptedFields.AsBsonDocument, kmsProvider, async)); - exception.Should().BeOfType().Which.Message.Should().Contain("BSON field 'create.encryptedFields.fields.keyId' is the wrong type 'bool', expected type 'binData'"); + var exception = Record.Exception(() => CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, effectiveEncryptedFields.AsBsonDocument, kmsProvider, async, out _)); + exception + .Should().BeOfType().Which.InnerException + .Should().BeOfType().Which.Message.Should().Contain("BSON field 'create.encryptedFields.fields.keyId' is the wrong type 'bool', expected type 'binData'"); } break; case 4: // Case 4: Insert encrypted value { var createCollectionOptions = new CreateCollectionOptions { EncryptedFields = encryptedFields }; - var collection = CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, createCollectionOptions, kmsProvider, async); - var dataKey = createCollectionOptions.EncryptedFields["fields"].AsBsonArray[0].AsBsonDocument["keyId"].AsGuid; // get generated datakey + var collection = CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, createCollectionOptions, kmsProvider, async, out var effectiveEncryptedFields); + var dataKey = effectiveEncryptedFields["fields"].AsBsonArray[0].AsBsonDocument["keyId"].AsGuid; // get generated datakey var encryptedValue = ExplicitEncrypt(clientEncryption, new EncryptOptions(algorithm: EncryptionAlgorithm.Unindexed, keyId: dataKey), "123-45-6789", async); // use explicit encryption to encrypt data before inserting Insert(collection, async, new BsonDocument("ssn", encryptedValue)); } @@ -2330,25 +2334,23 @@ private void CreateCollection(IMongoClient client, CollectionNamespace collectio }); } - private IMongoCollection CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, BsonDocument encryptedFields, string kmsProvider, bool async) + private IMongoCollection CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, BsonDocument encryptedFields, string kmsProvider, bool async, out BsonDocument effectiveEncryptedFields) { var createCollectionOptions = new CreateCollectionOptions { EncryptedFields = encryptedFields }; - return CreateEncryptedCollection(client, clientEncryption, collectionNamespace, createCollectionOptions, kmsProvider, async); + return CreateEncryptedCollection(client, clientEncryption, collectionNamespace, createCollectionOptions, kmsProvider, async, out effectiveEncryptedFields); } - private IMongoCollection CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, bool async) + private IMongoCollection CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, bool async, out BsonDocument effectiveEncryptedFields) { var datakeyOptions = CreateDataKeyOptions(kmsProvider); var database = client.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName); - if (async) - { - _ = clientEncryption.CreateEncryptedCollectionAsync(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default).GetAwaiter().GetResult(); - } - else - { - _ = clientEncryption.CreateEncryptedCollection(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default); - } + + var result = async + ? clientEncryption.CreateEncryptedCollectionAsync(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default).GetAwaiter().GetResult() + : clientEncryption.CreateEncryptedCollection(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default); + + effectiveEncryptedFields = result.EncryptedFields; return client.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName).GetCollection(collectionNamespace.CollectionName); }