Skip to content

Commit c7c1510

Browse files
CSHARP-4149: Add FLE 2 behavior for CreateCollection() and Collection.Drop(). (#790)
CSHARP-4149: Add FLE 2 behavior for CreateCollection() and Collection.Drop().
1 parent 50f6cb8 commit c7c1510

24 files changed

+4477
-977
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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.Collections.Generic;
18+
using System.Linq;
19+
using MongoDB.Bson;
20+
21+
namespace MongoDB.Driver.Encryption
22+
{
23+
internal static class EncryptedCollectionHelper
24+
{
25+
public static BsonDocument AdditionalCreateIndexDocument { get; } = new BsonDocument("__safeContent__", 1);
26+
27+
public static void EnsureCollectionsValid(IReadOnlyDictionary<string, BsonDocument> schemaMap, IReadOnlyDictionary<string, BsonDocument> encryptedFieldsMap)
28+
{
29+
if (schemaMap == null || encryptedFieldsMap == null || schemaMap.Count == 0 || encryptedFieldsMap.Count == 0)
30+
{
31+
return;
32+
}
33+
34+
var mutualKeys = schemaMap.Keys.Where(k => encryptedFieldsMap.ContainsKey(k));
35+
if (mutualKeys.Any())
36+
{
37+
throw new ArgumentException($"SchemaMap and EncryptedFieldsMap cannot both contain the same collections: {string.Join(", ", mutualKeys)}.");
38+
}
39+
}
40+
41+
public static string GetAdditionalCollectionName(BsonDocument encryptedFields, CollectionNamespace mainCollectionNamespace, HelperCollectionForEncryption helperCollection) =>
42+
helperCollection switch
43+
{
44+
HelperCollectionForEncryption.Esc => encryptedFields.GetValue("escCollection", defaultValue: $"enxcol_.{mainCollectionNamespace.CollectionName}.esc").ToString(),
45+
HelperCollectionForEncryption.Ecc => encryptedFields.GetValue("eccCollection", defaultValue: $"enxcol_.{mainCollectionNamespace.CollectionName}.ecc").ToString(),
46+
HelperCollectionForEncryption.Ecos => encryptedFields.GetValue("ecocCollection", defaultValue: $"enxcol_.{mainCollectionNamespace.CollectionName}.ecoc").ToString(),
47+
_ => throw new InvalidOperationException($"Not supported encryption helper collection {helperCollection}."),
48+
};
49+
50+
public static bool TryGetEffectiveEncryptedFields(CollectionNamespace collectionNamespace, BsonDocument encryptedFields, IReadOnlyDictionary<string, BsonDocument> encryptedFieldsMap, out BsonDocument effectiveEncryptedFields)
51+
{
52+
if (encryptedFields != null)
53+
{
54+
effectiveEncryptedFields = encryptedFields;
55+
return true;
56+
}
57+
58+
if (encryptedFieldsMap != null)
59+
{
60+
return encryptedFieldsMap.TryGetValue(collectionNamespace.ToString(), out effectiveEncryptedFields);
61+
}
62+
63+
effectiveEncryptedFields = null;
64+
return false;
65+
}
66+
67+
public static BsonDocument GetEffectiveEncryptedFields(CollectionNamespace collectionNamespace, BsonDocument encryptedFields, IReadOnlyDictionary<string, BsonDocument> encryptedFieldsMap)
68+
{
69+
if (TryGetEffectiveEncryptedFields(collectionNamespace, encryptedFields, encryptedFieldsMap, out var effectiveEncryptedFields))
70+
{
71+
return effectiveEncryptedFields;
72+
}
73+
else
74+
{
75+
return null;
76+
}
77+
}
78+
79+
public enum HelperCollectionForEncryption
80+
{
81+
Esc,
82+
Ecc,
83+
Ecos
84+
}
85+
}
86+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.Linq;
17+
using System.Threading;
18+
using System.Threading.Tasks;
19+
using MongoDB.Driver.Core.Bindings;
20+
using MongoDB.Driver.Core.Misc;
21+
22+
namespace MongoDB.Driver.Core.Operations
23+
{
24+
internal sealed class CompositeWriteOperation<TResult> : IWriteOperation<TResult>
25+
{
26+
private readonly (IWriteOperation<TResult> Operation, bool IsMainOperation)[] _operations;
27+
28+
public CompositeWriteOperation(params (IWriteOperation<TResult>, bool IsMainOperation)[] operations)
29+
{
30+
_operations = Ensure.IsNotNull(operations, nameof(operations));
31+
Ensure.IsGreaterThanZero(operations.Length, nameof(operations.Length));
32+
Ensure.That(operations.Count(o => o.IsMainOperation) == 1, message: $"{nameof(CompositeWriteOperation<TResult>)} must have a single main operation.");
33+
}
34+
35+
public TResult Execute(IWriteBinding binding, CancellationToken cancellationToken)
36+
{
37+
TResult result = default;
38+
foreach (var operationInfo in _operations)
39+
{
40+
var itemResult = operationInfo.Operation.Execute(binding, cancellationToken);
41+
if (operationInfo.IsMainOperation)
42+
{
43+
result = itemResult;
44+
}
45+
}
46+
47+
return result;
48+
}
49+
50+
public async Task<TResult> ExecuteAsync(IWriteBinding binding, CancellationToken cancellationToken)
51+
{
52+
TResult result = default;
53+
foreach (var operationInfo in _operations)
54+
{
55+
var itemResult = await operationInfo.Operation.ExecuteAsync(binding, cancellationToken).ConfigureAwait(false);
56+
if (operationInfo.IsMainOperation)
57+
{
58+
result = itemResult;
59+
}
60+
}
61+
62+
return result;
63+
}
64+
}
65+
}

src/MongoDB.Driver.Core/Core/Operations/CreateCollectionOperation.cs

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
using MongoDB.Bson;
2020
using MongoDB.Bson.Serialization.Serializers;
2121
using MongoDB.Driver.Core.Bindings;
22-
using MongoDB.Driver.Core.Connections;
2322
using MongoDB.Driver.Core.Misc;
2423
using MongoDB.Driver.Core.WireProtocol.Messages.Encoders;
24+
using MongoDB.Driver.Encryption;
25+
using static MongoDB.Driver.Encryption.EncryptedCollectionHelper;
2526

2627
namespace MongoDB.Driver.Core.Operations
2728
{
@@ -30,12 +31,48 @@ namespace MongoDB.Driver.Core.Operations
3031
/// </summary>
3132
public class CreateCollectionOperation : IWriteOperation<BsonDocument>
3233
{
34+
#region static
35+
internal static IWriteOperation<BsonDocument> CreateEncryptedCreateCollectionOperationIfConfigured(
36+
CollectionNamespace collectionNamespace,
37+
BsonDocument encryptedFields,
38+
MessageEncoderSettings messageEncoderSettings,
39+
Action<CreateCollectionOperation> createCollectionOperationConfigurator)
40+
{
41+
var mainOperation = new CreateCollectionOperation(
42+
collectionNamespace,
43+
messageEncoderSettings)
44+
{
45+
EncryptedFields = encryptedFields
46+
};
47+
48+
createCollectionOperationConfigurator?.Invoke(mainOperation);
49+
50+
if (encryptedFields != null)
51+
{
52+
return new CompositeWriteOperation<BsonDocument>(
53+
(CreateInnerCollectionOperation(EncryptedCollectionHelper.GetAdditionalCollectionName(encryptedFields, collectionNamespace, HelperCollectionForEncryption.Esc)), IsMainOperation: false),
54+
(CreateInnerCollectionOperation(EncryptedCollectionHelper.GetAdditionalCollectionName(encryptedFields, collectionNamespace, HelperCollectionForEncryption.Ecc)), IsMainOperation: false),
55+
(CreateInnerCollectionOperation(EncryptedCollectionHelper.GetAdditionalCollectionName(encryptedFields, collectionNamespace, HelperCollectionForEncryption.Ecos)), IsMainOperation: false),
56+
(mainOperation, IsMainOperation: true),
57+
(new CreateIndexesOperation(collectionNamespace, new[] { new CreateIndexRequest(EncryptedCollectionHelper.AdditionalCreateIndexDocument) }, messageEncoderSettings), IsMainOperation: false));
58+
}
59+
else
60+
{
61+
return mainOperation;
62+
}
63+
64+
CreateCollectionOperation CreateInnerCollectionOperation(string collectionName)
65+
=> new CreateCollectionOperation(new CollectionNamespace(collectionNamespace.DatabaseNamespace.DatabaseName, collectionName), messageEncoderSettings);
66+
}
67+
#endregion
68+
3369
// fields
3470
private bool? _autoIndexId;
3571
private bool? _capped;
3672
private Collation _collation;
3773
private readonly CollectionNamespace _collectionNamespace;
3874
private BsonValue _comment;
75+
private BsonDocument _encryptedFields;
3976
private TimeSpan? _expireAfter;
4077
private BsonDocument _indexOptionDefaults;
4178
private long? _maxDocuments;
@@ -125,6 +162,12 @@ public CollectionNamespace CollectionNamespace
125162
get { return _collectionNamespace; }
126163
}
127164

165+
internal BsonDocument EncryptedFields
166+
{
167+
get { return _encryptedFields; }
168+
private set { _encryptedFields = value; }
169+
}
170+
128171
/// <summary>
129172
/// Gets or sets the expiration timespan for time series collections. Used to automatically delete documents in time series collections.
130173
/// See https://www.mongodb.com/docs/manual/reference/command/create/ for supported options and https://www.mongodb.com/docs/manual/core/timeseries-collections/
@@ -282,7 +325,7 @@ public WriteConcern WriteConcern
282325
}
283326

284327
// methods
285-
internal BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescription connectionDescription)
328+
internal BsonDocument CreateCommand(ICoreSessionHandle session)
286329
{
287330
var flags = GetFlags();
288331
var writeConcern = WriteConcernHelper.GetEffectiveWriteConcern(session, _writeConcern);
@@ -303,7 +346,8 @@ internal BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescri
303346
{ "comment", _comment, _comment != null },
304347
{ "writeConcern", writeConcern, writeConcern != null },
305348
{ "expireAfterSeconds", () => _expireAfter.Value.TotalSeconds, _expireAfter.HasValue },
306-
{ "timeseries", () => _timeSeriesOptions.ToBsonDocument(), _timeSeriesOptions != null }
349+
{ "timeseries", () => _timeSeriesOptions.ToBsonDocument(), _timeSeriesOptions != null },
350+
{ "encryptedFields", _encryptedFields, _encryptedFields != null }
307351
};
308352
}
309353

@@ -337,7 +381,7 @@ public BsonDocument Execute(IWriteBinding binding, CancellationToken cancellatio
337381
using (var channel = channelSource.GetChannel(cancellationToken))
338382
using (var channelBinding = new ChannelReadWriteBinding(channelSource.Server, channel, binding.Session.Fork()))
339383
{
340-
var operation = CreateOperation(channelBinding.Session, channel.ConnectionDescription);
384+
var operation = CreateOperation(channelBinding.Session);
341385
return operation.Execute(channelBinding, cancellationToken);
342386
}
343387
}
@@ -351,14 +395,14 @@ public async Task<BsonDocument> ExecuteAsync(IWriteBinding binding, Cancellation
351395
using (var channel = await channelSource.GetChannelAsync(cancellationToken).ConfigureAwait(false))
352396
using (var channelBinding = new ChannelReadWriteBinding(channelSource.Server, channel, binding.Session.Fork()))
353397
{
354-
var operation = CreateOperation(channelBinding.Session, channel.ConnectionDescription);
398+
var operation = CreateOperation(channelBinding.Session);
355399
return await operation.ExecuteAsync(channelBinding, cancellationToken).ConfigureAwait(false);
356400
}
357401
}
358402

359-
private WriteCommandOperation<BsonDocument> CreateOperation(ICoreSessionHandle session, ConnectionDescription connectionDescription)
403+
private WriteCommandOperation<BsonDocument> CreateOperation(ICoreSessionHandle session)
360404
{
361-
var command = CreateCommand(session, connectionDescription);
405+
var command = CreateCommand(session);
362406
return new WriteCommandOperation<BsonDocument>(_collectionNamespace.DatabaseNamespace, command, BsonDocumentSerializer.Instance, _messageEncoderSettings);
363407
}
364408

src/MongoDB.Driver.Core/Core/Operations/CreateIndexesOperation.cs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
using System.Threading;
2020
using System.Threading.Tasks;
2121
using MongoDB.Bson;
22+
using MongoDB.Bson.Serialization.Serializers;
2223
using MongoDB.Driver.Core.Bindings;
24+
using MongoDB.Driver.Core.Connections;
2325
using MongoDB.Driver.Core.Events;
2426
using MongoDB.Driver.Core.Misc;
2527
using MongoDB.Driver.Core.WireProtocol.Messages.Encoders;
@@ -33,8 +35,8 @@ public class CreateIndexesOperation : IWriteOperation<BsonDocument>
3335
{
3436
// fields
3537
private readonly CollectionNamespace _collectionNamespace;
36-
private CreateIndexCommitQuorum _commitQuorum;
3738
private BsonValue _comment;
39+
private CreateIndexCommitQuorum _commitQuorum;
3840
private TimeSpan? _maxTime;
3941
private readonly MessageEncoderSettings _messageEncoderSettings;
4042
private readonly IEnumerable<CreateIndexRequest> _requests;
@@ -72,6 +74,9 @@ public CollectionNamespace CollectionNamespace
7274
/// <summary>
7375
/// Gets or sets the comment.
7476
/// </summary>
77+
/// <value>
78+
/// The comment.
79+
/// </value>
7580
public BsonValue Comment
7681
{
7782
get { return _comment; }
@@ -122,10 +127,10 @@ public WriteConcern WriteConcern
122127
}
123128

124129
/// <summary>
125-
/// Gets or sets the max time.
130+
/// Gets or sets the MaxTime.
126131
/// </summary>
127-
/// <value>
128-
/// The max time
132+
/// <value>
133+
/// The maxtime.
129134
/// </value>
130135
public TimeSpan? MaxTime
131136
{
@@ -142,7 +147,7 @@ public BsonDocument Execute(IWriteBinding binding, CancellationToken cancellatio
142147
using (var channel = channelSource.GetChannel(cancellationToken))
143148
using (var channelBinding = new ChannelReadWriteBinding(channelSource.Server, channel, binding.Session.Fork()))
144149
{
145-
var operation = CreateOperation();
150+
var operation = CreateOperation(channelBinding.Session, channel.ConnectionDescription);
146151
return operation.Execute(channelBinding, cancellationToken);
147152
}
148153
}
@@ -155,21 +160,38 @@ public async Task<BsonDocument> ExecuteAsync(IWriteBinding binding, Cancellation
155160
using (var channel = await channelSource.GetChannelAsync(cancellationToken).ConfigureAwait(false))
156161
using (var channelBinding = new ChannelReadWriteBinding(channelSource.Server, channel, binding.Session.Fork()))
157162
{
158-
var operation = CreateOperation();
163+
var operation = CreateOperation(channelBinding.Session, channel.ConnectionDescription);
159164
return await operation.ExecuteAsync(channelBinding, cancellationToken).ConfigureAwait(false);
160165
}
161166
}
162167

163-
// internal methods
164-
internal IWriteOperation<BsonDocument> CreateOperation()
168+
// private methods
169+
internal BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescription connectionDescription)
165170
{
166-
return new CreateIndexesUsingCommandOperation(_collectionNamespace, _requests, _messageEncoderSettings)
171+
var maxWireVersion = connectionDescription.MaxWireVersion;
172+
var writeConcern = WriteConcernHelper.GetEffectiveWriteConcern(session, _writeConcern);
173+
if (_commitQuorum != null)
167174
{
168-
Comment = _comment,
169-
CommitQuorum = _commitQuorum,
170-
MaxTime = _maxTime,
171-
WriteConcern = _writeConcern
175+
Feature.CreateIndexCommitQuorum.ThrowIfNotSupported(maxWireVersion);
176+
}
177+
178+
return new BsonDocument
179+
{
180+
{ "createIndexes", _collectionNamespace.CollectionName },
181+
{ "indexes", new BsonArray(_requests.Select(request => request.CreateIndexDocument())) },
182+
{ "maxTimeMS", () => MaxTimeHelper.ToMaxTimeMS(_maxTime.Value), _maxTime.HasValue },
183+
{ "writeConcern", writeConcern, writeConcern != null },
184+
{ "comment", _comment, _comment != null },
185+
{ "commitQuorum", () => _commitQuorum.ToBsonValue(), _commitQuorum != null }
172186
};
173187
}
188+
189+
private WriteCommandOperation<BsonDocument> CreateOperation(ICoreSessionHandle session, ConnectionDescription connectionDescription)
190+
{
191+
var databaseNamespace = _collectionNamespace.DatabaseNamespace;
192+
var command = CreateCommand(session, connectionDescription);
193+
var resultSerializer = BsonDocumentSerializer.Instance;
194+
return new WriteCommandOperation<BsonDocument>(databaseNamespace, command, resultSerializer, _messageEncoderSettings);
195+
}
174196
}
175197
}

0 commit comments

Comments
 (0)