Browse Source

Merge d8e20336b3 into 23dc1e56c2

pull/1631/merge
Ferdinando Papale 5 days ago
committed by GitHub
parent
commit
2ef1e5541e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 57
      src/MongoDB.Bson/ObjectModel/BsonTypeExtensions.cs
  2. 344
      src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs
  3. 29
      src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstEnumExtensions.cs
  4. 33
      src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs
  5. 710
      tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs
  6. 1
      tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs
  7. 1
      tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionExceptionTests.cs

57
src/MongoDB.Bson/ObjectModel/BsonTypeExtensions.cs

@ -0,0 +1,57 @@
/* 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;
namespace MongoDB.Bson
{
/// <summary>
/// A static class containing extension methods for <see cref="BsonType"/>.
/// </summary>
public static class BsonTypeExtensions
{
/// <summary>
/// Maps a <see cref="BsonType"/> to its corresponding server string representation.
/// </summary>
/// <param name="type">The input type to map.</param>
public static string ToServerString(this BsonType type)
{
return type switch
{
BsonType.Array => "array",
BsonType.Binary => "binData",
BsonType.Boolean => "bool",
BsonType.DateTime => "date",
BsonType.Decimal128 => "decimal",
BsonType.Document => "object",
BsonType.Double => "double",
BsonType.Int32 => "int",
BsonType.Int64 => "long",
BsonType.JavaScript => "javascript",
BsonType.JavaScriptWithScope => "javascriptWithScope",
BsonType.MaxKey => "maxKey",
BsonType.MinKey => "minKey",
BsonType.Null => "null",
BsonType.ObjectId => "objectId",
BsonType.RegularExpression => "regex",
BsonType.String => "string",
BsonType.Symbol => "symbol",
BsonType.Timestamp => "timestamp",
BsonType.Undefined => "undefined",
_ => throw new ArgumentException($"Unexpected BSON type: {type}.", nameof(type))
};
}
}
}

344
src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs

@ -0,0 +1,344 @@
/* 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.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
namespace MongoDB.Driver.Encryption
{
/// <summary>
/// A builder class for creating Client-Side Field Level Encryption (CSFLE) schemas.
/// </summary>
public class CsfleSchemaBuilder
{
private readonly Dictionary<string, BsonDocument> _schemas = new();
private CsfleSchemaBuilder()
{
}
/// <summary>
/// Creates a new instance of the <see cref="CsfleSchemaBuilder"/> and configures it using the provided action.
/// </summary>
/// <param name="configure">An action to configure the schema builder.</param>
public static CsfleSchemaBuilder Create(Action<CsfleSchemaBuilder> configure)
{
var builder = new CsfleSchemaBuilder();
configure(builder);
return builder;
}
/// <summary>
/// Adds an encrypted collection schema for a specific collection namespace.
/// </summary>
/// <typeparam name="T">The type of the document in the collection.</typeparam>
/// <param name="collectionNamespace">The namespace of the collection.</param>
/// <param name="configure">An action to configure the encrypted collection builder.</param>
/// <returns>The current <see cref="CsfleSchemaBuilder"/> instance.</returns>
public CsfleSchemaBuilder Encrypt<T>(CollectionNamespace collectionNamespace, Action<EncryptedCollectionBuilder<T>> configure)
{
var builder = new EncryptedCollectionBuilder<T>();
configure(builder);
_schemas.Add(collectionNamespace.FullName, builder.Build());
return this;
}
/// <summary>
/// Builds and returns the resulting CSFLE schema.
/// </summary>
public Dictionary<string, BsonDocument> Build()
{
if (!_schemas.Any())
{
throw new InvalidOperationException("No schemas were added. Use Encrypt<T> to add a schema.");
}
return _schemas;
}
}
/// <summary>
/// A builder class for creating encrypted collection schemas.
/// </summary>
/// <typeparam name="TDocument">The type of the document in the collection.</typeparam>
public class EncryptedCollectionBuilder<TDocument>
{
private readonly BsonDocument _schema = new("bsonType", "object");
private readonly RenderArgs<TDocument> _args = new(BsonSerializer.LookupSerializer<TDocument>(), BsonSerializer.SerializerRegistry);
internal EncryptedCollectionBuilder()
{
}
/// <summary>
/// Configures encryption metadata for the collection.
/// </summary>
/// <param name="keyId">The key ID to use for encryption.</param>
/// <param name="algorithm">The encryption algorithm to use.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> EncryptMetadata(Guid? keyId = null, EncryptionAlgorithm? algorithm = null)
{
if (keyId is null && algorithm is null)
{
throw new ArgumentException("At least one of keyId or algorithm must be specified.");
}
_schema["encryptMetadata"] = new BsonDocument
{
{ "keyId", () => new BsonArray { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }, keyId is not null },
{ "algorithm", () => MapCsfleEncryptionAlgorithmToString(algorithm!.Value), algorithm is not null }
};
return this;
}
/// <summary>
/// Adds a pattern property to the schema with encryption settings.
/// </summary>
/// <param name="pattern">The regex pattern for the property.</param>
/// <param name="bsonType">The BSON type of the property.</param>
/// <param name="algorithm">The encryption algorithm to use.</param>
/// <param name="keyId">The key ID to use for encryption.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> PatternProperty(
string pattern,
BsonType bsonType,
EncryptionAlgorithm? algorithm = null,
Guid? keyId = null)
=> PatternProperty(pattern, [bsonType], algorithm, keyId);
/// <summary>
/// Adds a pattern property to the schema with encryption settings.
/// </summary>
/// <param name="pattern">The regex pattern for the property.</param>
/// <param name="bsonTypes">The BSON types of the property.</param>
/// <param name="algorithm">The encryption algorithm to use.</param>
/// <param name="keyId">The key ID to use for encryption.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> PatternProperty(
string pattern,
IEnumerable<BsonType> bsonTypes = null,
EncryptionAlgorithm? algorithm = null,
Guid? keyId = null)
{
AddToPatternProperties(pattern, CreateEncryptDocument(bsonTypes, algorithm, keyId));
return this;
}
/// <summary>
/// Adds a nested pattern property to the schema.
/// </summary>
/// <typeparam name="TField">The type of the nested field.</typeparam>
/// <param name="path">The field.</param>
/// <param name="configure">An action to configure the nested builder.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> PatternProperty<TField>(
Expression<Func<TDocument, TField>> path,
Action<EncryptedCollectionBuilder<TField>> configure)
=> PatternProperty(new ExpressionFieldDefinition<TDocument, TField>(path), configure);
/// <summary>
/// Adds a nested pattern property to the schema.
/// </summary>
/// <typeparam name="TField">The type of the nested field.</typeparam>
/// <param name="path">The field.</param>
/// <param name="configure">An action to configure the nested builder.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> PatternProperty<TField>(
FieldDefinition<TDocument> path,
Action<EncryptedCollectionBuilder<TField>> configure)
{
var nestedBuilder = new EncryptedCollectionBuilder<TField>();
configure(nestedBuilder);
var fieldName = path.Render(_args).FieldName;
AddToPatternProperties(fieldName, nestedBuilder.Build());
return this;
}
/// <summary>
/// Adds a property to the schema with encryption settings.
/// </summary>
/// <typeparam name="TField">The type of the field.</typeparam>
/// <param name="path">The field.</param>
/// <param name="bsonType">The BSON type of the property.</param>
/// <param name="algorithm">The encryption algorithm to use.</param>
/// <param name="keyId">The key ID to use for encryption.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> Property<TField>(
Expression<Func<TDocument, TField>> path,
BsonType bsonType,
EncryptionAlgorithm? algorithm = null,
Guid? keyId = null)
=> Property(path, [bsonType], algorithm, keyId);
/// <summary>
/// Adds a property to the schema with encryption settings.
/// </summary>
/// <typeparam name="TField">The type of the field.</typeparam>
/// <param name="path">The field.</param>
/// <param name="bsonTypes">The BSON types of the property.</param>
/// <param name="algorithm">The encryption algorithm to use.</param>
/// <param name="keyId">The key ID to use for encryption.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> Property<TField>(
Expression<Func<TDocument, TField>> path,
IEnumerable<BsonType> bsonTypes = null,
EncryptionAlgorithm? algorithm = null,
Guid? keyId = null)
=> Property(new ExpressionFieldDefinition<TDocument, TField>(path), bsonTypes, algorithm, keyId);
/// <summary>
/// Adds a property to the schema with encryption settings.
/// </summary>
/// <param name="path">The field.</param>
/// <param name="bsonType">The BSON type of the property.</param>
/// <param name="algorithm">The encryption algorithm to use.</param>
/// <param name="keyId">The key ID to use for encryption.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> Property(
FieldDefinition<TDocument> path,
BsonType bsonType,
EncryptionAlgorithm? algorithm = null,
Guid? keyId = null)
=> Property(path, [bsonType], algorithm, keyId);
/// <summary>
/// Adds a property to the schema with encryption settings.
/// </summary>
/// <param name="path">The field.</param>
/// <param name="bsonTypes">The BSON types of the property.</param>
/// <param name="algorithm">The encryption algorithm to use.</param>
/// <param name="keyId">The key ID to use for encryption.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> Property(
FieldDefinition<TDocument> path,
IEnumerable<BsonType> bsonTypes = null,
EncryptionAlgorithm? algorithm = null,
Guid? keyId = null)
{
var fieldName = path.Render(_args).FieldName;
AddToProperties(fieldName, CreateEncryptDocument(bsonTypes, algorithm, keyId));
return this;
}
/// <summary>
/// Adds a nested property to the schema.
/// </summary>
/// <typeparam name="TField">The type of the nested field.</typeparam>
/// <param name="path">The field.</param>
/// <param name="configure">An action to configure the nested builder.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> Property<TField>(
Expression<Func<TDocument, TField>> path,
Action<EncryptedCollectionBuilder<TField>> configure)
=> Property(new ExpressionFieldDefinition<TDocument, TField>(path), configure);
/// <summary>
/// Adds a nested property to the schema.
/// </summary>
/// <typeparam name="TField">The type of the nested field.</typeparam>
/// <param name="path">The field.</param>
/// <param name="configure">An action to configure the nested builder.</param>
/// <returns>The current <see cref="EncryptedCollectionBuilder{TDocument}"/> instance.</returns>
public EncryptedCollectionBuilder<TDocument> Property<TField>(
FieldDefinition<TDocument> path,
Action<EncryptedCollectionBuilder<TField>> configure)
{
var nestedBuilder = new EncryptedCollectionBuilder<TField>();
configure(nestedBuilder);
var fieldName = path.Render(_args).FieldName;
AddToProperties(fieldName, nestedBuilder.Build());
return this;
}
internal BsonDocument Build() => _schema;
private static BsonDocument CreateEncryptDocument(
IEnumerable<BsonType> bsonTypes = null,
EncryptionAlgorithm? algorithm = null,
Guid? keyId = null)
{
BsonValue bsonTypeVal = null;
if (bsonTypes != null)
{
var convertedBsonTypes = bsonTypes.Select(type => type.ToServerString()).ToList();
if (convertedBsonTypes.Count == 0)
{
throw new ArgumentException("At least one BSON type must be specified.", nameof(bsonTypes));
}
bsonTypeVal = convertedBsonTypes.Count == 1
? convertedBsonTypes[0]
: new BsonArray(convertedBsonTypes);
}
return new BsonDocument
{
{ "encrypt", new BsonDocument
{
{ "bsonType", () => bsonTypeVal, bsonTypeVal is not null },
{ "algorithm", () => MapCsfleEncryptionAlgorithmToString(algorithm!.Value), algorithm is not null },
{
"keyId",
() => new BsonArray(new[] { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }),
keyId is not null
},
}
}
};
}
private void AddToPatternProperties(string field, BsonDocument document)
{
if (!_schema.TryGetValue("patternProperties", out var value))
{
value = new BsonDocument();
_schema["patternProperties"] = value;
}
var patternProperties = value.AsBsonDocument;
patternProperties[field] = document;
}
private void AddToProperties(string field, BsonDocument document)
{
if (!_schema.TryGetValue("properties", out var value))
{
value = new BsonDocument();
_schema["properties"] = value;
}
var properties = value.AsBsonDocument;
properties[field] = document;
}
private static string MapCsfleEncryptionAlgorithmToString(EncryptionAlgorithm algorithm)
{
return algorithm switch
{
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random => "AEAD_AES_256_CBC_HMAC_SHA_512-Random",
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic => "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
_ => throw new ArgumentException($"Unexpected algorithm type: {algorithm}.", nameof(algorithm))
};
}
}
}

29
src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstEnumExtensions.cs

@ -15,38 +15,13 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
namespace MongoDB.Driver.Linq.Linq3Implementation.Ast
{
internal static class AstEnumExtensions
{
public static string Render(this BsonType type)
{
return type switch
{
BsonType.Array => "array",
BsonType.Binary => "binData",
BsonType.Boolean => "bool",
BsonType.DateTime => "date",
BsonType.Decimal128 => "decimal",
BsonType.Document => "object",
BsonType.Double => "double",
BsonType.Int32 => "int",
BsonType.Int64 => "long",
BsonType.JavaScript => "javascript",
BsonType.JavaScriptWithScope => "javascriptWithScope",
BsonType.MaxKey => "maxKey",
BsonType.MinKey => "minKey",
BsonType.Null => "null",
BsonType.ObjectId => "objectId",
BsonType.RegularExpression => "regex",
BsonType.String => "string",
BsonType.Symbol => "symbol",
BsonType.Timestamp => "timestamp",
BsonType.Undefined => "undefined",
_ => throw new ArgumentException($"Unexpected BSON type: {type}.", nameof(type))
};
}
public static string Render(this BsonType type) => type.ToServerString();
public static string Render(this ByteOrder byteOrder)
{

33
src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs

@ -17,6 +17,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver.Core.Misc;
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors;
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
@ -51,39 +52,11 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters
if (_types.Count == 1)
{
var type = _types[0];
return new BsonDocument("$type", MapBsonTypeToString(type));
return new BsonDocument("$type", type.ToServerString());
}
else
{
return new BsonDocument("$type", new BsonArray(_types.Select(type => MapBsonTypeToString(type))));
}
}
private string MapBsonTypeToString(BsonType type)
{
switch (type)
{
case BsonType.Array: return "array";
case BsonType.Binary: return "binData";
case BsonType.Boolean: return "bool";
case BsonType.DateTime: return "date";
case BsonType.Decimal128: return "decimal";
case BsonType.Document: return "object";
case BsonType.Double: return "double";
case BsonType.Int32: return "int";
case BsonType.Int64: return "long";
case BsonType.JavaScript: return "javascript";
case BsonType.JavaScriptWithScope: return "javascriptWithScope";
case BsonType.MaxKey: return "maxKey";
case BsonType.MinKey: return "minKey";
case BsonType.Null: return "null";
case BsonType.ObjectId: return "objectId";
case BsonType.RegularExpression: return "regex";
case BsonType.String: return "string";
case BsonType.Symbol: return "symbol";
case BsonType.Timestamp: return "timestamp";
case BsonType.Undefined: return "undefined";
default: throw new ArgumentException($"Unexpected BSON type: {type}.", nameof(type));
return new BsonDocument("$type", new BsonArray(_types.Select(type => type.ToServerString())));
}
}
}

710
tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs

@ -0,0 +1,710 @@
/* 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.Collections.Generic;
using FluentAssertions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver.Encryption;
using Xunit;
namespace MongoDB.Driver.Tests.Encryption
{
public class CsfleSchemaBuilderTests
{
private readonly CollectionNamespace _collectionNamespace = CollectionNamespace.FromFullName("medicalRecords.patients");
private const string _keyIdString = "6f4af470-00d1-401f-ac39-f45902a0c0c8";
private static Guid _keyId = Guid.Parse(_keyIdString);
[Fact]
public void CsfleSchemaBuilder_works_as_expected()
{
var builder = CsfleSchemaBuilder.Create(schemaBuilder =>
{
schemaBuilder.Encrypt<Patient>(_collectionNamespace, builder =>
{
builder
.EncryptMetadata(keyId: _keyId)
.Property(p => p.MedicalRecords, BsonType.Array,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random)
.Property("bloodType", BsonType.String,
algorithm: EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random)
.Property(p => p.Ssn, BsonType.Int32,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
.Property(p => p.Insurance, innerBuilder =>
{
innerBuilder
.Property(i => i.PolicyNumber, BsonType.Int32,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic);
})
.PatternProperty("_PIIString$", BsonType.String, EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
.PatternProperty("_PIIArray$", BsonType.Array, EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random)
.PatternProperty(p => p.Insurance, innerBuilder =>
{
innerBuilder
.PatternProperty("_PIIString$", BsonType.String,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
.PatternProperty("_PIINumber$", BsonType.Int32,
algorithm: EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic);
});
} );
});
var expected = new Dictionary<string, string>
{
[_collectionNamespace.FullName] = """
{
"bsonType": "object",
"encryptMetadata": {
"keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
},
"properties": {
"insurance": {
"bsonType": "object",
"properties": {
"policyNumber": {
"encrypt": {
"bsonType": "int",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
}
}
}
},
"medicalRecords": {
"encrypt": {
"bsonType": "array",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
}
},
"bloodType": {
"encrypt": {
"bsonType": "string",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
}
},
"ssn": {
"encrypt": {
"bsonType": "int",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
}
}
},
"patternProperties": {
"_PIIString$": {
"encrypt": {
"bsonType": "string",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
},
},
"_PIIArray$": {
"encrypt": {
"bsonType": "array",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random",
},
},
"insurance": {
"bsonType": "object",
"patternProperties": {
"_PIINumber$": {
"encrypt": {
"bsonType": "int",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
},
},
"_PIIString$": {
"encrypt": {
"bsonType": "string",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
},
},
},
},
},
}
"""
};
AssertOutcomeCsfleSchemaBuilder(builder, expected);
}
[Fact]
public void CsfleSchemaBuilder_with_multiple_types_works_as_expected()
{
var testCollectionNamespace = CollectionNamespace.FromFullName("test.class");
var builder = CsfleSchemaBuilder.Create(schemaBuilder =>
{
schemaBuilder.Encrypt<Patient>(_collectionNamespace, builder =>
{
builder
.EncryptMetadata(keyId: _keyId)
.Property(p => p.MedicalRecords, BsonType.Array,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
});
schemaBuilder.Encrypt<TestClass>(testCollectionNamespace, builder =>
{
builder.Property(t => t.TestString, BsonType.String);
});
});
var expected = new Dictionary<string, string>
{
[_collectionNamespace.FullName] = """
{
"bsonType": "object",
"encryptMetadata": {
"keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
},
"properties": {
"medicalRecords": {
"encrypt": {
"bsonType": "array",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
}
},
},
}
""",
[testCollectionNamespace.FullName] = """
{
"bsonType": "object",
"properties": {
"TestString": {
"encrypt": {
"bsonType": "string",
}
},
}
}
"""
};
AssertOutcomeCsfleSchemaBuilder(builder, expected);
}
[Fact]
public void CsfleSchemaBuilder_with_no_schemas_throws()
{
var builder = CsfleSchemaBuilder.Create(_ =>
{
// No schemas added
});
var exception = Record.Exception(() => builder.Build());
exception.Should().NotBeNull();
exception.Should().BeOfType<InvalidOperationException>();
}
[Theory]
[InlineData(
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
null,
""" "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
[InlineData(
null,
_keyIdString,
""" "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
public void EncryptedCollection_Metadata_works_as_expected(EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
{
Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
var builder = new EncryptedCollectionBuilder<Patient>();
builder.EncryptMetadata(keyId, algorithm);
var expected = $$"""
{
"bsonType": "object",
"encryptMetadata": {
{{expectedContent}}
}
}
""";
AssertOutcomeCollectionBuilder(builder, expected);
}
[Theory]
[InlineData(BsonType.Array,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
null,
""" "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
[InlineData(BsonType.Array,
null,
_keyIdString,
""" "bsonType": "array", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
[InlineData(BsonType.Array,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
_keyIdString,
""" "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
public void EncryptedCollection_PatternProperty_works_as_expected(BsonType bsonType, EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
{
Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
var builder = new EncryptedCollectionBuilder<Patient>();
builder.PatternProperty("randomRegex*", bsonType, algorithm, keyId);
var expected = $$"""
{
"bsonType": "object",
"patternProperties": {
"randomRegex*": {
"encrypt": {
{{expectedContent}}
}
}
}
}
""";
AssertOutcomeCollectionBuilder(builder, expected);
}
[Theory]
[InlineData(null,
null,
null,
"")]
[InlineData(new[] {BsonType.Array, BsonType.String},
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
null,
""" "bsonType": ["array", "string"], "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
[InlineData(new[] {BsonType.Array, BsonType.String},
null,
_keyIdString,
""" "bsonType": ["array", "string"], "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
[InlineData(new[] {BsonType.Array, BsonType.String},
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
_keyIdString,
""" "bsonType": ["array", "string"], "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
public void EncryptedCollection_PatternProperty_with_multiple_bson_types_works_as_expected(IEnumerable<BsonType> bsonTypes, EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
{
Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
var builder = new EncryptedCollectionBuilder<Patient>();
builder.PatternProperty("randomRegex*", bsonTypes, algorithm, keyId);
var expected = $$"""
{
"bsonType": "object",
"patternProperties": {
"randomRegex*": {
"encrypt": {
{{expectedContent}}
}
}
}
}
""";
AssertOutcomeCollectionBuilder(builder, expected);
}
[Fact]
public void EncryptedCollection_PatternProperty_nested_works_as_expected()
{
Guid? keyId = Guid.Parse(_keyIdString);
var builder = new EncryptedCollectionBuilder<Patient>();
builder.PatternProperty(p => p.Insurance, innerBuilder =>
{
innerBuilder
.EncryptMetadata(keyId)
.Property("policyNumber", BsonType.Int32,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
.PatternProperty("randomRegex*", BsonType.String,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
});
var expected = """
{
"bsonType": "object",
"patternProperties": {
"insurance": {
"bsonType": "object",
"encryptMetadata": {
"keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
},
"properties": {
"policyNumber": {
"encrypt": {
"bsonType": "int",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
}
}
},
"patternProperties": {
"randomRegex*": {
"encrypt": {
"bsonType": "string",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
}
}
}
}
}
}
""";
AssertOutcomeCollectionBuilder(builder, expected);
}
[Fact]
public void EncryptedCollection_PatternProperty_nested_with_string_works_as_expected()
{
Guid? keyId = Guid.Parse(_keyIdString);
var builder = new EncryptedCollectionBuilder<Patient>();
builder.PatternProperty<Insurance>("insurance", innerBuilder =>
{
innerBuilder
.EncryptMetadata(keyId)
.Property("policyNumber", BsonType.Int32,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
.PatternProperty("randomRegex*", BsonType.String,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
});
var expected = """
{
"bsonType": "object",
"patternProperties": {
"insurance": {
"bsonType": "object",
"encryptMetadata": {
"keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
},
"properties": {
"policyNumber": {
"encrypt": {
"bsonType": "int",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
}
}
},
"patternProperties": {
"randomRegex*": {
"encrypt": {
"bsonType": "string",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
}
}
}
}
}
}
""";
AssertOutcomeCollectionBuilder(builder, expected);
}
[Theory]
[InlineData(BsonType.Array,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
null,
""" "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
[InlineData(BsonType.Array,
null,
_keyIdString,
""" "bsonType": "array", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
[InlineData(BsonType.Array,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
_keyIdString,
""" "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
public void EncryptedCollection_Property_with_expression_works_as_expected(BsonType bsonType, EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
{
Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
var builder = new EncryptedCollectionBuilder<Patient>();
builder.Property(p => p.MedicalRecords, bsonType, algorithm, keyId);
var expected = $$"""
{
"bsonType": "object",
"properties": {
"medicalRecords": {
"encrypt": {
{{expectedContent}}
}
}
}
}
""";
AssertOutcomeCollectionBuilder(builder, expected);
}
[Theory]
[InlineData(null,
null,
null,
"")]
[InlineData(new[] {BsonType.Array, BsonType.String},
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
null,
""" "bsonType": ["array", "string"], "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
[InlineData(new[] {BsonType.Array, BsonType.String},
null,
_keyIdString,
""" "bsonType": ["array", "string"], "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
[InlineData(new[] {BsonType.Array, BsonType.String},
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
_keyIdString,
""" "bsonType": ["array", "string"], "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
public void EncryptedCollection_Property_with_multiple_bson_types_works_as_expected(IEnumerable<BsonType> bsonTypes, EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
{
Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
var builder = new EncryptedCollectionBuilder<Patient>();
builder.Property(p => p.MedicalRecords, bsonTypes, algorithm, keyId);
var expected = $$"""
{
"bsonType": "object",
"properties": {
"medicalRecords": {
"encrypt": {
{{expectedContent}}
}
}
}
}
""";
AssertOutcomeCollectionBuilder(builder, expected);
}
[Theory]
[InlineData(BsonType.Array,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
null,
""" "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
[InlineData(BsonType.Array,
null,
_keyIdString,
""" "bsonType": "array", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
[InlineData(BsonType.Array,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
_keyIdString,
""" "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
public void EncryptedCollection_Property_with_string_works_as_expected(BsonType bsonType, EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
{
Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
var builder = new EncryptedCollectionBuilder<Patient>();
builder.Property("medicalRecords", bsonType, algorithm, keyId);
var expected = $$"""
{
"bsonType": "object",
"properties": {
"medicalRecords": {
"encrypt": {
{{expectedContent}}
}
}
}
}
""";
AssertOutcomeCollectionBuilder(builder, expected);
}
[Fact]
public void EncryptedCollection_Property_nested_works_as_expected()
{
Guid? keyId = Guid.Parse(_keyIdString);
var builder = new EncryptedCollectionBuilder<Patient>();
builder.Property(p => p.Insurance, innerBuilder =>
{
innerBuilder
.EncryptMetadata(keyId)
.Property("policyNumber", BsonType.Int32,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
.PatternProperty("randomRegex*", BsonType.String,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
});
var expected = """
{
"bsonType": "object",
"properties": {
"insurance": {
"bsonType": "object",
"encryptMetadata": {
"keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
},
"properties": {
"policyNumber": {
"encrypt": {
"bsonType": "int",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
}
}
},
"patternProperties": {
"randomRegex*": {
"encrypt": {
"bsonType": "string",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
}
}
}
}
}
}
""";
AssertOutcomeCollectionBuilder(builder, expected);
}
[Fact]
public void EncryptedCollection_Property_nested_with_string_works_as_expected()
{
Guid? keyId = Guid.Parse(_keyIdString);
var builder = new EncryptedCollectionBuilder<Patient>();
builder.Property<Insurance>("insurance", innerBuilder =>
{
innerBuilder
.EncryptMetadata(keyId)
.Property("policyNumber", BsonType.Int32,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
.PatternProperty("randomRegex*", BsonType.String,
EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
});
var expected = """
{
"bsonType": "object",
"properties": {
"insurance": {
"bsonType": "object",
"encryptMetadata": {
"keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
},
"properties": {
"policyNumber": {
"encrypt": {
"bsonType": "int",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
}
}
},
"patternProperties": {
"randomRegex*": {
"encrypt": {
"bsonType": "string",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
}
}
}
}
}
}
""";
AssertOutcomeCollectionBuilder(builder, expected);
}
[Fact]
public void EncryptedCollection_Property_with_empty_bson_types_throws()
{
var builder = new EncryptedCollectionBuilder<Patient>();
var recordedException = Record.Exception(() => builder.Property("test", []));
recordedException.Should().NotBeNull();
recordedException.Should().BeOfType<ArgumentException>();
}
[Fact]
public void EncryptedCollection_Metadata_with_empty_algorithm_and_key_throws()
{
var builder = new EncryptedCollectionBuilder<Patient>();
var recordedException = Record.Exception(() => builder.EncryptMetadata(null, null));
recordedException.Should().NotBeNull();
recordedException.Should().BeOfType<ArgumentException>();
}
private void AssertOutcomeCsfleSchemaBuilder(CsfleSchemaBuilder builder, Dictionary<string, string> expectedSchema)
{
var builtSchema = builder.Build();
expectedSchema.Should().HaveCount(builtSchema.Count);
foreach (var collectionNamespace in expectedSchema.Keys)
{
var parsed = BsonDocument.Parse(expectedSchema[collectionNamespace]);
builtSchema[collectionNamespace].Should().BeEquivalentTo(parsed);
}
}
private void AssertOutcomeCollectionBuilder<T>(EncryptedCollectionBuilder<T> builder, string expected)
{
var builtSchema = builder.Build();
var expectedSchema = BsonDocument.Parse(expected);
builtSchema.Should().BeEquivalentTo(expectedSchema);
}
internal class TestClass
{
public ObjectId Id { get; set; }
public string TestString { get; set; }
}
internal class Patient
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("name")]
public string Name { get; set; }
[BsonElement("ssn")]
public int Ssn { get; set; }
[BsonElement("bloodType")]
public string BloodType { get; set; }
[BsonElement("medicalRecords")]
public List<MedicalRecord> MedicalRecords { get; set; }
[BsonElement("insurance")]
public Insurance Insurance { get; set; }
}
internal class MedicalRecord
{
[BsonElement("weight")]
public int Weight { get; set; }
[BsonElement("bloodPressure")]
public string BloodPressure { get; set; }
}
internal class Insurance
{
[BsonElement("provider")]
public string Provider { get; set; }
[BsonElement("policyNumber")]
public int PolicyNumber { get; set; }
}
}
}

1
tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs

@ -1 +0,0 @@


1
tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionExceptionTests.cs

@ -1 +0,0 @@

Loading…
Cancel
Save