Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0bb42fa

Browse files
authoredJan 17, 2023
CSHARP-4255: Fix bug and some tests. (#993)
1 parent c0c521e commit 0bb42fa

File tree

9 files changed

+425
-46
lines changed

9 files changed

+425
-46
lines changed
 

‎src/MongoDB.Driver/CreateCollectionOptions.cs‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,27 @@ public DocumentValidationLevel? ValidationLevel
196196
get { return _validationLevel; }
197197
set { _validationLevel = value; }
198198
}
199+
200+
internal virtual CreateCollectionOptions Clone() =>
201+
new CreateCollectionOptions
202+
{
203+
_autoIndexId = _autoIndexId,
204+
_capped = _capped,
205+
_changeStreamPreAndPostImagesOptions = _changeStreamPreAndPostImagesOptions,
206+
_collation = _collation,
207+
_encryptedFields = _encryptedFields,
208+
_expireAfter = _expireAfter,
209+
_indexOptionDefaults = _indexOptionDefaults,
210+
_maxDocuments = _maxDocuments,
211+
_maxSize = _maxSize,
212+
_noPadding = _noPadding,
213+
_serializerRegistry = _serializerRegistry,
214+
_storageEngine = _storageEngine,
215+
_timeSeriesOptions = _timeSeriesOptions,
216+
_usePowerOf2Sizes = _usePowerOf2Sizes,
217+
_validationAction = _validationAction,
218+
_validationLevel = _validationLevel
219+
};
199220
}
200221

201222
/// <summary>
@@ -282,5 +303,32 @@ public FilterDefinition<TDocument> Validator
282303
get { return _validator; }
283304
set { _validator = value; }
284305
}
306+
307+
internal override CreateCollectionOptions Clone() =>
308+
new CreateCollectionOptions<TDocument>
309+
{
310+
#pragma warning disable CS0618 // Type or member is obsolete
311+
AutoIndexId = base.AutoIndexId,
312+
#pragma warning restore CS0618 // Type or member is obsolete
313+
Capped = base.Capped,
314+
ChangeStreamPreAndPostImagesOptions = base.ChangeStreamPreAndPostImagesOptions,
315+
Collation = base.Collation,
316+
EncryptedFields = base.EncryptedFields,
317+
ExpireAfter = base.ExpireAfter,
318+
IndexOptionDefaults = base.IndexOptionDefaults,
319+
MaxDocuments = base.MaxDocuments,
320+
MaxSize = base.MaxSize,
321+
NoPadding = base.NoPadding,
322+
SerializerRegistry = base.SerializerRegistry,
323+
StorageEngine = base.StorageEngine,
324+
TimeSeriesOptions = base.TimeSeriesOptions,
325+
UsePowerOf2Sizes = base.UsePowerOf2Sizes,
326+
ValidationAction = base.ValidationAction,
327+
ValidationLevel = base.ValidationLevel,
328+
329+
_clusteredIndex = _clusteredIndex,
330+
_documentSerializer = _documentSerializer,
331+
_validator = _validator
332+
};
285333
}
286334
}

‎src/MongoDB.Driver/Encryption/ClientEncryption.cs‎

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,59 +82,85 @@ public Task<BsonDocument> AddAlternateKeyNameAsync(Guid id, string alternateKeyN
8282
/// <summary>
8383
/// Create encrypted collection.
8484
/// </summary>
85-
/// <param name="collectionNamespace">The collection namespace.</param>
85+
/// <param name="database">The database.</param>
86+
/// <param name="collectionName">The collection name.</param>
8687
/// <param name="createCollectionOptions">The create collection options.</param>
8788
/// <param name="kmsProvider">The kms provider.</param>
8889
/// <param name="dataKeyOptions">The datakey options.</param>
8990
/// <param name="cancellationToken">The cancellation token.</param>
91+
/// <returns>The operation result.</returns>
9092
/// <remarks>
9193
/// if EncryptionFields contains a keyId with a null value, a data key will be automatically generated and assigned to keyId value.
9294
/// </remarks>
93-
public void CreateEncryptedCollection<TCollection>(CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default)
95+
public CreateEncryptedCollectionResult CreateEncryptedCollection(IMongoDatabase database, string collectionName, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default)
9496
{
95-
Ensure.IsNotNull(collectionNamespace, nameof(collectionNamespace));
97+
Ensure.IsNotNull(database, nameof(database));
98+
Ensure.IsNotNull(collectionName, nameof(collectionName));
9699
Ensure.IsNotNull(createCollectionOptions, nameof(createCollectionOptions));
97100
Ensure.IsNotNull(dataKeyOptions, nameof(dataKeyOptions));
98101
Ensure.IsNotNull(kmsProvider, nameof(kmsProvider));
99102

100-
foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(collectionNamespace, createCollectionOptions.EncryptedFields))
103+
var encryptedFields = createCollectionOptions.EncryptedFields?.DeepClone()?.AsBsonDocument;
104+
try
101105
{
102-
var dataKey = CreateDataKey(kmsProvider, dataKeyOptions, cancellationToken);
103-
EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey);
106+
foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(new CollectionNamespace(database.DatabaseNamespace.DatabaseName, collectionName), encryptedFields))
107+
{
108+
var dataKey = CreateDataKey(kmsProvider, dataKeyOptions, cancellationToken);
109+
EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey);
110+
}
111+
112+
var effectiveCreateEncryptionOptions = createCollectionOptions.Clone();
113+
effectiveCreateEncryptionOptions.EncryptedFields = encryptedFields;
114+
database.CreateCollection(collectionName, effectiveCreateEncryptionOptions, cancellationToken);
115+
}
116+
catch (Exception ex)
117+
{
118+
throw new MongoEncryptionCreateCollectionException(ex, encryptedFields);
104119
}
105120

106-
var database = _libMongoCryptController.KeyVaultClient.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName);
107-
108-
database.CreateCollection(collectionNamespace.CollectionName, createCollectionOptions, cancellationToken);
121+
return new CreateEncryptedCollectionResult(encryptedFields);
109122
}
110123

111124
/// <summary>
112125
/// Create encrypted collection.
113126
/// </summary>
114-
/// <param name="collectionNamespace">The collection namespace.</param>
127+
/// <param name="database">The database.</param>
128+
/// <param name="collectionName">The collection name.</param>
115129
/// <param name="createCollectionOptions">The create collection options.</param>
116130
/// <param name="kmsProvider">The kms provider.</param>
117131
/// <param name="dataKeyOptions">The datakey options.</param>
118132
/// <param name="cancellationToken">The cancellation token.</param>
133+
/// <returns>The operation result.</returns>
119134
/// <remarks>
120135
/// if EncryptionFields contains a keyId with a null value, a data key will be automatically generated and assigned to keyId value.
121136
/// </remarks>
122-
public async Task CreateEncryptedCollectionAsync<TCollection>(CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default)
137+
public async Task<CreateEncryptedCollectionResult> CreateEncryptedCollectionAsync(IMongoDatabase database, string collectionName, CreateCollectionOptions createCollectionOptions, string kmsProvider, DataKeyOptions dataKeyOptions, CancellationToken cancellationToken = default)
123138
{
124-
Ensure.IsNotNull(collectionNamespace, nameof(collectionNamespace));
139+
Ensure.IsNotNull(database, nameof(database));
140+
Ensure.IsNotNull(collectionName, nameof(collectionName));
125141
Ensure.IsNotNull(createCollectionOptions, nameof(createCollectionOptions));
126142
Ensure.IsNotNull(dataKeyOptions, nameof(dataKeyOptions));
127143
Ensure.IsNotNull(kmsProvider, nameof(kmsProvider));
128144

129-
foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(collectionNamespace, createCollectionOptions.EncryptedFields))
145+
var encryptedFields = createCollectionOptions.EncryptedFields?.DeepClone()?.AsBsonDocument;
146+
try
130147
{
131-
var dataKey = await CreateDataKeyAsync(kmsProvider, dataKeyOptions, cancellationToken).ConfigureAwait(false);
132-
EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey);
148+
foreach (var fieldDocument in EncryptedCollectionHelper.IterateEmptyKeyIds(new CollectionNamespace(database.DatabaseNamespace.DatabaseName, collectionName), encryptedFields))
149+
{
150+
var dataKey = await CreateDataKeyAsync(kmsProvider, dataKeyOptions, cancellationToken).ConfigureAwait(false);
151+
EncryptedCollectionHelper.ModifyEncryptedFields(fieldDocument, dataKey);
152+
}
153+
154+
var effectiveCreateEncryptionOptions = createCollectionOptions.Clone();
155+
effectiveCreateEncryptionOptions.EncryptedFields = encryptedFields;
156+
await database.CreateCollectionAsync(collectionName, effectiveCreateEncryptionOptions, cancellationToken).ConfigureAwait(false);
157+
}
158+
catch (Exception ex)
159+
{
160+
throw new MongoEncryptionCreateCollectionException(ex, encryptedFields);
133161
}
134162

135-
var database = _libMongoCryptController.KeyVaultClient.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName);
136-
137-
await database.CreateCollectionAsync(collectionNamespace.CollectionName, createCollectionOptions, cancellationToken).ConfigureAwait(false);
163+
return new CreateEncryptedCollectionResult(encryptedFields);
138164
}
139165

140166
/// <summary>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using MongoDB.Bson;
17+
18+
namespace MongoDB.Driver.Encryption
19+
{
20+
/// <summary>
21+
/// Represents the result of a create encrypted collection.
22+
/// </summary>
23+
public sealed class CreateEncryptedCollectionResult
24+
{
25+
private readonly BsonDocument _encryptedFields;
26+
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="CreateEncryptedCollectionResult"/> class.
29+
/// </summary>
30+
/// <param name="encryptedFields">The encrypted fields document.</param>
31+
public CreateEncryptedCollectionResult(BsonDocument encryptedFields) => _encryptedFields = encryptedFields;
32+
33+
/// <summary>
34+
/// The encrypted fields document.
35+
/// </summary>
36+
public BsonDocument EncryptedFields => _encryptedFields;
37+
}
38+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Runtime.Serialization;
18+
using MongoDB.Bson;
19+
20+
namespace MongoDB.Driver.Encryption
21+
{
22+
/// <summary>
23+
/// Represents an encryption exception.
24+
/// </summary>
25+
[Serializable]
26+
public class MongoEncryptionCreateCollectionException : MongoEncryptionException
27+
{
28+
private readonly BsonDocument _encryptedFields;
29+
30+
/// <summary>
31+
/// Initializes a new instance of the <see cref="MongoEncryptionException"/> class.
32+
/// </summary>
33+
/// <param name="innerException">The inner exception.</param>
34+
/// <param name="encryptedFields">The encrypted fields.</param>
35+
public MongoEncryptionCreateCollectionException(Exception innerException, BsonDocument encryptedFields)
36+
: base(innerException)
37+
{
38+
_encryptedFields = encryptedFields;
39+
}
40+
41+
/// <summary>
42+
/// Initializes a new instance of the <see cref="MongoEncryptionCreateCollectionException"/> class (this overload used by deserialization).
43+
/// </summary>
44+
/// <param name="info">The SerializationInfo.</param>
45+
/// <param name="context">The StreamingContext.</param>
46+
protected MongoEncryptionCreateCollectionException(SerializationInfo info, StreamingContext context)
47+
: base(info, context)
48+
{
49+
_encryptedFields = (BsonDocument)info.GetValue(nameof(_encryptedFields), typeof(BsonDocument));
50+
}
51+
52+
/// <summary>
53+
/// The encrypted fields.
54+
/// </summary>
55+
public BsonDocument EncryptedFields => _encryptedFields;
56+
57+
// public methods
58+
/// <summary>
59+
/// Gets the object data.
60+
/// </summary>
61+
/// <param name="info">The information.</param>
62+
/// <param name="context">The context.</param>
63+
public override void GetObjectData(SerializationInfo info, StreamingContext context)
64+
{
65+
base.GetObjectData(info, context);
66+
info.AddValue(nameof(_encryptedFields), _encryptedFields);
67+
}
68+
}
69+
}

‎src/MongoDB.Driver/Encryption/MongoEncryptionException.cs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
using System;
1717
using System.Runtime.Serialization;
18-
using MongoDB.Driver.Core.Misc;
1918

2019
namespace MongoDB.Driver.Encryption
2120
{

‎tests/MongoDB.Bson.TestHelpers/BsonValueEquivalencyComparer.cs‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* limitations under the License.
1414
*/
1515

16+
using System;
1617
using System.Collections.Generic;
1718

1819
namespace MongoDB.Bson.TestHelpers
@@ -22,15 +23,17 @@ public class BsonValueEquivalencyComparer : IEqualityComparer<BsonValue>
2223
#region static
2324
public static BsonValueEquivalencyComparer Instance { get; } = new BsonValueEquivalencyComparer();
2425

25-
public static bool Compare(BsonValue a, BsonValue b)
26+
public static bool Compare(BsonValue a, BsonValue b, Action<BsonValue, BsonValue> massageAction = null)
2627
{
28+
massageAction?.Invoke(a, b);
29+
2730
if (a.BsonType == BsonType.Document && b.BsonType == BsonType.Document)
2831
{
29-
return CompareDocuments((BsonDocument)a, (BsonDocument)b);
32+
return CompareDocuments((BsonDocument)a, (BsonDocument)b, massageAction);
3033
}
3134
else if (a.BsonType == BsonType.Array && b.BsonType == BsonType.Array)
3235
{
33-
return CompareArrays((BsonArray)a, (BsonArray)b);
36+
return CompareArrays((BsonArray)a, (BsonArray)b, massageAction);
3437
}
3538
else if (a.BsonType == b.BsonType)
3639
{
@@ -50,7 +53,7 @@ public static bool Compare(BsonValue a, BsonValue b)
5053
}
5154
}
5255

53-
private static bool CompareArrays(BsonArray a, BsonArray b)
56+
private static bool CompareArrays(BsonArray a, BsonArray b, Action<BsonValue, BsonValue> massageAction = null)
5457
{
5558
if (a.Count != b.Count)
5659
{
@@ -59,7 +62,7 @@ private static bool CompareArrays(BsonArray a, BsonArray b)
5962

6063
for (var i = 0; i < a.Count; i++)
6164
{
62-
if (!Compare(a[i], b[i]))
65+
if (!Compare(a[i], b[i], massageAction))
6366
{
6467
return false;
6568
}
@@ -68,7 +71,7 @@ private static bool CompareArrays(BsonArray a, BsonArray b)
6871
return true;
6972
}
7073

71-
private static bool CompareDocuments(BsonDocument a, BsonDocument b)
74+
private static bool CompareDocuments(BsonDocument a, BsonDocument b, Action<BsonValue, BsonValue> massageAction = null)
7275
{
7376
if (a.ElementCount != b.ElementCount)
7477
{
@@ -83,7 +86,7 @@ private static bool CompareDocuments(BsonDocument a, BsonDocument b)
8386
return false;
8487
}
8588

86-
if (!Compare(aElement.Value, bElement.Value))
89+
if (!Compare(aElement.Value, bElement.Value, massageAction))
8790
{
8891
return false;
8992
}

‎tests/MongoDB.Driver.Tests/Encryption/ClientEncryptionTests.cs‎

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
using MongoDB.Driver.Tests.Specifications.client_side_encryption;
2626
using MongoDB.Libmongocrypt;
2727
using Xunit;
28+
using Moq;
29+
using System.Collections.Generic;
30+
using System.Threading;
2831

2932
namespace MongoDB.Driver.Tests.Encryption
3033
{
@@ -64,6 +67,149 @@ public async Task CreateDataKey_should_correctly_handle_input_arguments()
6467
}
6568
}
6669

70+
[Fact]
71+
public async Task CreateEncryptedCollection_should_handle_input_arguments()
72+
{
73+
const string kmsProvider = "local";
74+
const string collectionName = "collName";
75+
var createCollectionOptions = new CreateCollectionOptions();
76+
var database = Mock.Of<IMongoDatabase>();
77+
78+
var dataKeyOptions = new DataKeyOptions();
79+
80+
using (var subject = CreateSubject())
81+
{
82+
ShouldBeArgumentException(Record.Exception(() => subject.CreateEncryptedCollection(database: null, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedParamName: "database");
83+
ShouldBeArgumentException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database: null, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedParamName: "database");
84+
85+
ShouldBeArgumentException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName: null, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedParamName: "collectionName");
86+
ShouldBeArgumentException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName: null, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedParamName: "collectionName");
87+
88+
ShouldBeArgumentException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName: collectionName, createCollectionOptions: null, kmsProvider, dataKeyOptions)), expectedParamName: "createCollectionOptions");
89+
ShouldBeArgumentException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions: null, kmsProvider, dataKeyOptions)), expectedParamName: "createCollectionOptions");
90+
91+
ShouldBeArgumentException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName: collectionName, createCollectionOptions, kmsProvider: null, dataKeyOptions)), expectedParamName: "kmsProvider");
92+
ShouldBeArgumentException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider: null, dataKeyOptions)), expectedParamName: "kmsProvider");
93+
94+
ShouldBeArgumentException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName: collectionName, createCollectionOptions, kmsProvider, dataKeyOptions: null)), expectedParamName: "dataKeyOptions");
95+
ShouldBeArgumentException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions: null)), expectedParamName: "dataKeyOptions");
96+
}
97+
}
98+
99+
[Fact]
100+
public async Task CreateEncryptedCollection_should_handle_save_generated_key_when_second_key_failed()
101+
{
102+
const string kmsProvider = "local";
103+
const string collectionName = "collName";
104+
const string encryptedFieldsStr = "{ fields : [{ keyId : null }, { keyId : null }] }";
105+
var database = Mock.Of<IMongoDatabase>(d => d.DatabaseNamespace == new DatabaseNamespace("db"));
106+
107+
var dataKeyOptions = new DataKeyOptions();
108+
109+
var mockCollection = new Mock<IMongoCollection<BsonDocument>>();
110+
mockCollection
111+
.SetupSequence(c => c.InsertOne(It.IsAny<BsonDocument>(), It.IsAny<InsertOneOptions>(), It.IsAny<CancellationToken>()))
112+
.Pass()
113+
.Throws(new Exception("test"));
114+
mockCollection
115+
.SetupSequence(c => c.InsertOneAsync(It.IsAny<BsonDocument>(), It.IsAny<InsertOneOptions>(), It.IsAny<CancellationToken>()))
116+
.Returns(Task.CompletedTask)
117+
.Throws(new Exception("test"));
118+
var mockDatabase = new Mock<IMongoDatabase>();
119+
mockDatabase.Setup(c => c.GetCollection<BsonDocument>(It.IsAny<string>(), It.IsAny<MongoCollectionSettings>())).Returns(mockCollection.Object);
120+
var client = new Mock<IMongoClient>();
121+
client.Setup(c => c.GetDatabase(It.IsAny<string>(), It.IsAny<MongoDatabaseSettings>())).Returns(mockDatabase.Object);
122+
123+
using (var subject = CreateSubject(client.Object))
124+
{
125+
var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = BsonDocument.Parse(encryptedFieldsStr) };
126+
var exception = Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions));
127+
AssertResults(exception, createCollectionOptions);
128+
129+
exception = await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions));
130+
AssertResults(exception, createCollectionOptions);
131+
}
132+
133+
void AssertResults(Exception ex, CreateCollectionOptions createCollectionOptions)
134+
{
135+
var createCollectionException = ex.Should().BeOfType<MongoEncryptionCreateCollectionException>().Subject;
136+
createCollectionException
137+
.InnerException
138+
.Should().BeOfType<MongoEncryptionException>().Subject.InnerException
139+
.Should().BeOfType<Exception>().Which.Message
140+
.Should().Be("test");
141+
var fields = createCollectionException.EncryptedFields["fields"].AsBsonArray;
142+
fields[0].AsBsonDocument["keyId"].Should().BeOfType<BsonBinaryData>(); // pass
143+
/*
144+
- If generating `D` resulted in an error `E`, the entire
145+
`CreateEncryptedCollection` must now fail with error `E`. Return the
146+
partially-formed `EF'` with the error so that the caller may know what
147+
datakeys have already been created by the helper.
148+
*/
149+
fields[1].AsBsonDocument["keyId"].Should().BeOfType<BsonNull>(); // throw
150+
}
151+
}
152+
153+
[Theory]
154+
[InlineData(null, "There are no encrypted fields defined for the collection.")]
155+
[InlineData("{}", "{}")]
156+
[InlineData("{ a : 1 }", "{ a : 1 }")]
157+
[InlineData("{ fields : { } }", "{ fields: { } }")]
158+
[InlineData("{ fields : [] }", "{ fields: [] }")]
159+
[InlineData("{ fields : [{ a : 1 }] }", "{ fields: [{ a : 1 }] }")]
160+
[InlineData("{ fields : [{ keyId : 1 }] }", "{ fields: [{ keyId : 1 }] }")]
161+
[InlineData("{ fields : [{ keyId : null }] }", "{ fields: [{ keyId : '#binary_generated#' }] }")]
162+
[InlineData("{ fields : [{ keyId : null }, { keyId : null }] }", "{ fields: [{ keyId : '#binary_generated#' }, { keyId : '#binary_generated#' }] }")]
163+
[InlineData("{ fields : [{ keyId : 3 }, { keyId : null }] }", "{ fields: [{ keyId : 3 }, { keyId : '#binary_generated#' }] }")]
164+
public async Task CreateEncryptedCollection_should_handle_various_encryptedFields(string encryptedFieldsStr, string expectedResult)
165+
{
166+
const string kmsProvider = "local";
167+
const string collectionName = "collName";
168+
var database = Mock.Of<IMongoDatabase>(d => d.DatabaseNamespace == new DatabaseNamespace("db"));
169+
170+
var dataKeyOptions = new DataKeyOptions();
171+
172+
using (var subject = CreateSubject())
173+
{
174+
var createCollectionOptions = new CreateCollectionOptions() { EncryptedFields = encryptedFieldsStr != null ? BsonDocument.Parse(encryptedFieldsStr) : null };
175+
176+
if (BsonDocument.TryParse(expectedResult, out var encryptedFields))
177+
{
178+
var createCollectionResult = subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions);
179+
createCollectionResult.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone());
180+
181+
createCollectionResult = await subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions);
182+
createCollectionResult.EncryptedFields.WithComparer(new EncryptedFieldsComparer()).Should().Be(encryptedFields.DeepClone());
183+
}
184+
else
185+
{
186+
AssertInvalidOperationException(Record.Exception(() => subject.CreateEncryptedCollection(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedResult);
187+
AssertInvalidOperationException(await Record.ExceptionAsync(() => subject.CreateEncryptedCollectionAsync(database, collectionName, createCollectionOptions, kmsProvider, dataKeyOptions)), expectedResult);
188+
}
189+
}
190+
191+
void AssertInvalidOperationException(Exception ex, string message) =>
192+
ex
193+
.Should().BeOfType<MongoEncryptionCreateCollectionException>().Subject.InnerException
194+
.Should().BeOfType<InvalidOperationException>().Which.Message.Should().Be(message);
195+
}
196+
197+
private sealed class EncryptedFieldsComparer : IEqualityComparer<BsonDocument>
198+
{
199+
public bool Equals(BsonDocument x, BsonDocument y) =>
200+
BsonValueEquivalencyComparer.Compare(
201+
x, y,
202+
massageAction: (a, b) =>
203+
{
204+
if (a is BsonDocument aDocument && aDocument.TryGetValue("keyId", out var aKeyId) && aKeyId.IsBsonBinaryData &&
205+
b is BsonDocument bDocument && bDocument.TryGetValue("keyId", out var bKeyId) && bKeyId == "#binary_generated#")
206+
{
207+
bDocument["keyId"] = aDocument["keyId"];
208+
}
209+
});
210+
211+
public int GetHashCode(BsonDocument obj) => obj.GetHashCode();
212+
}
67213

68214
[Fact]
69215
public void CryptClient_should_be_initialized()
@@ -167,10 +313,10 @@ public async Task RewrapManyDataKey_should_correctly_handle_input_arguments()
167313
}
168314

169315
// private methods
170-
private ClientEncryption CreateSubject()
316+
private ClientEncryption CreateSubject(IMongoClient client = null)
171317
{
172318
var clientEncryptionOptions = new ClientEncryptionOptions(
173-
DriverTestConfiguration.Client,
319+
client ?? DriverTestConfiguration.Client,
174320
__keyVaultCollectionNamespace,
175321
kmsProviders: EncryptionTestHelper.GetKmsProviders(filter: "local"));
176322

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.IO;
18+
using System.Runtime.Serialization.Formatters.Binary;
19+
using FluentAssertions;
20+
using MongoDB.Bson;
21+
using MongoDB.Driver.Encryption;
22+
using Xunit;
23+
24+
namespace MongoDB.Driver.Tests.Encryption
25+
{
26+
public class MongoEncryptionCreateCollectionExceptionTests
27+
{
28+
[Fact]
29+
public void Serialization_should_work()
30+
{
31+
var subject = new MongoEncryptionCreateCollectionException(new Exception("inner"), new BsonDocument("value", 1));
32+
33+
var formatter = new BinaryFormatter();
34+
using (var stream = new MemoryStream())
35+
{
36+
#pragma warning disable SYSLIB0011 // BinaryFormatter serialization is obsolete
37+
formatter.Serialize(stream, subject);
38+
stream.Position = 0;
39+
var rehydrated = (MongoEncryptionCreateCollectionException)formatter.Deserialize(stream);
40+
#pragma warning restore SYSLIB0011 // BinaryFormatter serialization is obsolete
41+
42+
rehydrated.InnerException.Message.Should().Be(subject.InnerException.Message);
43+
rehydrated.EncryptedFields.Should().Be(subject.EncryptedFields).And.Should().NotBeNull();
44+
}
45+
}
46+
}
47+
}

‎tests/MongoDB.Driver.Tests/Specifications/client-side-encryption/prose-tests/ClientEncryptionProseTests.cs‎

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -119,32 +119,36 @@ void RunTestCase(int testCase)
119119
{
120120
case 1: // Case 1: Simple Creation and Validation
121121
{
122-
var collection = CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, encryptedFields, kmsProvider, async);
122+
var collection = CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, encryptedFields, kmsProvider, async, out _);
123123

124124
var exception = Record.Exception(() => Insert(collection, async, new BsonDocument("ssn", "123-45-6789")));
125125
exception.Should().BeOfType<MongoBulkWriteException<BsonDocument>>().Which.Message.Should().Contain("Document failed validation");
126126
}
127127
break;
128128
case 2: // Case 2: Missing ``encryptedFields``
129129
{
130-
var exception = Record.Exception(() => CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, encryptedFields: null, kmsProvider, async));
130+
var exception = Record.Exception(() => CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, encryptedFields: null, kmsProvider, async, out _));
131131

132-
exception.Should().BeOfType<InvalidOperationException>().Which.Message.Should().Contain("There are no encrypted fields defined for the collection.") ;
132+
exception
133+
.Should().BeOfType<MongoEncryptionCreateCollectionException>().Which.InnerException
134+
.Should().BeOfType<InvalidOperationException>().Which.Message.Should().Contain("There are no encrypted fields defined for the collection.") ;
133135
}
134136
break;
135137
case 3: // Case 3: Invalid ``keyId``
136138
{
137139
var effectiveEncryptedFields = encryptedFields.DeepClone();
138140
effectiveEncryptedFields["fields"].AsBsonArray[0].AsBsonDocument["keyId"] = false;
139-
var exception = Record.Exception(() => CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, effectiveEncryptedFields.AsBsonDocument, kmsProvider, async));
140-
exception.Should().BeOfType<MongoCommandException>().Which.Message.Should().Contain("BSON field 'create.encryptedFields.fields.keyId' is the wrong type 'bool', expected type 'binData'");
141+
var exception = Record.Exception(() => CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, effectiveEncryptedFields.AsBsonDocument, kmsProvider, async, out _));
142+
exception
143+
.Should().BeOfType<MongoEncryptionCreateCollectionException>().Which.InnerException
144+
.Should().BeOfType<MongoCommandException>().Which.Message.Should().Contain("BSON field 'create.encryptedFields.fields.keyId' is the wrong type 'bool', expected type 'binData'");
141145
}
142146
break;
143147
case 4: // Case 4: Insert encrypted value
144148
{
145149
var createCollectionOptions = new CreateCollectionOptions { EncryptedFields = encryptedFields };
146-
var collection = CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, createCollectionOptions, kmsProvider, async);
147-
var dataKey = createCollectionOptions.EncryptedFields["fields"].AsBsonArray[0].AsBsonDocument["keyId"].AsGuid; // get generated datakey
150+
var collection = CreateEncryptedCollection(client, clientEncryption, __collCollectionNamespace, createCollectionOptions, kmsProvider, async, out var effectiveEncryptedFields);
151+
var dataKey = effectiveEncryptedFields["fields"].AsBsonArray[0].AsBsonDocument["keyId"].AsGuid; // get generated datakey
148152
var encryptedValue = ExplicitEncrypt(clientEncryption, new EncryptOptions(algorithm: EncryptionAlgorithm.Unindexed, keyId: dataKey), "123-45-6789", async); // use explicit encryption to encrypt data before inserting
149153
Insert(collection, async, new BsonDocument("ssn", encryptedValue));
150154
}
@@ -2340,24 +2344,23 @@ private void CreateCollection(IMongoClient client, CollectionNamespace collectio
23402344
});
23412345
}
23422346

2343-
private IMongoCollection<BsonDocument> CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, BsonDocument encryptedFields, string kmsProvider, bool async)
2347+
private IMongoCollection<BsonDocument> CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, BsonDocument encryptedFields, string kmsProvider, bool async, out BsonDocument effectiveEncryptedFields)
23442348
{
23452349
var createCollectionOptions = new CreateCollectionOptions { EncryptedFields = encryptedFields };
2346-
return CreateEncryptedCollection(client, clientEncryption, collectionNamespace, createCollectionOptions, kmsProvider, async);
2350+
return CreateEncryptedCollection(client, clientEncryption, collectionNamespace, createCollectionOptions, kmsProvider, async, out effectiveEncryptedFields);
23472351
}
23482352

2349-
private IMongoCollection<BsonDocument> CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, bool async)
2353+
private IMongoCollection<BsonDocument> CreateEncryptedCollection(IMongoClient client, ClientEncryption clientEncryption, CollectionNamespace collectionNamespace, CreateCollectionOptions createCollectionOptions, string kmsProvider, bool async, out BsonDocument effectiveEncryptedFields)
23502354
{
23512355
var datakeyOptions = CreateDataKeyOptions(kmsProvider);
2356+
var database = client.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName);
23522357

2353-
if (async)
2354-
{
2355-
clientEncryption.CreateEncryptedCollectionAsync<BsonDocument>(collectionNamespace, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default).GetAwaiter().GetResult();
2356-
}
2357-
else
2358-
{
2359-
clientEncryption.CreateEncryptedCollection<BsonDocument>(collectionNamespace, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default);
2360-
}
2358+
2359+
var result = async
2360+
? clientEncryption.CreateEncryptedCollectionAsync(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default).GetAwaiter().GetResult()
2361+
: clientEncryption.CreateEncryptedCollection(database, collectionNamespace.CollectionName, createCollectionOptions, kmsProvider, datakeyOptions, cancellationToken: default);
2362+
2363+
effectiveEncryptedFields = result.EncryptedFields;
23612364

23622365
return client.GetDatabase(collectionNamespace.DatabaseNamespace.DatabaseName).GetCollection<BsonDocument>(collectionNamespace.CollectionName);
23632366
}

0 commit comments

Comments
 (0)
Please sign in to comment.