From db1dea769c72874716dc7124331492af0357a8da Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:55:28 +0100 Subject: [PATCH 01/38] CSHARP-5453: Improve field encryption usability with attributes/API --- .../Encryption/EncryptionSchemaBuilder.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs diff --git a/src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs new file mode 100644 index 00000000000..ce91870ed73 --- /dev/null +++ b/src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs @@ -0,0 +1,157 @@ +/* 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.Expressions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace MongoDB.Driver.Encryption +{ + //TODO Need to specify this is for CSFLE (add the name everywhere ...?) + //TODO Do we need to do local validation of the schema? + internal class EncryptionSchemaBuilder + { + public static TypedEncryptionSchemaBuilder GetTypedBuilder() + { + return new TypedEncryptionSchemaBuilder(); + } + + public EncryptionSchemaBuilder WithType(CollectionNamespace collectionNamespace, TypedEncryptionSchemaBuilder typedBuilder) + { + return this; + } + + public EncryptionSchemaBuilder WithType(CollectionNamespace collectionNamespace, Action> configure) + { + return this; + } + + public IReadOnlyDictionary Build() + { + return null; + } + } + + internal class TypedEncryptionSchemaBuilder + { + public TypedEncryptionSchemaBuilder WithField(FieldDefinition path, string keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) + { + return this; + } + + public TypedEncryptionSchemaBuilder WithField(Expression> path, string keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) + { + return this; + } + + public TypedEncryptionSchemaBuilder WithNestedField(FieldDefinition path, Action> configure) + { + return this; + } + + public TypedEncryptionSchemaBuilder WithNestedField(Expression> path, Action> configure) + { + return this; + } + + public TypedEncryptionSchemaBuilder WithPattern(string pattern, string keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) + { + return this; + } + + public TypedEncryptionSchemaBuilder WithMetadata(string keyId = null, CsfleEncyptionAlgorithm? algorithm = null ) + { + return this; + } + + public BsonDocument Build() + { + return null; + } + + public static void Example() + { + var myKeyId = "myKey"; + + var typedBuilder = EncryptionSchemaBuilder.GetTypedBuilder() + .WithMetadata(keyId: myKeyId) + .WithField("bloodType", bsonType: BsonType.String, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) //string field + .WithField(p => p.Ssn, bsonType: BsonType.Int32, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //expression field + .WithField(p => p.MedicalRecords, bsonType: BsonType.Int32, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //expression field with array + .WithNestedField(p => p.Insurance, insurance => insurance + .WithField(i => i.PolicyNumber)) //nested field + .WithNestedField("insurance", insurance => insurance + .WithField(i => i.PolicyNumber)) //nested field with string + .WithPattern("ins*", algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); //with pattern + + var encryptionSchemaBuilder = new EncryptionSchemaBuilder() + .WithType(CollectionNamespace.FromFullName("db.coll1"), typedBuilder) //with builder + .WithType(CollectionNamespace.FromFullName("db.coll2"), builder => builder //with configure + .WithField("bloodType", bsonType: BsonType.String, + algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + ); + + var schema = encryptionSchemaBuilder.Build(); + } + } + + internal enum CsfleEncyptionAlgorithm + { + AEAD_AES_256_CBC_HMAC_SHA_512_Random, + AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic + } + + // Taken from the docs, just to have an example case + 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 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; } + } +} \ No newline at end of file From cee7a93eef0d6c9de08045321a8ca9cfe0496020 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:58:44 +0100 Subject: [PATCH 02/38] Small corrections --- .../Encryption/EncryptionSchemaBuilder.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs index ce91870ed73..8ca733bf1b7 100644 --- a/src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs @@ -48,12 +48,12 @@ public IReadOnlyDictionary Build() internal class TypedEncryptionSchemaBuilder { - public TypedEncryptionSchemaBuilder WithField(FieldDefinition path, string keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) + public TypedEncryptionSchemaBuilder WithField(FieldDefinition path, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) { return this; } - public TypedEncryptionSchemaBuilder WithField(Expression> path, string keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) + public TypedEncryptionSchemaBuilder WithField(Expression> path, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) { return this; } @@ -68,12 +68,12 @@ public TypedEncryptionSchemaBuilder WithNestedField(Expressio return this; } - public TypedEncryptionSchemaBuilder WithPattern(string pattern, string keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) + public TypedEncryptionSchemaBuilder WithPattern(string pattern, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) { return this; } - public TypedEncryptionSchemaBuilder WithMetadata(string keyId = null, CsfleEncyptionAlgorithm? algorithm = null ) + public TypedEncryptionSchemaBuilder WithMetadata(Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null ) { return this; } @@ -85,7 +85,7 @@ public BsonDocument Build() public static void Example() { - var myKeyId = "myKey"; + var myKeyId = Guid.NewGuid(); var typedBuilder = EncryptionSchemaBuilder.GetTypedBuilder() .WithMetadata(keyId: myKeyId) @@ -105,7 +105,7 @@ public static void Example() algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) ); - var schema = encryptionSchemaBuilder.Build(); + var schema = encryptionSchemaBuilder.Build(); //This can be passed to AutoEncryptionOptions } } From 7d1c109096e6a916e844849962d9557e5c7d3ef8 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 11 Mar 2025 12:01:39 +0100 Subject: [PATCH 03/38] Fixed stub --- .../Encryption/CsfleSchemaBuilder.cs | 338 ++++++++++++++++++ .../Encryption/EncryptionSchemaBuilder.cs | 157 -------- .../Ast/Filters/AstTypeFilterOperation.cs | 2 +- .../Encryption/CsfleSchemaBuilderTests.cs | 107 ++++++ 4 files changed, 446 insertions(+), 158 deletions(-) create mode 100644 src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs delete mode 100644 src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs create mode 100644 tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs new file mode 100644 index 00000000000..e14146e0d0d --- /dev/null +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -0,0 +1,338 @@ +/* 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 +{ + //TODO Add docs + + /// + /// + /// + public class CsfleSchemaBuilder + { + private Dictionary _typeSchemaBuilders = new(); + + /// + /// + /// + /// + /// + public static CsfleTypeSchemaBuilder GetTypeBuilder() //TODO Maybe we should remove this...? + { + return new CsfleTypeSchemaBuilder(); + } + + /// + /// + /// + /// + /// + /// + /// + public CsfleSchemaBuilder WithType(CollectionNamespace collectionNamespace, CsfleTypeSchemaBuilder typedBuilder) + { + _typeSchemaBuilders.Add(collectionNamespace.FullName, typedBuilder); + return this; + } + + /// + /// + /// + /// + /// + /// + /// + public CsfleSchemaBuilder WithType(CollectionNamespace collectionNamespace, Action> configure) + { + var typedBuilder = new CsfleTypeSchemaBuilder(); + configure(typedBuilder); + _typeSchemaBuilders.Add(collectionNamespace.FullName, typedBuilder); + return this; + } + + /// + /// + /// + /// + public IReadOnlyDictionary Build() + { + return null; + } + } + + /// + /// + /// + public class CsfleTypeSchemaBuilder + { + + } + + /// + /// + /// + /// + public class CsfleTypeSchemaBuilder : CsfleTypeSchemaBuilder + { + private List _fields; + private List _nestedFields; + private List _patterns; + private SchemaMetadata _metadata; + + /// + /// + /// + /// + /// + /// + /// + /// + public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) + { + _fields ??= []; + _fields.Add(new SchemaField(path, keyId, algorithm, bsonType)); + return this; + } + + //TODO We need an overload that accepts an array of bsonTypes (it's supported) + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public CsfleTypeSchemaBuilder Encrypt(Expression> path, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) + { + return Encrypt(new ExpressionFieldDefinition(path), keyId, algorithm, bsonType); + } + + /// + /// + /// + /// + /// + /// + /// + public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path, Action> configure) + { + _nestedFields ??= []; + _nestedFields.Add(new SchemaNestedField(path, configure)); + return this; + } + + /// + /// + /// + /// + /// + /// + /// + public CsfleTypeSchemaBuilder Encrypt(Expression> path, Action> configure) + { + return Encrypt(new ExpressionFieldDefinition(path), configure); + } + + /// + /// + /// + /// + /// + /// + /// + /// + public CsfleTypeSchemaBuilder PatternProperties(string pattern, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) //TODO This is not correct, + { + _patterns ??= []; + _patterns.Add(new SchemaPattern(pattern, keyId, algorithm, bsonType)); + return this; + } + + /// + /// + /// + /// + /// + /// + public CsfleTypeSchemaBuilder EncryptMetadata(Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null ) + { + _metadata = new SchemaMetadata(keyId, algorithm); + return this; + } + + internal BsonDocument Build() + { + var schema = new BsonDocument(); + var args = new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); + + if (_fields.Any()) + { + var properties = new BsonDocument(); + foreach (var field in _fields) + { + properties.Merge(field.Build(args)); + } + + schema.Add("properties", properties); + } + + return schema; + } + + private static string MapCsfleEncyptionAlgorithmToString(CsfleEncyptionAlgorithm? algorithm) + { + return algorithm switch + { + CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random => "AEAD_AES_256_CBC_HMAC_SHA_512-Random", + CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic => "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + _ => throw new InvalidOperationException() + }; + } + + private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstTypeFilterOperation + { + 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)); + } + } + + private class SchemaField + { + public FieldDefinition Path { get; } + public Guid? KeyId { get; } + public CsfleEncyptionAlgorithm? Algorithm { get; } + public BsonType? BsonType { get; } + + public SchemaField(FieldDefinition path, Guid? keyId, CsfleEncyptionAlgorithm? algorithm, BsonType? bsonType) + { + Path = path; + KeyId = keyId; + Algorithm = algorithm; + BsonType = bsonType; + } + + public BsonDocument Build(RenderArgs args) + { + return new BsonDocument + { + { + Path.Render(args).FieldName, new BsonDocument + { + { + "encrypt", new BsonDocument + { + { "algorithm", () => MapCsfleEncyptionAlgorithmToString(Algorithm), Algorithm is not null }, + { "bsonType", () => MapBsonTypeToString(BsonType!.Value), BsonType is not null }, + { "keyId", () => new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard), KeyId is not null }, + } + } + } + } + }; + } + } + + private class SchemaNestedField + { + } + + private class SchemaNestedField : SchemaNestedField + { + public FieldDefinition Path { get; } + public Action> Configure { get; } + + public SchemaNestedField(FieldDefinition path, Action> configure) + { + Path = path; + Configure = configure; + } + } + + private class SchemaPattern + { + public string Pattern { get; } + public Guid? KeyId { get; } + public CsfleEncyptionAlgorithm? Algorithm { get; } + public BsonType? BsonType { get; } + + public SchemaPattern(string pattern, Guid? keyId, CsfleEncyptionAlgorithm? algorithm, BsonType? bsonType) + { + Pattern = pattern; + KeyId = keyId; + Algorithm = algorithm; + BsonType = bsonType; + } + } + + private class SchemaMetadata + { + public Guid? KeyId { get; } + public CsfleEncyptionAlgorithm? Algorithm { get; } + + public SchemaMetadata(Guid? keyId, CsfleEncyptionAlgorithm? algorithm) + { + KeyId = keyId; + Algorithm = algorithm; + } + } + } + + /// + /// + /// + public enum CsfleEncyptionAlgorithm + { + /// + /// + /// + AEAD_AES_256_CBC_HMAC_SHA_512_Random, + /// + /// + /// + AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic + } + +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs deleted file mode 100644 index 8ca733bf1b7..00000000000 --- a/src/MongoDB.Driver/Encryption/EncryptionSchemaBuilder.cs +++ /dev/null @@ -1,157 +0,0 @@ -/* 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.Expressions; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; - -namespace MongoDB.Driver.Encryption -{ - //TODO Need to specify this is for CSFLE (add the name everywhere ...?) - //TODO Do we need to do local validation of the schema? - internal class EncryptionSchemaBuilder - { - public static TypedEncryptionSchemaBuilder GetTypedBuilder() - { - return new TypedEncryptionSchemaBuilder(); - } - - public EncryptionSchemaBuilder WithType(CollectionNamespace collectionNamespace, TypedEncryptionSchemaBuilder typedBuilder) - { - return this; - } - - public EncryptionSchemaBuilder WithType(CollectionNamespace collectionNamespace, Action> configure) - { - return this; - } - - public IReadOnlyDictionary Build() - { - return null; - } - } - - internal class TypedEncryptionSchemaBuilder - { - public TypedEncryptionSchemaBuilder WithField(FieldDefinition path, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) - { - return this; - } - - public TypedEncryptionSchemaBuilder WithField(Expression> path, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) - { - return this; - } - - public TypedEncryptionSchemaBuilder WithNestedField(FieldDefinition path, Action> configure) - { - return this; - } - - public TypedEncryptionSchemaBuilder WithNestedField(Expression> path, Action> configure) - { - return this; - } - - public TypedEncryptionSchemaBuilder WithPattern(string pattern, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) - { - return this; - } - - public TypedEncryptionSchemaBuilder WithMetadata(Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null ) - { - return this; - } - - public BsonDocument Build() - { - return null; - } - - public static void Example() - { - var myKeyId = Guid.NewGuid(); - - var typedBuilder = EncryptionSchemaBuilder.GetTypedBuilder() - .WithMetadata(keyId: myKeyId) - .WithField("bloodType", bsonType: BsonType.String, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) //string field - .WithField(p => p.Ssn, bsonType: BsonType.Int32, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //expression field - .WithField(p => p.MedicalRecords, bsonType: BsonType.Int32, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //expression field with array - .WithNestedField(p => p.Insurance, insurance => insurance - .WithField(i => i.PolicyNumber)) //nested field - .WithNestedField("insurance", insurance => insurance - .WithField(i => i.PolicyNumber)) //nested field with string - .WithPattern("ins*", algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); //with pattern - - var encryptionSchemaBuilder = new EncryptionSchemaBuilder() - .WithType(CollectionNamespace.FromFullName("db.coll1"), typedBuilder) //with builder - .WithType(CollectionNamespace.FromFullName("db.coll2"), builder => builder //with configure - .WithField("bloodType", bsonType: BsonType.String, - algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - ); - - var schema = encryptionSchemaBuilder.Build(); //This can be passed to AutoEncryptionOptions - } - } - - internal enum CsfleEncyptionAlgorithm - { - AEAD_AES_256_CBC_HMAC_SHA_512_Random, - AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic - } - - // Taken from the docs, just to have an example case - 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 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; } - } -} \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs index f44ae90b500..f05cc3f7ff9 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs @@ -59,7 +59,7 @@ public override BsonValue Render() } } - private string MapBsonTypeToString(BsonType type) + private string MapBsonTypeToString(BsonType type) //TODO Is this the only place where we do this conversion? { switch (type) { diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs new file mode 100644 index 00000000000..ac48b766fd0 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -0,0 +1,107 @@ +/* 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 MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Encryption; +using Xunit; + +namespace MongoDB.Driver.Tests.Encryption +{ + public class CsfleSchemaBuilderTests + { + [Fact] + public void Test1() + { + var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() + .Encrypt("bloodType", bsonType: BsonType.String, + algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .Encrypt(p => p.Ssn, bsonType: BsonType.Int32, + algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + + var expected = "{}"; + var parsedExpected = BsonDocument.Parse(expected); + + Assert.Equal(parsedExpected, typedBuilder.Build()); + } + + internal static void Example() + { + var myKeyId = Guid.NewGuid(); + + var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() + .EncryptMetadata(keyId: myKeyId) + .Encrypt("bloodType", bsonType: BsonType.String, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) //string field + .Encrypt(p => p.Ssn, bsonType: BsonType.Int32, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //expression field + .Encrypt(p => p.MedicalRecords, bsonType: BsonType.Int32, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //expression field with array + .Encrypt(p => p.Insurance, insurance => insurance + .Encrypt(i => i.PolicyNumber)) //nested field + .Encrypt("insurance", insurance => insurance + .Encrypt(i => i.PolicyNumber)) //nested field with string + .PatternProperties("ins*", algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); //with pattern + + var encryptionSchemaBuilder = new CsfleSchemaBuilder() + .WithType(CollectionNamespace.FromFullName("db.coll1"), typedBuilder) //with builder + .WithType(CollectionNamespace.FromFullName("db.coll2"), builder => builder //with configure + .Encrypt("bloodType", bsonType: BsonType.String, + algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + ); + + var schema = encryptionSchemaBuilder.Build(); //This can be passed to AutoEncryptionOptions + } + + // Taken from the docs, just to have an example case + 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 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; } + } + } +} \ No newline at end of file From 87b1c2f10c16c2e6af45a783823afca877bff77b Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 11 Mar 2025 19:08:01 +0100 Subject: [PATCH 04/38] Small fix --- .../Encryption/CsfleSchemaBuilder.cs | 25 ++++++++++++++++--- .../Encryption/CsfleSchemaBuilderTests.cs | 3 +++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index e14146e0d0d..b05ac73ed83 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -198,10 +198,15 @@ internal BsonDocument Build() schema.Add("properties", properties); } + if (_metadata is not null) + { + schema.Merge(_metadata.Build(args)); + } + return schema; } - private static string MapCsfleEncyptionAlgorithmToString(CsfleEncyptionAlgorithm? algorithm) + private static string MapCsfleEncyptionAlgorithmToString(CsfleEncyptionAlgorithm algorithm) { return algorithm switch { @@ -264,9 +269,9 @@ public BsonDocument Build(RenderArgs args) { "encrypt", new BsonDocument { - { "algorithm", () => MapCsfleEncyptionAlgorithmToString(Algorithm), Algorithm is not null }, + { "algorithm", () => MapCsfleEncyptionAlgorithmToString(Algorithm!.Value), Algorithm is not null }, { "bsonType", () => MapBsonTypeToString(BsonType!.Value), BsonType is not null }, - { "keyId", () => new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard), KeyId is not null }, + { "keyId", () => new BsonArray( new [] {new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard) }), KeyId is not null }, } } } @@ -317,6 +322,20 @@ public SchemaMetadata(Guid? keyId, CsfleEncyptionAlgorithm? algorithm) KeyId = keyId; Algorithm = algorithm; } + + public BsonDocument Build(RenderArgs args) + { + return new BsonDocument + { + { + "encryptMetadata", new BsonDocument + { + { "algorithm", () => MapCsfleEncyptionAlgorithmToString(Algorithm!.Value), Algorithm is not null }, + { "keyId", () => new BsonArray( new [] {new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard) }), KeyId is not null }, + } + } + }; + } } } diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index ac48b766fd0..f34b382c9b6 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -27,7 +27,10 @@ public class CsfleSchemaBuilderTests [Fact] public void Test1() { + var myKeyId = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); + var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() + .EncryptMetadata(keyId: myKeyId) .Encrypt("bloodType", bsonType: BsonType.String, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) .Encrypt(p => p.Ssn, bsonType: BsonType.Int32, From ee898d3d8b38cba2650c0cf1b43dd77afc066d0e Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:53:41 +0100 Subject: [PATCH 05/38] Various improvements --- .../Encryption/CsfleSchemaBuilder.cs | 64 +++++++--- .../Encryption/CsfleSchemaBuilderTests.cs | 110 +++++++++++++++++- 2 files changed, 156 insertions(+), 18 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index b05ac73ed83..c86f837e722 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -75,16 +75,20 @@ public CsfleSchemaBuilder WithType(CollectionNamespace collectionNamespace, A /// public IReadOnlyDictionary Build() { - return null; + return _typeSchemaBuilders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Build()); } } /// /// /// - public class CsfleTypeSchemaBuilder + public abstract class CsfleTypeSchemaBuilder { - + /// + /// + /// + /// + public abstract BsonDocument Build(); } /// @@ -182,25 +186,40 @@ public CsfleTypeSchemaBuilder EncryptMetadata(Guid? keyId = null, Csf return this; } - internal BsonDocument Build() + /// + public override BsonDocument Build() { var schema = new BsonDocument(); var args = new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); - if (_fields.Any()) + schema.Add("bsonType", "object"); + + if (_metadata is not null) + { + schema.Merge(_metadata.Build(args)); + } + + var properties = new BsonDocument(); + + if (_nestedFields is not null) + { + foreach (var nestedFields in _nestedFields) + { + properties.Merge(nestedFields.Build(args)); + } + } + + if (_fields is not null) { - var properties = new BsonDocument(); foreach (var field in _fields) { properties.Merge(field.Build(args)); } - - schema.Add("properties", properties); } - if (_metadata is not null) + if (properties.Any()) { - schema.Merge(_metadata.Build(args)); + schema.Add("properties", properties); } return schema; @@ -246,10 +265,10 @@ private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstT private class SchemaField { - public FieldDefinition Path { get; } - public Guid? KeyId { get; } - public CsfleEncyptionAlgorithm? Algorithm { get; } - public BsonType? BsonType { get; } + private FieldDefinition Path { get; } //TODO These could all be private properties + private Guid? KeyId { get; } + private CsfleEncyptionAlgorithm? Algorithm { get; } + private BsonType? BsonType { get; } public SchemaField(FieldDefinition path, Guid? keyId, CsfleEncyptionAlgorithm? algorithm, BsonType? bsonType) { @@ -269,8 +288,8 @@ public BsonDocument Build(RenderArgs args) { "encrypt", new BsonDocument { - { "algorithm", () => MapCsfleEncyptionAlgorithmToString(Algorithm!.Value), Algorithm is not null }, { "bsonType", () => MapBsonTypeToString(BsonType!.Value), BsonType is not null }, + { "algorithm", () => MapCsfleEncyptionAlgorithmToString(Algorithm!.Value), Algorithm is not null }, { "keyId", () => new BsonArray( new [] {new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard) }), KeyId is not null }, } } @@ -280,8 +299,9 @@ public BsonDocument Build(RenderArgs args) } } - private class SchemaNestedField + private abstract class SchemaNestedField { + public abstract BsonDocument Build(RenderArgs args); } private class SchemaNestedField : SchemaNestedField @@ -294,6 +314,18 @@ public SchemaNestedField(FieldDefinition path, Action args) + { + var fieldBuilder = new CsfleTypeSchemaBuilder(); + Configure(fieldBuilder); + var builtInternalSchema = fieldBuilder.Build(); + + return new BsonDocument + { + { Path.Render(args).FieldName, builtInternalSchema } + }; + } } private class SchemaPattern diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index f34b382c9b6..092497cc06f 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using System.Linq; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver.Encryption; @@ -28,18 +29,123 @@ public class CsfleSchemaBuilderTests public void Test1() { var myKeyId = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); + var collectionName = "medicalRecords.patients"; var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() .EncryptMetadata(keyId: myKeyId) + .Encrypt(p => p.Insurance, insurance => insurance + .Encrypt(i => i.PolicyNumber, bsonType: BsonType.Int32, + algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)) + .Encrypt(p => p.MedicalRecords, bsonType: BsonType.Array, + algorithm: CsfleEncyptionAlgorithm + .AEAD_AES_256_CBC_HMAC_SHA_512_Random) .Encrypt("bloodType", bsonType: BsonType.String, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) .Encrypt(p => p.Ssn, bsonType: BsonType.Int32, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - var expected = "{}"; + var encryptionSchemaBuilder = new CsfleSchemaBuilder() + .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); + + const string expected = """ + { + "medicalRecords.patients": { + "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" + } + } + } + } + } + """; var parsedExpected = BsonDocument.Parse(expected); - Assert.Equal(parsedExpected, typedBuilder.Build()); + var builtSchema = encryptionSchemaBuilder.Build(); + Assert.Equal(parsedExpected.Count(), builtSchema.Count); + foreach (var name in parsedExpected.Names) + { + var builtSchemaForName = builtSchema[name]; + var parseExpectedForName = parsedExpected[name]; + Assert.Equal(parsedExpected[name].AsBsonDocument, builtSchema[name]); + } + } + + [Fact] + public void TestBson() + { + const string v1 = """ + { + "prop1": "test1" + "prop2": "test2" + } + """; + const string v2 = """ + { + "prop2": "test2" + "prop1": "test1" + } + """; + + var parsedV1 = BsonDocument.Parse(v1); + var parsedV2 = BsonDocument.Parse(v2); + + Assert.Equal(parsedV1, parsedV2); + + const string v1Nested = """ + { + "prop1": "test1" + "prop2": "test2" + "inner": { + "propIn1": "testIn1", + "propIn2": "testIn2", + } + } + """; + const string v2Nested = """ + { + "prop1": "test1" + "prop2": "test2" + "inner": { + "propIn2": "testIn2", + "propIn1": "testIn1", + } + } + """; + + var parsedV1Nested = BsonDocument.Parse(v1Nested); + var parsedV2Nested = BsonDocument.Parse(v2Nested); + + Assert.NotEqual(parsedV1Nested, parsedV2Nested); } internal static void Example() From a3a06a7fa3f3ffa8209385a653fb549da4d5f192 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:56:47 +0100 Subject: [PATCH 06/38] Conversion to records --- .../Encryption/CsfleSchemaBuilder.cs | 59 ++------------- .../Encryption/CsfleSchemaBuilderTests.cs | 73 ------------------- 2 files changed, 7 insertions(+), 125 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index c86f837e722..62a304d1c55 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -263,21 +263,8 @@ private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstT } } - private class SchemaField + private record SchemaField(FieldDefinition Path, Guid? KeyId, CsfleEncyptionAlgorithm? Algorithm, BsonType? BsonType) { - private FieldDefinition Path { get; } //TODO These could all be private properties - private Guid? KeyId { get; } - private CsfleEncyptionAlgorithm? Algorithm { get; } - private BsonType? BsonType { get; } - - public SchemaField(FieldDefinition path, Guid? keyId, CsfleEncyptionAlgorithm? algorithm, BsonType? bsonType) - { - Path = path; - KeyId = keyId; - Algorithm = algorithm; - BsonType = bsonType; - } - public BsonDocument Build(RenderArgs args) { return new BsonDocument @@ -290,7 +277,7 @@ public BsonDocument Build(RenderArgs args) { { "bsonType", () => MapBsonTypeToString(BsonType!.Value), BsonType is not null }, { "algorithm", () => MapCsfleEncyptionAlgorithmToString(Algorithm!.Value), Algorithm is not null }, - { "keyId", () => new BsonArray( new [] {new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard) }), KeyId is not null }, + { "keyId", () => new BsonArray(new[] { new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard) }), KeyId is not null }, } } } @@ -299,22 +286,13 @@ public BsonDocument Build(RenderArgs args) } } - private abstract class SchemaNestedField + private abstract record SchemaNestedField { public abstract BsonDocument Build(RenderArgs args); } - private class SchemaNestedField : SchemaNestedField + private record SchemaNestedField(FieldDefinition Path, Action> Configure) : SchemaNestedField { - public FieldDefinition Path { get; } - public Action> Configure { get; } - - public SchemaNestedField(FieldDefinition path, Action> configure) - { - Path = path; - Configure = configure; - } - public override BsonDocument Build(RenderArgs args) { var fieldBuilder = new CsfleTypeSchemaBuilder(); @@ -328,33 +306,10 @@ public override BsonDocument Build(RenderArgs args) } } - private class SchemaPattern - { - public string Pattern { get; } - public Guid? KeyId { get; } - public CsfleEncyptionAlgorithm? Algorithm { get; } - public BsonType? BsonType { get; } + private record SchemaPattern(string Pattern, Guid? KeyId, CsfleEncyptionAlgorithm? Algorithm, BsonType? BsonType); - public SchemaPattern(string pattern, Guid? keyId, CsfleEncyptionAlgorithm? algorithm, BsonType? bsonType) - { - Pattern = pattern; - KeyId = keyId; - Algorithm = algorithm; - BsonType = bsonType; - } - } - - private class SchemaMetadata + private record SchemaMetadata(Guid? KeyId, CsfleEncyptionAlgorithm? Algorithm) { - public Guid? KeyId { get; } - public CsfleEncyptionAlgorithm? Algorithm { get; } - - public SchemaMetadata(Guid? keyId, CsfleEncyptionAlgorithm? algorithm) - { - KeyId = keyId; - Algorithm = algorithm; - } - public BsonDocument Build(RenderArgs args) { return new BsonDocument @@ -363,7 +318,7 @@ public BsonDocument Build(RenderArgs args) "encryptMetadata", new BsonDocument { { "algorithm", () => MapCsfleEncyptionAlgorithmToString(Algorithm!.Value), Algorithm is not null }, - { "keyId", () => new BsonArray( new [] {new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard) }), KeyId is not null }, + { "keyId", () => new BsonArray(new[] { new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard) }), KeyId is not null }, } } }; diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 092497cc06f..81ee74e4b41 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -100,79 +100,6 @@ public void Test1() } } - [Fact] - public void TestBson() - { - const string v1 = """ - { - "prop1": "test1" - "prop2": "test2" - } - """; - const string v2 = """ - { - "prop2": "test2" - "prop1": "test1" - } - """; - - var parsedV1 = BsonDocument.Parse(v1); - var parsedV2 = BsonDocument.Parse(v2); - - Assert.Equal(parsedV1, parsedV2); - - const string v1Nested = """ - { - "prop1": "test1" - "prop2": "test2" - "inner": { - "propIn1": "testIn1", - "propIn2": "testIn2", - } - } - """; - const string v2Nested = """ - { - "prop1": "test1" - "prop2": "test2" - "inner": { - "propIn2": "testIn2", - "propIn1": "testIn1", - } - } - """; - - var parsedV1Nested = BsonDocument.Parse(v1Nested); - var parsedV2Nested = BsonDocument.Parse(v2Nested); - - Assert.NotEqual(parsedV1Nested, parsedV2Nested); - } - - internal static void Example() - { - var myKeyId = Guid.NewGuid(); - - var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() - .EncryptMetadata(keyId: myKeyId) - .Encrypt("bloodType", bsonType: BsonType.String, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) //string field - .Encrypt(p => p.Ssn, bsonType: BsonType.Int32, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //expression field - .Encrypt(p => p.MedicalRecords, bsonType: BsonType.Int32, algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //expression field with array - .Encrypt(p => p.Insurance, insurance => insurance - .Encrypt(i => i.PolicyNumber)) //nested field - .Encrypt("insurance", insurance => insurance - .Encrypt(i => i.PolicyNumber)) //nested field with string - .PatternProperties("ins*", algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); //with pattern - - var encryptionSchemaBuilder = new CsfleSchemaBuilder() - .WithType(CollectionNamespace.FromFullName("db.coll1"), typedBuilder) //with builder - .WithType(CollectionNamespace.FromFullName("db.coll2"), builder => builder //with configure - .Encrypt("bloodType", bsonType: BsonType.String, - algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - ); - - var schema = encryptionSchemaBuilder.Build(); //This can be passed to AutoEncryptionOptions - } - // Taken from the docs, just to have an example case internal class Patient { From e599cc092f017ae45c4d34c8d2dd0226395668f4 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:04:05 +0100 Subject: [PATCH 07/38] Various improvements --- .../Encryption/CsfleSchemaBuilder.cs | 163 +++++++++--------- .../Encryption/CsfleSchemaBuilderTests.cs | 8 +- 2 files changed, 85 insertions(+), 86 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 62a304d1c55..2145bdc647f 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -29,17 +29,14 @@ namespace MongoDB.Driver.Encryption /// public class CsfleSchemaBuilder { - private Dictionary _typeSchemaBuilders = new(); + private readonly Dictionary _typeSchemaBuilders = new(); /// /// /// /// /// - public static CsfleTypeSchemaBuilder GetTypeBuilder() //TODO Maybe we should remove this...? - { - return new CsfleTypeSchemaBuilder(); - } + public static CsfleTypeSchemaBuilder GetTypeBuilder() => new(); /// /// @@ -73,10 +70,7 @@ public CsfleSchemaBuilder WithType(CollectionNamespace collectionNamespace, A /// /// /// - public IReadOnlyDictionary Build() - { - return _typeSchemaBuilders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Build()); - } + public IReadOnlyDictionary Build() => _typeSchemaBuilders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Build()); } /// @@ -97,9 +91,9 @@ public abstract class CsfleTypeSchemaBuilder /// public class CsfleTypeSchemaBuilder : CsfleTypeSchemaBuilder { - private List _fields; - private List _nestedFields; - private List _patterns; + private readonly List _fields = []; + private readonly List _nestedFields = []; + private readonly List _patterns = []; private SchemaMetadata _metadata; /// @@ -110,15 +104,12 @@ public class CsfleTypeSchemaBuilder : CsfleTypeSchemaBuilder /// /// /// - public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) + public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) { - _fields ??= []; _fields.Add(new SchemaField(path, keyId, algorithm, bsonType)); return this; } - //TODO We need an overload that accepts an array of bsonTypes (it's supported) - /// /// /// @@ -128,7 +119,7 @@ public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path /// /// /// - public CsfleTypeSchemaBuilder Encrypt(Expression> path, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) + public CsfleTypeSchemaBuilder Encrypt(Expression> path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) { return Encrypt(new ExpressionFieldDefinition(path), keyId, algorithm, bsonType); } @@ -142,7 +133,6 @@ public CsfleTypeSchemaBuilder Encrypt(Expression public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path, Action> configure) { - _nestedFields ??= []; _nestedFields.Add(new SchemaNestedField(path, configure)); return this; } @@ -167,9 +157,8 @@ public CsfleTypeSchemaBuilder Encrypt(Expression /// /// - public CsfleTypeSchemaBuilder PatternProperties(string pattern, Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null, BsonType? bsonType = null) //TODO This is not correct, + public CsfleTypeSchemaBuilder PatternProperties(string pattern, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) //TODO This is not correct, { - _patterns ??= []; _patterns.Add(new SchemaPattern(pattern, keyId, algorithm, bsonType)); return this; } @@ -180,7 +169,7 @@ public CsfleTypeSchemaBuilder PatternProperties(string pattern, Guid? /// /// /// - public CsfleTypeSchemaBuilder EncryptMetadata(Guid? keyId = null, CsfleEncyptionAlgorithm? algorithm = null ) + public CsfleTypeSchemaBuilder EncryptMetadata(Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null ) { _metadata = new SchemaMetadata(keyId, algorithm); return this; @@ -189,32 +178,23 @@ public CsfleTypeSchemaBuilder EncryptMetadata(Guid? keyId = null, Csf /// public override BsonDocument Build() { - var schema = new BsonDocument(); + var schema = new BsonDocument { { "bsonType", "object" } }; var args = new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); - - schema.Add("bsonType", "object"); + var properties = new BsonDocument(); if (_metadata is not null) { - schema.Merge(_metadata.Build(args)); + schema.Merge(_metadata.Build()); } - var properties = new BsonDocument(); - - if (_nestedFields is not null) + foreach (var nestedField in _nestedFields) { - foreach (var nestedFields in _nestedFields) - { - properties.Merge(nestedFields.Build(args)); - } + properties.Merge(nestedField.Build(args)); } - if (_fields is not null) + foreach (var field in _fields) { - foreach (var field in _fields) - { - properties.Merge(field.Build(args)); - } + properties.Merge(field.Build(args)); } if (properties.Any()) @@ -225,45 +205,45 @@ public override BsonDocument Build() return schema; } - private static string MapCsfleEncyptionAlgorithmToString(CsfleEncyptionAlgorithm algorithm) + private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorithm algorithm) { return algorithm switch { - CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random => "AEAD_AES_256_CBC_HMAC_SHA_512-Random", - CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic => "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", - _ => throw new InvalidOperationException() + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random => "AEAD_AES_256_CBC_HMAC_SHA_512-Random", + CsfleEncryptionAlgorithm.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)) }; } private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstTypeFilterOperation { - switch (type) + return type switch { - 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)); - } + 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)) + }; } - private record SchemaField(FieldDefinition Path, Guid? KeyId, CsfleEncyptionAlgorithm? Algorithm, BsonType? BsonType) + private record SchemaField(FieldDefinition Path, Guid? KeyId, CsfleEncryptionAlgorithm? Algorithm, BsonType? BsonType) { public BsonDocument Build(RenderArgs args) { @@ -272,14 +252,7 @@ public BsonDocument Build(RenderArgs args) { Path.Render(args).FieldName, new BsonDocument { - { - "encrypt", new BsonDocument - { - { "bsonType", () => MapBsonTypeToString(BsonType!.Value), BsonType is not null }, - { "algorithm", () => MapCsfleEncyptionAlgorithmToString(Algorithm!.Value), Algorithm is not null }, - { "keyId", () => new BsonArray(new[] { new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard) }), KeyId is not null }, - } - } + { "encrypt", GetEncryptBsonDocument(KeyId, Algorithm, BsonType) } } } }; @@ -306,30 +279,57 @@ public override BsonDocument Build(RenderArgs args) } } - private record SchemaPattern(string Pattern, Guid? KeyId, CsfleEncyptionAlgorithm? Algorithm, BsonType? BsonType); - - private record SchemaMetadata(Guid? KeyId, CsfleEncyptionAlgorithm? Algorithm) + private record SchemaPattern( + string Pattern, + Guid? KeyId, + CsfleEncryptionAlgorithm? Algorithm, + BsonType? BsonType) { - public BsonDocument Build(RenderArgs args) + public BsonDocument Build() { return new BsonDocument { { - "encryptMetadata", new BsonDocument + "pattern", new BsonDocument { - { "algorithm", () => MapCsfleEncyptionAlgorithmToString(Algorithm!.Value), Algorithm is not null }, - { "keyId", () => new BsonArray(new[] { new BsonBinaryData(KeyId!.Value, GuidRepresentation.Standard) }), KeyId is not null }, + { "encrypt", GetEncryptBsonDocument(KeyId, Algorithm, BsonType) } } } }; } } + + private record SchemaMetadata(Guid? KeyId, CsfleEncryptionAlgorithm? Algorithm) + { + public BsonDocument Build() + { + return new BsonDocument + { + { "encryptMetadata", GetEncryptBsonDocument(KeyId, Algorithm, null)} + }; + } + } + + private static BsonDocument GetEncryptBsonDocument(Guid? keyId, CsfleEncryptionAlgorithm? algorithm, BsonType? bsonType) + { + return new BsonDocument + { + { "bsonType", () => MapBsonTypeToString(bsonType!.Value), bsonType is not null }, + { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null }, + { + "keyId", + () => new BsonArray(new[] { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }), + keyId is not null + }, + }; + + } } /// /// /// - public enum CsfleEncyptionAlgorithm + public enum CsfleEncryptionAlgorithm { /// /// @@ -340,5 +340,4 @@ public enum CsfleEncyptionAlgorithm /// AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic } - } \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 81ee74e4b41..6db7782a89a 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -35,14 +35,14 @@ public void Test1() .EncryptMetadata(keyId: myKeyId) .Encrypt(p => p.Insurance, insurance => insurance .Encrypt(i => i.PolicyNumber, bsonType: BsonType.Int32, - algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)) + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)) .Encrypt(p => p.MedicalRecords, bsonType: BsonType.Array, - algorithm: CsfleEncyptionAlgorithm + algorithm: CsfleEncryptionAlgorithm .AEAD_AES_256_CBC_HMAC_SHA_512_Random) .Encrypt("bloodType", bsonType: BsonType.String, - algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) .Encrypt(p => p.Ssn, bsonType: BsonType.Int32, - algorithm: CsfleEncyptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); var encryptionSchemaBuilder = new CsfleSchemaBuilder() .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); From 400b8b026da7ef5b136a879a058da5b167812e6c Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:55:23 +0100 Subject: [PATCH 08/38] Fixed API --- .../Encryption/CsfleSchemaBuilder.cs | 131 ++++++++++-------- .../Encryption/CsfleSchemaBuilderTests.cs | 72 +++++++++- 2 files changed, 142 insertions(+), 61 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 2145bdc647f..2e4e294c14f 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -92,7 +92,6 @@ public abstract class CsfleTypeSchemaBuilder public class CsfleTypeSchemaBuilder : CsfleTypeSchemaBuilder { private readonly List _fields = []; - private readonly List _nestedFields = []; private readonly List _patterns = []; private SchemaMetadata _metadata; @@ -106,7 +105,7 @@ public class CsfleTypeSchemaBuilder : CsfleTypeSchemaBuilder /// public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) { - _fields.Add(new SchemaField(path, keyId, algorithm, bsonType)); + _fields.Add(new SchemaSimpleField(path, keyId, algorithm, bsonType)); return this; } @@ -133,7 +132,7 @@ public CsfleTypeSchemaBuilder Encrypt(Expression public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path, Action> configure) { - _nestedFields.Add(new SchemaNestedField(path, configure)); + _fields.Add(new SchemaNestedField(path, configure)); return this; } @@ -159,10 +158,35 @@ public CsfleTypeSchemaBuilder Encrypt(Expression public CsfleTypeSchemaBuilder PatternProperties(string pattern, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) //TODO This is not correct, { - _patterns.Add(new SchemaPattern(pattern, keyId, algorithm, bsonType)); + _patterns.Add(new SchemaSimplePattern(pattern, keyId, algorithm, bsonType)); return this; } + /// + /// + /// + /// + /// + /// + /// + public CsfleTypeSchemaBuilder PatternProperties(FieldDefinition path, Action> configure) + { + _patterns.Add(new SchemaNestedPattern(path, configure)); + return this; + } + + /// + /// + /// + /// + /// + /// + /// + public CsfleTypeSchemaBuilder PatternProperties(Expression> path, Action> configure) + { + return PatternProperties(new ExpressionFieldDefinition(path), configure); + } + /// /// /// @@ -178,28 +202,37 @@ public CsfleTypeSchemaBuilder EncryptMetadata(Guid? keyId = null, Csf /// public override BsonDocument Build() { - var schema = new BsonDocument { { "bsonType", "object" } }; - var args = new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); - var properties = new BsonDocument(); + var schema = new BsonDocument("bsonType", "object"); if (_metadata is not null) { schema.Merge(_metadata.Build()); } - foreach (var nestedField in _nestedFields) - { - properties.Merge(nestedField.Build(args)); - } + var args = new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); - foreach (var field in _fields) + if (_fields.Any()) { - properties.Merge(field.Build(args)); + var properties = new BsonDocument(); + + foreach (var field in _fields) + { + properties.Merge(field.Build(args)); + } + + schema.Add("properties", properties); } - if (properties.Any()) + if (_patterns.Any()) { - schema.Add("properties", properties); + var patternProperties = new BsonDocument(); + + foreach (var pattern in _patterns) + { + patternProperties.Merge(pattern.Build(args)); + } + + schema.Add("patternProperties", patternProperties); } return schema; @@ -243,71 +276,56 @@ private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstT }; } - private record SchemaField(FieldDefinition Path, Guid? KeyId, CsfleEncryptionAlgorithm? Algorithm, BsonType? BsonType) + private abstract record SchemaField { - public BsonDocument Build(RenderArgs args) - { - return new BsonDocument - { - { - Path.Render(args).FieldName, new BsonDocument - { - { "encrypt", GetEncryptBsonDocument(KeyId, Algorithm, BsonType) } - } - } - }; - } + public abstract BsonDocument Build(RenderArgs args); } - private abstract record SchemaNestedField + private record SchemaSimpleField(FieldDefinition Path, Guid? KeyId, CsfleEncryptionAlgorithm? Algorithm, BsonType? BsonType) : SchemaField { - public abstract BsonDocument Build(RenderArgs args); + public override BsonDocument Build(RenderArgs args) => + new(Path.Render(args).FieldName, new BsonDocument("encrypt", GetEncryptBsonDocument(KeyId, Algorithm, BsonType))); } - private record SchemaNestedField(FieldDefinition Path, Action> Configure) : SchemaNestedField + private record SchemaNestedField(FieldDefinition Path, Action> Configure) : SchemaField { public override BsonDocument Build(RenderArgs args) { var fieldBuilder = new CsfleTypeSchemaBuilder(); Configure(fieldBuilder); - var builtInternalSchema = fieldBuilder.Build(); - - return new BsonDocument - { - { Path.Render(args).FieldName, builtInternalSchema } - }; + return new BsonDocument(Path.Render(args).FieldName, fieldBuilder.Build()); } } - private record SchemaPattern( + private abstract record SchemaPattern() + { + public abstract BsonDocument Build(RenderArgs args); + } + + private record SchemaSimplePattern( string Pattern, Guid? KeyId, CsfleEncryptionAlgorithm? Algorithm, - BsonType? BsonType) + BsonType? BsonType) : SchemaPattern { - public BsonDocument Build() + public override BsonDocument Build(RenderArgs args) => new(Pattern, new BsonDocument("encrypt", GetEncryptBsonDocument(KeyId, Algorithm, BsonType))); + } + + private record SchemaNestedPattern( + FieldDefinition Path, + Action> Configure) : SchemaPattern + { + public override BsonDocument Build(RenderArgs args) { - return new BsonDocument - { - { - "pattern", new BsonDocument - { - { "encrypt", GetEncryptBsonDocument(KeyId, Algorithm, BsonType) } - } - } - }; + var fieldBuilder = new CsfleTypeSchemaBuilder(); + Configure(fieldBuilder); + return new BsonDocument(Path.Render(args).FieldName, fieldBuilder.Build()); } } private record SchemaMetadata(Guid? KeyId, CsfleEncryptionAlgorithm? Algorithm) { - public BsonDocument Build() - { - return new BsonDocument - { - { "encryptMetadata", GetEncryptBsonDocument(KeyId, Algorithm, null)} - }; - } + public BsonDocument Build() => new("encryptMetadata", GetEncryptBsonDocument(KeyId, Algorithm, null)); } private static BsonDocument GetEncryptBsonDocument(Guid? keyId, CsfleEncryptionAlgorithm? algorithm, BsonType? bsonType) @@ -322,7 +340,6 @@ private static BsonDocument GetEncryptBsonDocument(Guid? keyId, CsfleEncryptionA keyId is not null }, }; - } } diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 6db7782a89a..5e053f4ecb0 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -84,7 +84,7 @@ public void Test1() "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" } } - } + }, } } """; @@ -94,13 +94,77 @@ public void Test1() Assert.Equal(parsedExpected.Count(), builtSchema.Count); foreach (var name in parsedExpected.Names) { - var builtSchemaForName = builtSchema[name]; - var parseExpectedForName = parsedExpected[name]; Assert.Equal(parsedExpected[name].AsBsonDocument, builtSchema[name]); } } - // Taken from the docs, just to have an example case + [Fact] + public void Test2() + { + var collectionName = "medicalRecords.patients"; + + var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() + .PatternProperties("_PIIString$", bsonType: BsonType.String, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperties("_PIIArray$", bsonType: BsonType.Array, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .PatternProperties(p => p.Insurance, builder => builder + .PatternProperties("_PIINumber$", bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperties("_PIIString$", bsonType: BsonType.String, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + ); + + var encryptionSchemaBuilder = new CsfleSchemaBuilder() + .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); + + const string expected = """ + { + "medicalRecords.patients": { + "bsonType": "object", + "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", + }, + }, + }, + }, + }, + }, + } + """; + var parsedExpected = BsonDocument.Parse(expected); + + var builtSchema = encryptionSchemaBuilder.Build(); + Assert.Equal(parsedExpected.Count(), builtSchema.Count); + foreach (var name in parsedExpected.Names) + { + Assert.Equal(parsedExpected[name].AsBsonDocument, builtSchema[name]); + } + } + + // Taken from the docs internal class Patient { [BsonId] From 0a32d51beeac3544564adac42f88eaef49bb4698 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:16:50 +0100 Subject: [PATCH 09/38] Small comment --- src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 2e4e294c14f..f8e634e969e 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -58,7 +58,7 @@ public CsfleSchemaBuilder WithType(CollectionNamespace collectionNamespace, C /// /// /// - public CsfleSchemaBuilder WithType(CollectionNamespace collectionNamespace, Action> configure) + public CsfleSchemaBuilder WithType(CollectionNamespace collectionNamespace, Action> configure) //TODO Do we want to keep this? { var typedBuilder = new CsfleTypeSchemaBuilder(); configure(typedBuilder); From c3bfde4500719f38b1f46fc35eff06f64c4c2694 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:28:36 +0100 Subject: [PATCH 10/38] Fix to naming --- .../Encryption/CsfleSchemaBuilder.cs | 22 +++++++++---------- .../Encryption/CsfleSchemaBuilderTests.cs | 20 ++++++++--------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index f8e634e969e..bdb6264e645 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -103,7 +103,7 @@ public class CsfleTypeSchemaBuilder : CsfleTypeSchemaBuilder /// /// /// - public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) + public CsfleTypeSchemaBuilder Property(FieldDefinition path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) { _fields.Add(new SchemaSimpleField(path, keyId, algorithm, bsonType)); return this; @@ -118,9 +118,9 @@ public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path /// /// /// - public CsfleTypeSchemaBuilder Encrypt(Expression> path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) + public CsfleTypeSchemaBuilder Property(Expression> path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) { - return Encrypt(new ExpressionFieldDefinition(path), keyId, algorithm, bsonType); + return Property(new ExpressionFieldDefinition(path), keyId, algorithm, bsonType); } /// @@ -130,7 +130,7 @@ public CsfleTypeSchemaBuilder Encrypt(Expression /// /// - public CsfleTypeSchemaBuilder Encrypt(FieldDefinition path, Action> configure) + public CsfleTypeSchemaBuilder Property(FieldDefinition path, Action> configure) { _fields.Add(new SchemaNestedField(path, configure)); return this; @@ -143,9 +143,9 @@ public CsfleTypeSchemaBuilder Encrypt(FieldDefinition /// /// - public CsfleTypeSchemaBuilder Encrypt(Expression> path, Action> configure) + public CsfleTypeSchemaBuilder Property(Expression> path, Action> configure) { - return Encrypt(new ExpressionFieldDefinition(path), configure); + return Property(new ExpressionFieldDefinition(path), configure); } /// @@ -156,7 +156,7 @@ public CsfleTypeSchemaBuilder Encrypt(Expression /// /// - public CsfleTypeSchemaBuilder PatternProperties(string pattern, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) //TODO This is not correct, + public CsfleTypeSchemaBuilder PatternProperty(string pattern, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) //TODO This is not correct, { _patterns.Add(new SchemaSimplePattern(pattern, keyId, algorithm, bsonType)); return this; @@ -169,7 +169,7 @@ public CsfleTypeSchemaBuilder PatternProperties(string pattern, Guid? /// /// /// - public CsfleTypeSchemaBuilder PatternProperties(FieldDefinition path, Action> configure) + public CsfleTypeSchemaBuilder PatternProperty(FieldDefinition path, Action> configure) { _patterns.Add(new SchemaNestedPattern(path, configure)); return this; @@ -182,9 +182,9 @@ public CsfleTypeSchemaBuilder PatternProperties(FieldDefiniti /// /// /// - public CsfleTypeSchemaBuilder PatternProperties(Expression> path, Action> configure) + public CsfleTypeSchemaBuilder PatternProperty(Expression> path, Action> configure) { - return PatternProperties(new ExpressionFieldDefinition(path), configure); + return PatternProperty(new ExpressionFieldDefinition(path), configure); } /// @@ -297,7 +297,7 @@ public override BsonDocument Build(RenderArgs args) } } - private abstract record SchemaPattern() + private abstract record SchemaPattern { public abstract BsonDocument Build(RenderArgs args); } diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 5e053f4ecb0..e1200bfdc61 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -33,15 +33,15 @@ public void Test1() var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() .EncryptMetadata(keyId: myKeyId) - .Encrypt(p => p.Insurance, insurance => insurance - .Encrypt(i => i.PolicyNumber, bsonType: BsonType.Int32, + .Property(p => p.Insurance, insurance => insurance + .Property(i => i.PolicyNumber, bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)) - .Encrypt(p => p.MedicalRecords, bsonType: BsonType.Array, + .Property(p => p.MedicalRecords, bsonType: BsonType.Array, algorithm: CsfleEncryptionAlgorithm .AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .Encrypt("bloodType", bsonType: BsonType.String, + .Property("bloodType", bsonType: BsonType.String, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .Encrypt(p => p.Ssn, bsonType: BsonType.Int32, + .Property(p => p.Ssn, bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); var encryptionSchemaBuilder = new CsfleSchemaBuilder() @@ -104,13 +104,13 @@ public void Test2() var collectionName = "medicalRecords.patients"; var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() - .PatternProperties("_PIIString$", bsonType: BsonType.String, + .PatternProperty("_PIIString$", bsonType: BsonType.String, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - .PatternProperties("_PIIArray$", bsonType: BsonType.Array, + .PatternProperty("_PIIArray$", bsonType: BsonType.Array, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .PatternProperties(p => p.Insurance, builder => builder - .PatternProperties("_PIINumber$", bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - .PatternProperties("_PIIString$", bsonType: BsonType.String, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperty(p => p.Insurance, builder => builder + .PatternProperty("_PIINumber$", bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperty("_PIIString$", bsonType: BsonType.String, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) ); var encryptionSchemaBuilder = new CsfleSchemaBuilder() From dd1aeaf44978a10e3ea734ebc4503098e7c68635 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:55:38 +0100 Subject: [PATCH 11/38] Several improvements --- .../Encryption/CsfleSchemaBuilder.cs | 83 ++- .../Encryption/CsfleSchemaBuilderTests.cs | 620 +++++++++++++++--- 2 files changed, 581 insertions(+), 122 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index bdb6264e645..f1afea52961 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -36,12 +36,12 @@ public class CsfleSchemaBuilder /// /// /// - public static CsfleTypeSchemaBuilder GetTypeBuilder() => new(); + public static CsfleTypeSchemaBuilder GetTypeBuilder() => new(); //TODO Do we need this? /// /// /// - /// + /// The namespace to which the encryption schema applies. /// /// /// @@ -54,7 +54,7 @@ public CsfleSchemaBuilder WithType(CollectionNamespace collectionNamespace, C /// /// /// - /// + /// The namespace to which the encryption schema applies. /// /// /// @@ -98,13 +98,18 @@ public class CsfleTypeSchemaBuilder : CsfleTypeSchemaBuilder /// /// /// - /// - /// - /// - /// + /// The field to be encrypted. + /// The id of the Data Encryption Key to use for encrypting. + /// The encryption algorithm to use. + /// The BSON type of the field being encrypted. /// public CsfleTypeSchemaBuilder Property(FieldDefinition path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + _fields.Add(new SchemaSimpleField(path, keyId, algorithm, bsonType)); return this; } @@ -112,10 +117,10 @@ public CsfleTypeSchemaBuilder Property(FieldDefinition pat /// /// /// - /// - /// - /// - /// + /// The field to be encrypted. + /// The id of the Data Encryption Key to use for encrypting. + /// The encryption algorithm to use. + /// The BSON type of the field being encrypted. /// /// public CsfleTypeSchemaBuilder Property(Expression> path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) @@ -126,12 +131,22 @@ public CsfleTypeSchemaBuilder Property(Expression /// /// - /// + /// The field to use for the nested property. /// /// /// public CsfleTypeSchemaBuilder Property(FieldDefinition path, Action> configure) { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + _fields.Add(new SchemaNestedField(path, configure)); return this; } @@ -139,7 +154,7 @@ public CsfleTypeSchemaBuilder Property(FieldDefinition /// /// - /// + /// The field to be encrypted. /// /// /// @@ -151,13 +166,18 @@ public CsfleTypeSchemaBuilder Property(Expression /// /// - /// - /// - /// - /// + /// The pattern to use. + /// The id of the Data Encryption Key to use for encrypting. + /// The encryption algorithm to use. + /// The BSON type of the field being encrypted. /// - public CsfleTypeSchemaBuilder PatternProperty(string pattern, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) //TODO This is not correct, + public CsfleTypeSchemaBuilder PatternProperty(string pattern, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) { + if (string.IsNullOrWhiteSpace(pattern)) + { + throw new ArgumentException("Input pattern cannot be empty or null", nameof(pattern)); + } + _patterns.Add(new SchemaSimplePattern(pattern, keyId, algorithm, bsonType)); return this; } @@ -165,12 +185,22 @@ public CsfleTypeSchemaBuilder PatternProperty(string pattern, Guid? k /// /// /// - /// + /// The field to use for the nested pattern property. /// /// /// public CsfleTypeSchemaBuilder PatternProperty(FieldDefinition path, Action> configure) { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + _patterns.Add(new SchemaNestedPattern(path, configure)); return this; } @@ -178,7 +208,7 @@ public CsfleTypeSchemaBuilder PatternProperty(FieldDefinition /// /// /// - /// + /// The field to use for the nested pattern property. /// /// /// @@ -190,8 +220,8 @@ public CsfleTypeSchemaBuilder PatternProperty(Expression /// /// - /// - /// + /// The id of the Data Encryption Key to use for encrypting. + /// The encryption algorithm to use. /// public CsfleTypeSchemaBuilder EncryptMetadata(Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null ) { @@ -248,7 +278,7 @@ private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorith }; } - private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstTypeFilterOperation + private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstTypeFilterOperation, do we have a common place where this could go? { return type switch { @@ -344,16 +374,17 @@ keyId is not null } /// - /// + /// The type of possible encryption algorithms. /// public enum CsfleEncryptionAlgorithm { /// - /// + /// Randomized encryption algorithm. /// AEAD_AES_256_CBC_HMAC_SHA_512_Random, + /// - /// + /// Deterministic encryption algorithm. /// AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic } diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index e1200bfdc61..48a80bf18c2 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -25,14 +25,434 @@ namespace MongoDB.Driver.Tests.Encryption { public class CsfleSchemaBuilderTests { + private readonly Guid _keyIdExample = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); + + [Fact] + public void TypedSchemaBuilder_Property_throws_when_path_is_null() + { + var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().Property(null)); + + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void TypedSchemaBuilder_PropertyWithConfigure_throws_when_path_is_null() + { + Action> configure = b => { }; + var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().Property((FieldDefinition)null, configure)); + + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void TypedSchemaBuilder_PropertyWithConfigure_throws_when_configure_is_null() + { + Action> configure = null; + var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().Property("path", configure)); + + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void TypedSchemaBuilder_PatternProperty_throws_when_pattern_is_null() + { + var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().PatternProperty(null)); + + Assert.NotNull(exception); + Assert.IsType(exception); + } + [Fact] - public void Test1() + public void TypedSchemaBuilder_PatternPropertyWithConfigure_throws_when_pattern_is_null() + { + Action> configure = b => { }; + var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().PatternProperty((FieldDefinition)null, configure)); + + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void TypedSchemaBuilder_PatternPropertyWithConfigure_throws_when_configure_is_null() + { + Action> configure = null; + var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().PatternProperty("path", configure)); + + Assert.NotNull(exception); + Assert.IsType(exception); + } + + [Fact] + public void TypedSchemaBuilder_Property_works_as_expected() + { + var builder = new CsfleTypeSchemaBuilder() + .Property("bloodType", keyId: _keyIdExample, bsonType: BsonType.String, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + + var expected = """ + { + "bsonType": "object", + "properties": { + "bloodType": { + "encrypt": { + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" + "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] + } + } + } + } + """; + + AssertOutcomeTypeBuilder(builder, expected); + } + + [Fact] + public void TypedSchemaBuilder_PropertyWithExpression_works_as_expected() + { + var builder = new CsfleTypeSchemaBuilder() + .Property(p => p.BloodType, keyId: _keyIdExample, bsonType: BsonType.String, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + + var expected = """ + { + "bsonType": "object", + "properties": { + "bloodType": { + "encrypt": { + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" + "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] + } + } + } + } + """; + + AssertOutcomeTypeBuilder(builder, expected); + } + + [Fact] + public void TypedSchemaBuilder_PropertyWithConfigure_works_as_expected() + { + var builder = new CsfleTypeSchemaBuilder() + .Property("insurance", insurance => insurance + .Property("policyNumber", bsonType: BsonType.Int32, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, + keyId: _keyIdExample)); + + var expected = """ + { + "bsonType": "object", + "properties": { + "insurance": { + "bsonType": "object", + "properties": { + "policyNumber": { + "encrypt": { + "bsonType": "int", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] + } + } + } + } + } + } + """; + + AssertOutcomeTypeBuilder(builder, expected); + } + + [Fact] + public void TypedSchemaBuilder_PropertyWithExpressionAndConfigure_works_as_expected() + { + var builder = new CsfleTypeSchemaBuilder() + .Property(p => p.Insurance, insurance => insurance + .Property("policyNumber", bsonType: BsonType.Int32, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, + keyId: _keyIdExample)); + + var expected = """ + { + "bsonType": "object", + "properties": { + "insurance": { + "bsonType": "object", + "properties": { + "policyNumber": { + "encrypt": { + "bsonType": "int", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] + } + } + } + } + } + } + """; + + AssertOutcomeTypeBuilder(builder, expected); + } + + [Fact] + public void TypedSchemaBuilder_PatternProperty_works_as_expected() + { + var builder = new CsfleTypeSchemaBuilder() + .PatternProperty("_PIIString$", bsonType: BsonType.String, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, + keyId: _keyIdExample); + + var expected = """ + { + "bsonType": "object", + "patternProperties": { + "_PIIString$": { + "encrypt": { + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] + } + } + } + } + """; + + AssertOutcomeTypeBuilder(builder, expected); + } + + [Fact] + public void TypedSchemaBuilder_PatternPropertyWithConfigure_works_as_expected() + { + var builder = new CsfleTypeSchemaBuilder() + .PatternProperty("insurance", builder => builder + .PatternProperty("_PIINumber$", bsonType: BsonType.Int32, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)); + + var expected = """ + { + "bsonType": "object", + "patternProperties": { + "insurance": { + "bsonType": "object", + "patternProperties": { + "_PIINumber$": { + "encrypt": { + "bsonType": "int", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + }, + }, + }, + } + } + } + """; + + AssertOutcomeTypeBuilder(builder, expected); + } + + [Fact] + public void TypedSchemaBuilder_PatternPropertyWithExpressionAndConfigure_works_as_expected() + { + var builder = new CsfleTypeSchemaBuilder() + .PatternProperty(p=> p.Insurance, builder => builder + .PatternProperty("_PIINumber$", bsonType: BsonType.Int32, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)); + + var expected = """ + { + "bsonType": "object", + "patternProperties": { + "insurance": { + "bsonType": "object", + "patternProperties": { + "_PIINumber$": { + "encrypt": { + "bsonType": "int", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + }, + }, + }, + } + } + } + """; + + AssertOutcomeTypeBuilder(builder, expected); + } + + [Fact] + public void TypedSchemaBuilder_EncryptMetadata_works_as_expected() + { + var builder = new CsfleTypeSchemaBuilder() + .EncryptMetadata(keyId: _keyIdExample, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + + var expected = """ + { + "bsonType": "object", + "encryptMetadata": { + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] + }, + } + """; + + AssertOutcomeTypeBuilder(builder, expected); + } + + [Fact] + public void SchemaBuilder_WithType_works_as_expected() + { + const string collectionName1 = "medicalRecords.patients"; + const string collectionName2 = "test.collection"; + + var typedBuilder1 = new CsfleTypeSchemaBuilder() + .EncryptMetadata(keyId: _keyIdExample, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + var typeBuilder2 = new CsfleTypeSchemaBuilder() + .EncryptMetadata(algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + + var encryptionSchemaBuilder = new CsfleSchemaBuilder() + .WithType(CollectionNamespace.FromFullName(collectionName1), typedBuilder1) + .WithType(CollectionNamespace.FromFullName(collectionName2), typeBuilder2); + + var expected = new Dictionary + { + [collectionName1] = """ + { + "bsonType": "object", + "encryptMetadata": { + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] + }, + } + """, + [collectionName2] = """ + { + "bsonType": "object", + "encryptMetadata": { + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", + }, + } + """ + }; + + AssertOutcomeBuilder(encryptionSchemaBuilder, expected); + } + + [Fact] + public void SchemaBuilder_WithTypeAndConfigure_works_as_expected() + { + const string collectionName1 = "medicalRecords.patients"; + const string collectionName2 = "test.collection"; + + var encryptionSchemaBuilder = new CsfleSchemaBuilder() + .WithType(CollectionNamespace.FromFullName(collectionName1), b => b.EncryptMetadata(keyId: _keyIdExample, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)) + .WithType(CollectionNamespace.FromFullName(collectionName2), b => b.EncryptMetadata(algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random)); + + var expected = new Dictionary + { + [collectionName1] = """ + { + "bsonType": "object", + "encryptMetadata": { + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] + }, + } + """, + [collectionName2] = """ + { + "bsonType": "object", + "encryptMetadata": { + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", + }, + } + """ + }; + + AssertOutcomeBuilder(encryptionSchemaBuilder, expected); + } + + [Fact] + public void SchemaBuilder_CompleteExampleWithBsonDocument_work_as_expected() { - var myKeyId = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); var collectionName = "medicalRecords.patients"; + var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() + .EncryptMetadata(keyId: _keyIdExample) + .Property("insurance", insurance => insurance + .Property("policyNumber", bsonType: BsonType.Int32, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)) + .Property("medicalRecords", bsonType: BsonType.Array, + algorithm: CsfleEncryptionAlgorithm + .AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .Property("bloodType", bsonType: BsonType.String, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .Property("ssn", bsonType: BsonType.Int32, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + + var encryptionSchemaBuilder = new CsfleSchemaBuilder() + .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); + + var expected = new Dictionary + { + [collectionName] = """ + { + "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" + } + } + }, + } + """ + }; + + AssertOutcomeBuilder(encryptionSchemaBuilder, expected); + } + + [Fact] + public void SchemaBuilder_CompleteExample_work_as_expected() + { + const string collectionName = "medicalRecords.patients"; + var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() - .EncryptMetadata(keyId: myKeyId) + .EncryptMetadata(keyId: _keyIdExample) .Property(p => p.Insurance, insurance => insurance .Property(i => i.PolicyNumber, bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)) @@ -47,61 +467,56 @@ public void Test1() var encryptionSchemaBuilder = new CsfleSchemaBuilder() .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); - const string expected = """ - { - "medicalRecords.patients": { - "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" - } - } - }, - } - } - """; - var parsedExpected = BsonDocument.Parse(expected); - - var builtSchema = encryptionSchemaBuilder.Build(); - Assert.Equal(parsedExpected.Count(), builtSchema.Count); - foreach (var name in parsedExpected.Names) + var expected = new Dictionary { - Assert.Equal(parsedExpected[name].AsBsonDocument, builtSchema[name]); - } + [collectionName] = """ + { + "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" + } + } + }, + } + """ + }; + + AssertOutcomeBuilder(encryptionSchemaBuilder, expected); } [Fact] - public void Test2() + public void SchemaBuilder_CompleteExampleWithPatternProperties_work_as_expected() { - var collectionName = "medicalRecords.patients"; + const string collectionName = "medicalRecords.patients"; var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() .PatternProperty("_PIIString$", bsonType: BsonType.String, @@ -116,55 +531,68 @@ public void Test2() var encryptionSchemaBuilder = new CsfleSchemaBuilder() .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); - const string expected = """ - { - "medicalRecords.patients": { - "bsonType": "object", - "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", - }, - }, - }, - }, - }, - }, - } - """; + var expected = new Dictionary + { + [collectionName] = """ + { + "bsonType": "object", + "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", + }, + }, + }, + }, + }, + } + """ + }; + + AssertOutcomeBuilder(encryptionSchemaBuilder, expected); + } + + private void AssertOutcomeTypeBuilder(CsfleTypeSchemaBuilder builder, string expected) + { var parsedExpected = BsonDocument.Parse(expected); + var builtSchema = builder.Build(); - var builtSchema = encryptionSchemaBuilder.Build(); - Assert.Equal(parsedExpected.Count(), builtSchema.Count); - foreach (var name in parsedExpected.Names) + Assert.Equal(parsedExpected, builtSchema); + } + + private void AssertOutcomeBuilder(CsfleSchemaBuilder builder, Dictionary expected) + { + var builtSchema = builder.Build(); + Assert.Equal(expected.Count, builtSchema.Count); + foreach (var collectionNamespace in expected.Keys) { - Assert.Equal(parsedExpected[name].AsBsonDocument, builtSchema[name]); + var parsed = BsonDocument.Parse(expected[collectionNamespace]); + Assert.Equal(parsed, builtSchema[collectionNamespace]); } } - // Taken from the docs internal class Patient { [BsonId] From f21b87a7e8f8eca544c7cee45ffc5ae852998f69 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:13:39 +0100 Subject: [PATCH 12/38] Removed unused --- tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 48a80bf18c2..90ffc018603 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -15,7 +15,6 @@ using System; using System.Collections.Generic; -using System.Linq; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver.Encryption; From f88465cd60e844d5dc744b396451c850dc6a5912 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 20 Mar 2025 20:15:39 +0100 Subject: [PATCH 13/38] First improvement --- .../Encryption/CsfleSchemaBuilder.cs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index f1afea52961..a3e10f7f30c 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -23,6 +23,7 @@ namespace MongoDB.Driver.Encryption { //TODO Add docs + //TODO BsonType can be multiple types in some cases /// /// @@ -373,6 +374,92 @@ keyId is not null } } + public class ElementBuilder where TSelf : ElementBuilder + { + private CsfleEncryptionAlgorithm _algorithm; + private Guid _keyId; + + public TSelf WithKeyId(Guid keyId) + { + _keyId = keyId; + return (TSelf)this; + } + + public TSelf WithAlgorithm(CsfleEncryptionAlgorithm algorithm) + { + _algorithm = algorithm; + return (TSelf)this; + } + } + + public class EncryptMetadataBuilder : ElementBuilder + { + + } + + public abstract class PropertyBuilder: ElementBuilder> + { + } + + public class PropertyBuilder : PropertyBuilder + { + private readonly FieldDefinition _path; + private List _bsonTypes; + + public PropertyBuilder(FieldDefinition path) + { + _path = path; + } + + public PropertyBuilder WithBsonType(BsonType bsonType) + { + _bsonTypes = [bsonType]; + return this; + } + + public PropertyBuilder WithBsonTypes(IEnumerable bsonTypes) + { + _bsonTypes = [..bsonTypes]; + return this; + } + } + + public abstract class NestedDocumentBuilder: ElementBuilder + { + } + + public class NestedDocumentBuilder : NestedDocumentBuilder + { + private readonly FieldDefinition _path; + private readonly Action> _configure; + + public NestedDocumentBuilder(FieldDefinition path, Action> configure) + { + _path = path; + _configure = configure; + } + } + + public class TypedBuilder + { + private readonly List _nestedDocument = []; + private readonly List _properties = []; + private EncryptMetadataBuilder _metadata; + + public EncryptMetadataBuilder EncryptMetadata() + { + _metadata = new EncryptMetadataBuilder(); + return _metadata; + } + + public PropertyBuilder Property(FieldDefinition path) + { + var property = new PropertyBuilder(path); + _properties.Add(property); + return property; + } + } + /// /// The type of possible encryption algorithms. /// From 33fcbeb2d86cb1a7153854909236b0913473816f Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:58:10 +0200 Subject: [PATCH 14/38] Removed old things --- .../Encryption/CsfleSchemaBuilder.cs | 553 ++++++++-------- .../Encryption/CsfleSchemaBuilderTests.cs | 595 +++--------------- 2 files changed, 355 insertions(+), 793 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index a3e10f7f30c..00f28bc2b48 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -22,260 +22,92 @@ namespace MongoDB.Driver.Encryption { - //TODO Add docs - //TODO BsonType can be multiple types in some cases - /// /// /// public class CsfleSchemaBuilder { - private readonly Dictionary _typeSchemaBuilders = new(); - - /// - /// - /// - /// - /// - public static CsfleTypeSchemaBuilder GetTypeBuilder() => new(); //TODO Do we need this? + private readonly Dictionary _typedBuilders = []; + private CsfleSchemaBuilder() + { + } /// /// /// - /// The namespace to which the encryption schema applies. - /// - /// + /// /// - public CsfleSchemaBuilder WithType(CollectionNamespace collectionNamespace, CsfleTypeSchemaBuilder typedBuilder) + public static CsfleSchemaBuilder Create(Action configure) { - _typeSchemaBuilders.Add(collectionNamespace.FullName, typedBuilder); - return this; + var builder = new CsfleSchemaBuilder(); + configure(builder); + return builder; } /// /// /// - /// The namespace to which the encryption schema applies. + /// /// - /// - /// - public CsfleSchemaBuilder WithType(CollectionNamespace collectionNamespace, Action> configure) //TODO Do we want to keep this? + /// + public void Encrypt(CollectionNamespace collectionNamespace, Action> configure) { - var typedBuilder = new CsfleTypeSchemaBuilder(); + var typedBuilder = new TypedBuilder(); configure(typedBuilder); - _typeSchemaBuilders.Add(collectionNamespace.FullName, typedBuilder); - return this; + _typedBuilders.Add(collectionNamespace.FullName, typedBuilder); } /// /// /// /// - public IReadOnlyDictionary Build() => _typeSchemaBuilders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Build()); - } - - /// - /// - /// - public abstract class CsfleTypeSchemaBuilder - { - /// - /// - /// - /// - public abstract BsonDocument Build(); + public IReadOnlyDictionary Build() => _typedBuilders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Build()); } /// /// /// - /// - public class CsfleTypeSchemaBuilder : CsfleTypeSchemaBuilder + /// + public class ElementBuilder where TSelf : ElementBuilder { - private readonly List _fields = []; - private readonly List _patterns = []; - private SchemaMetadata _metadata; - - /// - /// - /// - /// The field to be encrypted. - /// The id of the Data Encryption Key to use for encrypting. - /// The encryption algorithm to use. - /// The BSON type of the field being encrypted. - /// - public CsfleTypeSchemaBuilder Property(FieldDefinition path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - _fields.Add(new SchemaSimpleField(path, keyId, algorithm, bsonType)); - return this; - } - - /// - /// - /// - /// The field to be encrypted. - /// The id of the Data Encryption Key to use for encrypting. - /// The encryption algorithm to use. - /// The BSON type of the field being encrypted. - /// - /// - public CsfleTypeSchemaBuilder Property(Expression> path, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) - { - return Property(new ExpressionFieldDefinition(path), keyId, algorithm, bsonType); - } - - /// - /// - /// - /// The field to use for the nested property. - /// - /// - /// - public CsfleTypeSchemaBuilder Property(FieldDefinition path, Action> configure) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - if (configure == null) - { - throw new ArgumentNullException(nameof(configure)); - } - - _fields.Add(new SchemaNestedField(path, configure)); - return this; - } - - /// - /// - /// - /// The field to be encrypted. - /// - /// - /// - public CsfleTypeSchemaBuilder Property(Expression> path, Action> configure) - { - return Property(new ExpressionFieldDefinition(path), configure); - } - - /// - /// - /// - /// The pattern to use. - /// The id of the Data Encryption Key to use for encrypting. - /// The encryption algorithm to use. - /// The BSON type of the field being encrypted. - /// - public CsfleTypeSchemaBuilder PatternProperty(string pattern, Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null, BsonType? bsonType = null) - { - if (string.IsNullOrWhiteSpace(pattern)) - { - throw new ArgumentException("Input pattern cannot be empty or null", nameof(pattern)); - } - - _patterns.Add(new SchemaSimplePattern(pattern, keyId, algorithm, bsonType)); - return this; - } - - /// - /// - /// - /// The field to use for the nested pattern property. - /// - /// - /// - public CsfleTypeSchemaBuilder PatternProperty(FieldDefinition path, Action> configure) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - if (configure == null) - { - throw new ArgumentNullException(nameof(configure)); - } - - _patterns.Add(new SchemaNestedPattern(path, configure)); - return this; - } + internal CsfleEncryptionAlgorithm? _algorithm; //TODO These should be protected as well + internal Guid? _keyId; /// /// /// - /// The field to use for the nested pattern property. - /// - /// + /// /// - public CsfleTypeSchemaBuilder PatternProperty(Expression> path, Action> configure) + public TSelf WithKeyId(Guid keyId) { - return PatternProperty(new ExpressionFieldDefinition(path), configure); + _keyId = keyId; + return (TSelf)this; } /// /// /// - /// The id of the Data Encryption Key to use for encrypting. - /// The encryption algorithm to use. + /// /// - public CsfleTypeSchemaBuilder EncryptMetadata(Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null ) + public TSelf WithAlgorithm(CsfleEncryptionAlgorithm algorithm) { - _metadata = new SchemaMetadata(keyId, algorithm); - return this; + _algorithm = algorithm; + return (TSelf)this; } - /// - public override BsonDocument Build() + internal static BsonDocument GetEncryptBsonDocument(Guid? keyId, CsfleEncryptionAlgorithm? algorithm, List bsonTypes) { - var schema = new BsonDocument("bsonType", "object"); + var bsonType = bsonTypes?.First(); //TODO need to support multiple types - if (_metadata is not null) - { - schema.Merge(_metadata.Build()); - } - - var args = new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); - - if (_fields.Any()) - { - var properties = new BsonDocument(); - - foreach (var field in _fields) - { - properties.Merge(field.Build(args)); - } - - schema.Add("properties", properties); - } - - if (_patterns.Any()) + return new BsonDocument { - var patternProperties = new BsonDocument(); - - foreach (var pattern in _patterns) + { "bsonType", () => MapBsonTypeToString(bsonType!.Value), bsonType is not null }, + { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null }, { - patternProperties.Merge(pattern.Build(args)); - } - - schema.Add("patternProperties", patternProperties); - } - - return schema; - } - - private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorithm algorithm) - { - return algorithm switch - { - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random => "AEAD_AES_256_CBC_HMAC_SHA_512-Random", - CsfleEncryptionAlgorithm.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)) + "keyId", + () => new BsonArray(new[] { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }), + keyId is not null + }, }; } @@ -307,157 +139,308 @@ private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorith }; } - private abstract record SchemaField - { - public abstract BsonDocument Build(RenderArgs args); - } - - private record SchemaSimpleField(FieldDefinition Path, Guid? KeyId, CsfleEncryptionAlgorithm? Algorithm, BsonType? BsonType) : SchemaField - { - public override BsonDocument Build(RenderArgs args) => - new(Path.Render(args).FieldName, new BsonDocument("encrypt", GetEncryptBsonDocument(KeyId, Algorithm, BsonType))); - } - - private record SchemaNestedField(FieldDefinition Path, Action> Configure) : SchemaField + private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorithm algorithm) { - public override BsonDocument Build(RenderArgs args) + return algorithm switch { - var fieldBuilder = new CsfleTypeSchemaBuilder(); - Configure(fieldBuilder); - return new BsonDocument(Path.Render(args).FieldName, fieldBuilder.Build()); - } + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random => "AEAD_AES_256_CBC_HMAC_SHA_512-Random", + CsfleEncryptionAlgorithm.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)) + }; } + } - private abstract record SchemaPattern - { - public abstract BsonDocument Build(RenderArgs args); - } + /// + /// + /// + public class EncryptMetadataBuilder : ElementBuilder + { + internal BsonDocument Build() => new("encryptMetadata", GetEncryptBsonDocument(_keyId, _algorithm, null)); + } - private record SchemaSimplePattern( - string Pattern, - Guid? KeyId, - CsfleEncryptionAlgorithm? Algorithm, - BsonType? BsonType) : SchemaPattern + /// + /// + /// + /// + public class PropertyBuilder : ElementBuilder> //TODO Maybe we can have a common class for this and patternPropertyBuilder + { + private readonly FieldDefinition _path; + private List _bsonTypes; + + /// + /// + /// + /// + public PropertyBuilder(FieldDefinition path) { - public override BsonDocument Build(RenderArgs args) => new(Pattern, new BsonDocument("encrypt", GetEncryptBsonDocument(KeyId, Algorithm, BsonType))); + _path = path; } - private record SchemaNestedPattern( - FieldDefinition Path, - Action> Configure) : SchemaPattern + /// + /// + /// + /// + /// + public PropertyBuilder WithBsonType(BsonType bsonType) { - public override BsonDocument Build(RenderArgs args) - { - var fieldBuilder = new CsfleTypeSchemaBuilder(); - Configure(fieldBuilder); - return new BsonDocument(Path.Render(args).FieldName, fieldBuilder.Build()); - } + _bsonTypes = [bsonType]; + return this; } - private record SchemaMetadata(Guid? KeyId, CsfleEncryptionAlgorithm? Algorithm) + /// + /// + /// + /// + /// + public PropertyBuilder WithBsonTypes(IEnumerable bsonTypes) { - public BsonDocument Build() => new("encryptMetadata", GetEncryptBsonDocument(KeyId, Algorithm, null)); + _bsonTypes = [..bsonTypes]; + return this; } - private static BsonDocument GetEncryptBsonDocument(Guid? keyId, CsfleEncryptionAlgorithm? algorithm, BsonType? bsonType) + internal BsonDocument Build(RenderArgs args) { - return new BsonDocument - { - { "bsonType", () => MapBsonTypeToString(bsonType!.Value), bsonType is not null }, - { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null }, - { - "keyId", - () => new BsonArray(new[] { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }), - keyId is not null - }, - }; + return new BsonDocument(_path.Render(args).FieldName, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes))); } } - public class ElementBuilder where TSelf : ElementBuilder + /// + /// + /// + /// + public class PatternPropertyBuilder : ElementBuilder> { - private CsfleEncryptionAlgorithm _algorithm; - private Guid _keyId; + private readonly string _pattern; + private List _bsonTypes; - public TSelf WithKeyId(Guid keyId) + /// + /// + /// + /// + public PatternPropertyBuilder(string pattern) { - _keyId = keyId; - return (TSelf)this; + _pattern = pattern; } - public TSelf WithAlgorithm(CsfleEncryptionAlgorithm algorithm) + /// + /// + /// + /// + /// + public PatternPropertyBuilder WithBsonType(BsonType bsonType) { - _algorithm = algorithm; - return (TSelf)this; + _bsonTypes = [bsonType]; + return this; } - } - public class EncryptMetadataBuilder : ElementBuilder - { + /// + /// + /// + /// + /// + public PatternPropertyBuilder WithBsonTypes(IEnumerable bsonTypes) + { + _bsonTypes = [..bsonTypes]; + return this; + } + internal BsonDocument Build(RenderArgs args) + { + return new BsonDocument(_pattern, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes))); + } } - public abstract class PropertyBuilder: ElementBuilder> + /// + /// + /// + public abstract class NestedPropertyBuilder { + internal abstract BsonDocument Build(RenderArgs args); } - public class PropertyBuilder : PropertyBuilder + /// + /// + /// + /// + /// + public class NestedPropertyBuilder : NestedPropertyBuilder { private readonly FieldDefinition _path; - private List _bsonTypes; + private readonly Action> _configure; - public PropertyBuilder(FieldDefinition path) + /// + /// + /// + /// + /// + public NestedPropertyBuilder(FieldDefinition path, Action> configure) { _path = path; + _configure = configure; } - public PropertyBuilder WithBsonType(BsonType bsonType) - { - _bsonTypes = [bsonType]; - return this; - } - - public PropertyBuilder WithBsonTypes(IEnumerable bsonTypes) + internal override BsonDocument Build(RenderArgs args) { - _bsonTypes = [..bsonTypes]; - return this; + var fieldBuilder = new TypedBuilder(); + _configure(fieldBuilder); + return new BsonDocument(_path.Render(args).FieldName, fieldBuilder.Build()); } } - public abstract class NestedDocumentBuilder: ElementBuilder + + /// + /// + /// + public abstract class NestedPatternPropertyBuilder { + internal abstract BsonDocument Build(RenderArgs args); } - public class NestedDocumentBuilder : NestedDocumentBuilder + /// + /// + /// + /// + /// + public class NestedPatternPropertyBuilder : NestedPatternPropertyBuilder { private readonly FieldDefinition _path; private readonly Action> _configure; - public NestedDocumentBuilder(FieldDefinition path, Action> configure) + /// + /// + /// + /// + /// + public NestedPatternPropertyBuilder(FieldDefinition path, Action> configure) { _path = path; _configure = configure; } + + internal override BsonDocument Build(RenderArgs args) + { + var fieldBuilder = new TypedBuilder(); + _configure(fieldBuilder); + return new BsonDocument(_path.Render(args).FieldName, fieldBuilder.Build()); + } } - public class TypedBuilder + + /// + /// + /// + public abstract class TypedBuilder { - private readonly List _nestedDocument = []; - private readonly List _properties = []; + internal abstract BsonDocument Build(); + } + + /// + /// + /// + /// + public class TypedBuilder : TypedBuilder + { + private readonly List> _nestedProperties = []; + private readonly List> _properties = []; private EncryptMetadataBuilder _metadata; + /// + /// + /// + /// public EncryptMetadataBuilder EncryptMetadata() { _metadata = new EncryptMetadataBuilder(); return _metadata; } - public PropertyBuilder Property(FieldDefinition path) + /// + /// + /// + /// + /// + public PropertyBuilder Property(FieldDefinition path) { var property = new PropertyBuilder(path); _properties.Add(property); return property; } + + /// + /// + /// + /// + /// + public PropertyBuilder Property(Expression> path) + { + return Property(new ExpressionFieldDefinition(path)); + } + + /// + /// + /// + /// + /// + /// + public NestedPropertyBuilder NestedProperty(FieldDefinition path, Action> configure) + { + var nestedProperty = new NestedPropertyBuilder(path, configure); + _nestedProperties.Add(nestedProperty); + return nestedProperty; + } + + /// + /// + /// + /// + /// + /// + public NestedPropertyBuilder NestedProperty(Expression> path, Action> configure) + { + return NestedProperty(new ExpressionFieldDefinition(path), configure); + } + + internal override BsonDocument Build() + { + var schema = new BsonDocument("bsonType", "object"); + + if (_metadata is not null) + { + schema.Merge(_metadata.Build()); + } + + var args = new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); + + + BsonDocument properties = null; + + if (_nestedProperties.Any()) + { + properties = new BsonDocument(); + + foreach (var nestedProperty in _nestedProperties) + { + properties.Merge(nestedProperty.Build(args)); + } + } + + if (_properties.Any()) + { + properties ??= new BsonDocument(); + + foreach (var property in _properties) + { + properties.Merge(property.Build(args)); + } + } + + if (properties != null) + { + schema.Add("properties", properties); + } + + return schema; + } } /// diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 90ffc018603..54b7a4ea865 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -27,444 +27,30 @@ public class CsfleSchemaBuilderTests private readonly Guid _keyIdExample = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); [Fact] - public void TypedSchemaBuilder_Property_throws_when_path_is_null() - { - var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().Property(null)); - - Assert.NotNull(exception); - Assert.IsType(exception); - } - - [Fact] - public void TypedSchemaBuilder_PropertyWithConfigure_throws_when_path_is_null() - { - Action> configure = b => { }; - var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().Property((FieldDefinition)null, configure)); - - Assert.NotNull(exception); - Assert.IsType(exception); - } - - [Fact] - public void TypedSchemaBuilder_PropertyWithConfigure_throws_when_configure_is_null() - { - Action> configure = null; - var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().Property("path", configure)); - - Assert.NotNull(exception); - Assert.IsType(exception); - } - - [Fact] - public void TypedSchemaBuilder_PatternProperty_throws_when_pattern_is_null() - { - var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().PatternProperty(null)); - - Assert.NotNull(exception); - Assert.IsType(exception); - } - - [Fact] - public void TypedSchemaBuilder_PatternPropertyWithConfigure_throws_when_pattern_is_null() - { - Action> configure = b => { }; - var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().PatternProperty((FieldDefinition)null, configure)); - - Assert.NotNull(exception); - Assert.IsType(exception); - } - - [Fact] - public void TypedSchemaBuilder_PatternPropertyWithConfigure_throws_when_configure_is_null() - { - Action> configure = null; - var exception = Record.Exception(() => new CsfleTypeSchemaBuilder().PatternProperty("path", configure)); - - Assert.NotNull(exception); - Assert.IsType(exception); - } - - [Fact] - public void TypedSchemaBuilder_Property_works_as_expected() - { - var builder = new CsfleTypeSchemaBuilder() - .Property("bloodType", keyId: _keyIdExample, bsonType: BsonType.String, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); - - var expected = """ - { - "bsonType": "object", - "properties": { - "bloodType": { - "encrypt": { - "bsonType": "string", - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" - "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] - } - } - } - } - """; - - AssertOutcomeTypeBuilder(builder, expected); - } - - [Fact] - public void TypedSchemaBuilder_PropertyWithExpression_works_as_expected() - { - var builder = new CsfleTypeSchemaBuilder() - .Property(p => p.BloodType, keyId: _keyIdExample, bsonType: BsonType.String, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); - - var expected = """ - { - "bsonType": "object", - "properties": { - "bloodType": { - "encrypt": { - "bsonType": "string", - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" - "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] - } - } - } - } - """; - - AssertOutcomeTypeBuilder(builder, expected); - } - - [Fact] - public void TypedSchemaBuilder_PropertyWithConfigure_works_as_expected() - { - var builder = new CsfleTypeSchemaBuilder() - .Property("insurance", insurance => insurance - .Property("policyNumber", bsonType: BsonType.Int32, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, - keyId: _keyIdExample)); - - var expected = """ - { - "bsonType": "object", - "properties": { - "insurance": { - "bsonType": "object", - "properties": { - "policyNumber": { - "encrypt": { - "bsonType": "int", - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" - "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] - } - } - } - } - } - } - """; - - AssertOutcomeTypeBuilder(builder, expected); - } - - [Fact] - public void TypedSchemaBuilder_PropertyWithExpressionAndConfigure_works_as_expected() - { - var builder = new CsfleTypeSchemaBuilder() - .Property(p => p.Insurance, insurance => insurance - .Property("policyNumber", bsonType: BsonType.Int32, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, - keyId: _keyIdExample)); - - var expected = """ - { - "bsonType": "object", - "properties": { - "insurance": { - "bsonType": "object", - "properties": { - "policyNumber": { - "encrypt": { - "bsonType": "int", - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" - "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] - } - } - } - } - } - } - """; - - AssertOutcomeTypeBuilder(builder, expected); - } - - [Fact] - public void TypedSchemaBuilder_PatternProperty_works_as_expected() - { - var builder = new CsfleTypeSchemaBuilder() - .PatternProperty("_PIIString$", bsonType: BsonType.String, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, - keyId: _keyIdExample); - - var expected = """ - { - "bsonType": "object", - "patternProperties": { - "_PIIString$": { - "encrypt": { - "bsonType": "string", - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" - "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] - } - } - } - } - """; - - AssertOutcomeTypeBuilder(builder, expected); - } - - [Fact] - public void TypedSchemaBuilder_PatternPropertyWithConfigure_works_as_expected() - { - var builder = new CsfleTypeSchemaBuilder() - .PatternProperty("insurance", builder => builder - .PatternProperty("_PIINumber$", bsonType: BsonType.Int32, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)); - - var expected = """ - { - "bsonType": "object", - "patternProperties": { - "insurance": { - "bsonType": "object", - "patternProperties": { - "_PIINumber$": { - "encrypt": { - "bsonType": "int", - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", - }, - }, - }, - } - } - } - """; - - AssertOutcomeTypeBuilder(builder, expected); - } - - [Fact] - public void TypedSchemaBuilder_PatternPropertyWithExpressionAndConfigure_works_as_expected() - { - var builder = new CsfleTypeSchemaBuilder() - .PatternProperty(p=> p.Insurance, builder => builder - .PatternProperty("_PIINumber$", bsonType: BsonType.Int32, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)); - - var expected = """ - { - "bsonType": "object", - "patternProperties": { - "insurance": { - "bsonType": "object", - "patternProperties": { - "_PIINumber$": { - "encrypt": { - "bsonType": "int", - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", - }, - }, - }, - } - } - } - """; - - AssertOutcomeTypeBuilder(builder, expected); - } - - [Fact] - public void TypedSchemaBuilder_EncryptMetadata_works_as_expected() - { - var builder = new CsfleTypeSchemaBuilder() - .EncryptMetadata(keyId: _keyIdExample, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - - var expected = """ - { - "bsonType": "object", - "encryptMetadata": { - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", - "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] - }, - } - """; - - AssertOutcomeTypeBuilder(builder, expected); - } - - [Fact] - public void SchemaBuilder_WithType_works_as_expected() - { - const string collectionName1 = "medicalRecords.patients"; - const string collectionName2 = "test.collection"; - - var typedBuilder1 = new CsfleTypeSchemaBuilder() - .EncryptMetadata(keyId: _keyIdExample, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - var typeBuilder2 = new CsfleTypeSchemaBuilder() - .EncryptMetadata(algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); - - var encryptionSchemaBuilder = new CsfleSchemaBuilder() - .WithType(CollectionNamespace.FromFullName(collectionName1), typedBuilder1) - .WithType(CollectionNamespace.FromFullName(collectionName2), typeBuilder2); - - var expected = new Dictionary - { - [collectionName1] = """ - { - "bsonType": "object", - "encryptMetadata": { - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", - "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] - }, - } - """, - [collectionName2] = """ - { - "bsonType": "object", - "encryptMetadata": { - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", - }, - } - """ - }; - - AssertOutcomeBuilder(encryptionSchemaBuilder, expected); - } - - [Fact] - public void SchemaBuilder_WithTypeAndConfigure_works_as_expected() - { - const string collectionName1 = "medicalRecords.patients"; - const string collectionName2 = "test.collection"; - - var encryptionSchemaBuilder = new CsfleSchemaBuilder() - .WithType(CollectionNamespace.FromFullName(collectionName1), b => b.EncryptMetadata(keyId: _keyIdExample, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)) - .WithType(CollectionNamespace.FromFullName(collectionName2), b => b.EncryptMetadata(algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random)); - - var expected = new Dictionary - { - [collectionName1] = """ - { - "bsonType": "object", - "encryptMetadata": { - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", - "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] - }, - } - """, - [collectionName2] = """ - { - "bsonType": "object", - "encryptMetadata": { - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", - }, - } - """ - }; - - AssertOutcomeBuilder(encryptionSchemaBuilder, expected); - } - - [Fact] - public void SchemaBuilder_CompleteExampleWithBsonDocument_work_as_expected() - { - var collectionName = "medicalRecords.patients"; - - var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() - .EncryptMetadata(keyId: _keyIdExample) - .Property("insurance", insurance => insurance - .Property("policyNumber", bsonType: BsonType.Int32, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)) - .Property("medicalRecords", bsonType: BsonType.Array, - algorithm: CsfleEncryptionAlgorithm - .AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .Property("bloodType", bsonType: BsonType.String, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .Property("ssn", bsonType: BsonType.Int32, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - - var encryptionSchemaBuilder = new CsfleSchemaBuilder() - .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); - - var expected = new Dictionary - { - [collectionName] = """ - { - "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" - } - } - }, - } - """ - }; - - AssertOutcomeBuilder(encryptionSchemaBuilder, expected); - } - - [Fact] - public void SchemaBuilder_CompleteExample_work_as_expected() + public void BasicCompleteTest() { const string collectionName = "medicalRecords.patients"; - var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() - .EncryptMetadata(keyId: _keyIdExample) - .Property(p => p.Insurance, insurance => insurance - .Property(i => i.PolicyNumber, bsonType: BsonType.Int32, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)) - .Property(p => p.MedicalRecords, bsonType: BsonType.Array, - algorithm: CsfleEncryptionAlgorithm - .AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .Property("bloodType", bsonType: BsonType.String, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .Property(p => p.Ssn, bsonType: BsonType.Int32, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - - var encryptionSchemaBuilder = new CsfleSchemaBuilder() - .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); + var builder = CsfleSchemaBuilder.Create(schemaBuilder => + { + schemaBuilder.Encrypt(CollectionNamespace.FromFullName(collectionName), builder1 => + { + builder1.EncryptMetadata().WithKeyId(_keyIdExample); + + builder1.NestedProperty(p => p.Insurance, typedBuilder1 => + { + typedBuilder1.Property(i => i.PolicyNumber).WithBsonType(BsonType.Int32) + .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + }); + + builder1.Property(p => p.MedicalRecords).WithBsonType(BsonType.Array) + .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + builder1.Property("bloodType").WithBsonType(BsonType.String) + .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + builder1.Property(p => p.Ssn).WithBsonType(BsonType.Int32) + .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + } ); + }); var expected = new Dictionary { @@ -509,79 +95,71 @@ public void SchemaBuilder_CompleteExample_work_as_expected() """ }; - AssertOutcomeBuilder(encryptionSchemaBuilder, expected); - } - - [Fact] - public void SchemaBuilder_CompleteExampleWithPatternProperties_work_as_expected() - { - const string collectionName = "medicalRecords.patients"; - - var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() - .PatternProperty("_PIIString$", bsonType: BsonType.String, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - .PatternProperty("_PIIArray$", bsonType: BsonType.Array, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .PatternProperty(p => p.Insurance, builder => builder - .PatternProperty("_PIINumber$", bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - .PatternProperty("_PIIString$", bsonType: BsonType.String, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - ); - - var encryptionSchemaBuilder = new CsfleSchemaBuilder() - .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); - - var expected = new Dictionary - { - [collectionName] = """ - { - "bsonType": "object", - "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", - }, - }, - }, - }, - }, - } - """ - }; - - AssertOutcomeBuilder(encryptionSchemaBuilder, expected); - } - - private void AssertOutcomeTypeBuilder(CsfleTypeSchemaBuilder builder, string expected) - { - var parsedExpected = BsonDocument.Parse(expected); - var builtSchema = builder.Build(); - - Assert.Equal(parsedExpected, builtSchema); - } - - private void AssertOutcomeBuilder(CsfleSchemaBuilder builder, Dictionary expected) + AssertOutcomeBuilder2(builder, expected); + } + + // [Fact] + // public void BasicPatternTest() + // { + // const string collectionName = "medicalRecords.patients"; + // + // var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() + // .PatternProperty("_PIIString$", bsonType: BsonType.String, + // algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + // .PatternProperty("_PIIArray$", bsonType: BsonType.Array, + // algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + // .PatternProperty(p => p.Insurance, builder => builder + // .PatternProperty("_PIINumber$", bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + // .PatternProperty("_PIIString$", bsonType: BsonType.String, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + // ); + // + // var encryptionSchemaBuilder = new CsfleSchemaBuilder() + // .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); + // + // var expected = new Dictionary + // { + // [collectionName] = """ + // { + // "bsonType": "object", + // "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", + // }, + // }, + // }, + // }, + // }, + // } + // """ + // }; + // + // AssertOutcomeBuilder(encryptionSchemaBuilder, expected); + // } + + private void AssertOutcomeBuilder2(CsfleSchemaBuilder builder, Dictionary expected) { var builtSchema = builder.Build(); Assert.Equal(expected.Count, builtSchema.Count); @@ -592,6 +170,7 @@ private void AssertOutcomeBuilder(CsfleSchemaBuilder builder, Dictionary Date: Mon, 14 Apr 2025 12:30:00 +0200 Subject: [PATCH 15/38] Improvements --- .../Encryption/CsfleSchemaBuilder.cs | 70 +++++++------------ 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 00f28bc2b48..6498c588ffe 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -70,8 +70,8 @@ public void Encrypt(CollectionNamespace collectionNamespace, Action public class ElementBuilder where TSelf : ElementBuilder { - internal CsfleEncryptionAlgorithm? _algorithm; //TODO These should be protected as well - internal Guid? _keyId; + private protected CsfleEncryptionAlgorithm? _algorithm; + private protected Guid? _keyId; /// /// @@ -161,41 +161,49 @@ public class EncryptMetadataBuilder : ElementBuilder /// /// /// - /// - public class PropertyBuilder : ElementBuilder> //TODO Maybe we can have a common class for this and patternPropertyBuilder + /// + public abstract class SinglePropertyBuilder : ElementBuilder> where T : SinglePropertyBuilder { - private readonly FieldDefinition _path; - private List _bsonTypes; + private protected List _bsonTypes; /// /// /// - /// - public PropertyBuilder(FieldDefinition path) + /// + /// + public T WithBsonType(BsonType bsonType) { - _path = path; + _bsonTypes = [bsonType]; + return (T)this; } /// /// /// - /// + /// /// - public PropertyBuilder WithBsonType(BsonType bsonType) + public T WithBsonTypes(IEnumerable bsonTypes) { - _bsonTypes = [bsonType]; - return this; + _bsonTypes = [..bsonTypes]; + return (T)this; } + } + + /// + /// + /// + /// + public class PropertyBuilder : SinglePropertyBuilder> + { + private readonly FieldDefinition _path; /// /// /// - /// - /// - public PropertyBuilder WithBsonTypes(IEnumerable bsonTypes) + /// + public PropertyBuilder(FieldDefinition path) { - _bsonTypes = [..bsonTypes]; - return this; + _path = path; } internal BsonDocument Build(RenderArgs args) @@ -208,10 +216,9 @@ internal BsonDocument Build(RenderArgs args) /// /// /// - public class PatternPropertyBuilder : ElementBuilder> + public class PatternPropertyBuilder : SinglePropertyBuilder> { private readonly string _pattern; - private List _bsonTypes; /// /// @@ -222,28 +229,6 @@ public PatternPropertyBuilder(string pattern) _pattern = pattern; } - /// - /// - /// - /// - /// - public PatternPropertyBuilder WithBsonType(BsonType bsonType) - { - _bsonTypes = [bsonType]; - return this; - } - - /// - /// - /// - /// - /// - public PatternPropertyBuilder WithBsonTypes(IEnumerable bsonTypes) - { - _bsonTypes = [..bsonTypes]; - return this; - } - internal BsonDocument Build(RenderArgs args) { return new BsonDocument(_pattern, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes))); @@ -287,7 +272,6 @@ internal override BsonDocument Build(RenderArgs args) } } - /// /// /// From c37c6bb6bbc965258896a594693697302e0f8a58 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:39:00 +0200 Subject: [PATCH 16/38] Some simplifications --- .../Encryption/CsfleSchemaBuilder.cs | 73 ++++++++++++------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 6498c588ffe..92ba313091f 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -158,11 +158,14 @@ public class EncryptMetadataBuilder : ElementBuilder internal BsonDocument Build() => new("encryptMetadata", GetEncryptBsonDocument(_keyId, _algorithm, null)); } + + /// /// /// - /// - public abstract class SinglePropertyBuilder : ElementBuilder> where T : SinglePropertyBuilder + /// + /// + public abstract class SinglePropertyBuilder : ElementBuilder> where TSelf : SinglePropertyBuilder { private protected List _bsonTypes; @@ -171,10 +174,10 @@ public abstract class SinglePropertyBuilder : ElementBuilder /// /// - public T WithBsonType(BsonType bsonType) + public TSelf WithBsonType(BsonType bsonType) { _bsonTypes = [bsonType]; - return (T)this; + return (TSelf)this; } /// @@ -182,18 +185,20 @@ public T WithBsonType(BsonType bsonType) /// /// /// - public T WithBsonTypes(IEnumerable bsonTypes) + public TSelf WithBsonTypes(IEnumerable bsonTypes) { _bsonTypes = [..bsonTypes]; - return (T)this; + return (TSelf)this; } + + internal abstract BsonDocument Build(RenderArgs args); } /// /// /// /// - public class PropertyBuilder : SinglePropertyBuilder> + public class PropertyBuilder : SinglePropertyBuilder, TDocument> { private readonly FieldDefinition _path; @@ -206,7 +211,7 @@ public PropertyBuilder(FieldDefinition path) _path = path; } - internal BsonDocument Build(RenderArgs args) + internal override BsonDocument Build(RenderArgs args) { return new BsonDocument(_path.Render(args).FieldName, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes))); } @@ -216,7 +221,7 @@ internal BsonDocument Build(RenderArgs args) /// /// /// - public class PatternPropertyBuilder : SinglePropertyBuilder> + public class PatternPropertyBuilder : SinglePropertyBuilder, TDocument> { private readonly string _pattern; @@ -229,7 +234,7 @@ public PatternPropertyBuilder(string pattern) _pattern = pattern; } - internal BsonDocument Build(RenderArgs args) + internal override BsonDocument Build(RenderArgs args) { return new BsonDocument(_pattern, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes))); } @@ -238,7 +243,7 @@ internal BsonDocument Build(RenderArgs args) /// /// /// - public abstract class NestedPropertyBuilder + public abstract class SubdocumentPropertyBuilder { internal abstract BsonDocument Build(RenderArgs args); } @@ -248,7 +253,7 @@ public abstract class NestedPropertyBuilder /// /// /// - public class NestedPropertyBuilder : NestedPropertyBuilder + public class NestedPropertyBuilder : SubdocumentPropertyBuilder { private readonly FieldDefinition _path; private readonly Action> _configure; @@ -272,20 +277,12 @@ internal override BsonDocument Build(RenderArgs args) } } - /// - /// - /// - public abstract class NestedPatternPropertyBuilder - { - internal abstract BsonDocument Build(RenderArgs args); - } - /// /// /// /// /// - public class NestedPatternPropertyBuilder : NestedPatternPropertyBuilder + public class NestedPatternPropertyBuilder : SubdocumentPropertyBuilder { private readonly FieldDefinition _path; private readonly Action> _configure; @@ -309,7 +306,6 @@ internal override BsonDocument Build(RenderArgs args) } } - /// /// /// @@ -324,7 +320,7 @@ public abstract class TypedBuilder /// public class TypedBuilder : TypedBuilder { - private readonly List> _nestedProperties = []; + private readonly List> _subdocumentProperties = []; private readonly List> _properties = []; private EncryptMetadataBuilder _metadata; @@ -360,6 +356,18 @@ public PropertyBuilder Property(Expression(path)); } + /// + /// + /// + /// + /// + public PatternPropertyBuilder PatternProperty(string pattern) + { + var property = new PatternPropertyBuilder(pattern); + _properties.Add(property); + return property; + } + /// /// /// @@ -369,7 +377,7 @@ public PropertyBuilder Property(Expression NestedProperty(FieldDefinition path, Action> configure) { var nestedProperty = new NestedPropertyBuilder(path, configure); - _nestedProperties.Add(nestedProperty); + _subdocumentProperties.Add(nestedProperty); return nestedProperty; } @@ -384,6 +392,19 @@ public NestedPropertyBuilder NestedProperty(Expressio return NestedProperty(new ExpressionFieldDefinition(path), configure); } + /// + /// + /// + /// + /// + /// + public NestedPatternPropertyBuilder NestedPatternProperty(string pattern, Action> configure) + { + var nestedProperty = new NestedPatternPropertyBuilder(pattern, configure); + _subdocumentProperties.Add(nestedProperty); + return nestedProperty; + } + internal override BsonDocument Build() { var schema = new BsonDocument("bsonType", "object"); @@ -398,11 +419,11 @@ internal override BsonDocument Build() BsonDocument properties = null; - if (_nestedProperties.Any()) + if (_subdocumentProperties.Any()) { properties = new BsonDocument(); - foreach (var nestedProperty in _nestedProperties) + foreach (var nestedProperty in _subdocumentProperties) { properties.Merge(nestedProperty.Build(args)); } From 6f1b2a3cf9d4ef0ba145464cee8ed28049a5d7c4 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:41:06 +0200 Subject: [PATCH 17/38] Simplified --- .../Encryption/CsfleSchemaBuilder.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 92ba313091f..531f3935913 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -159,13 +159,21 @@ public class EncryptMetadataBuilder : ElementBuilder } + /// + /// + /// + /// + public abstract class SinglePropertyBuilder : ElementBuilder> + { + internal abstract BsonDocument Build(RenderArgs args); + } /// /// /// /// /// - public abstract class SinglePropertyBuilder : ElementBuilder> where TSelf : SinglePropertyBuilder + public abstract class SinglePropertyBuilder : SinglePropertyBuilder where TSelf : SinglePropertyBuilder { private protected List _bsonTypes; @@ -190,8 +198,6 @@ public TSelf WithBsonTypes(IEnumerable bsonTypes) _bsonTypes = [..bsonTypes]; return (TSelf)this; } - - internal abstract BsonDocument Build(RenderArgs args); } /// @@ -321,7 +327,7 @@ public abstract class TypedBuilder public class TypedBuilder : TypedBuilder { private readonly List> _subdocumentProperties = []; - private readonly List> _properties = []; + private readonly List> _properties = []; private EncryptMetadataBuilder _metadata; /// From 4942f9760ca21851f1583b32d6b21cf7af252bf8 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:10:03 +0200 Subject: [PATCH 18/38] Improved API --- .../Encryption/CsfleSchemaBuilder.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 531f3935913..fd1e7e08f57 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -171,9 +171,11 @@ public abstract class SinglePropertyBuilder : ElementBuilder /// /// - /// + /// /// - public abstract class SinglePropertyBuilder : SinglePropertyBuilder where TSelf : SinglePropertyBuilder + public abstract class SinglePropertyBuilder + : ElementBuilder + where TBuilder : SinglePropertyBuilder { private protected List _bsonTypes; @@ -182,10 +184,10 @@ public abstract class SinglePropertyBuilder : SinglePropertyBu /// /// /// - public TSelf WithBsonType(BsonType bsonType) + public TBuilder WithBsonType(BsonType bsonType) { _bsonTypes = [bsonType]; - return (TSelf)this; + return (TBuilder)this; } /// @@ -193,11 +195,13 @@ public TSelf WithBsonType(BsonType bsonType) /// /// /// - public TSelf WithBsonTypes(IEnumerable bsonTypes) + public TBuilder WithBsonTypes(IEnumerable bsonTypes) { _bsonTypes = [..bsonTypes]; - return (TSelf)this; + return (TBuilder)this; } + + internal abstract BsonDocument Build(RenderArgs args); } /// @@ -327,7 +331,8 @@ public abstract class TypedBuilder public class TypedBuilder : TypedBuilder { private readonly List> _subdocumentProperties = []; - private readonly List> _properties = []; + private readonly List> _properties = []; + private readonly List> _patternProperties = []; private EncryptMetadataBuilder _metadata; /// @@ -370,7 +375,7 @@ public PropertyBuilder Property(Expression PatternProperty(string pattern) { var property = new PatternPropertyBuilder(pattern); - _properties.Add(property); + _patternProperties.Add(property); return property; } From 93f46f979f0ee60e9a7cd2220e5e280ae99e2800 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 14 Apr 2025 18:51:23 +0200 Subject: [PATCH 19/38] Small fix --- .../Encryption/CsfleSchemaBuilder.cs | 61 +++++++++++++++---- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index fd1e7e08f57..306d3be2201 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -253,7 +253,7 @@ internal override BsonDocument Build(RenderArgs args) /// /// /// - public abstract class SubdocumentPropertyBuilder + public abstract class NestedPropertyBuilderBase { internal abstract BsonDocument Build(RenderArgs args); } @@ -263,7 +263,7 @@ public abstract class SubdocumentPropertyBuilder /// /// /// - public class NestedPropertyBuilder : SubdocumentPropertyBuilder + public class NestedPropertyBuilder : NestedPropertyBuilderBase { private readonly FieldDefinition _path; private readonly Action> _configure; @@ -287,12 +287,20 @@ internal override BsonDocument Build(RenderArgs args) } } + /// + /// + /// + public abstract class NestedPatternPropertyBuilderBase + { + internal abstract BsonDocument Build(RenderArgs args); + } + /// /// /// /// /// - public class NestedPatternPropertyBuilder : SubdocumentPropertyBuilder + public class NestedPatternPropertyBuilder : NestedPatternPropertyBuilderBase { private readonly FieldDefinition _path; private readonly Action> _configure; @@ -330,7 +338,8 @@ public abstract class TypedBuilder /// public class TypedBuilder : TypedBuilder { - private readonly List> _subdocumentProperties = []; + private readonly List> _nestedProperties = []; + private readonly List> _nestedPatternProperties = []; private readonly List> _properties = []; private readonly List> _patternProperties = []; private EncryptMetadataBuilder _metadata; @@ -388,7 +397,7 @@ public PatternPropertyBuilder PatternProperty(string pattern) public NestedPropertyBuilder NestedProperty(FieldDefinition path, Action> configure) { var nestedProperty = new NestedPropertyBuilder(path, configure); - _subdocumentProperties.Add(nestedProperty); + _nestedProperties.Add(nestedProperty); return nestedProperty; } @@ -412,7 +421,7 @@ public NestedPropertyBuilder NestedProperty(Expressio public NestedPatternPropertyBuilder NestedPatternProperty(string pattern, Action> configure) { var nestedProperty = new NestedPatternPropertyBuilder(pattern, configure); - _subdocumentProperties.Add(nestedProperty); + _nestedPatternProperties.Add(nestedProperty); return nestedProperty; } @@ -429,24 +438,45 @@ internal override BsonDocument Build() BsonDocument properties = null; + BsonDocument patternProperties = null; - if (_subdocumentProperties.Any()) + if (_properties.Any()) { - properties = new BsonDocument(); + properties ??= new BsonDocument(); - foreach (var nestedProperty in _subdocumentProperties) + foreach (var property in _properties) { - properties.Merge(nestedProperty.Build(args)); + properties.Merge(property.Build(args)); } } - if (_properties.Any()) + if (_nestedProperties.Any()) { properties ??= new BsonDocument(); - foreach (var property in _properties) + foreach (var nestedProperty in _nestedProperties) { - properties.Merge(property.Build(args)); + properties.Merge(nestedProperty.Build(args)); + } + } + + if (_patternProperties.Any()) + { + patternProperties ??= new BsonDocument(); + + foreach (var patternProperty in _patternProperties) + { + patternProperties.Merge(patternProperty.Build(args)); + } + } + + if (_nestedPatternProperties.Any()) + { + patternProperties ??= new BsonDocument(); + + foreach (var nestedPatternProperty in _nestedPatternProperties) + { + patternProperties.Merge(nestedPatternProperty.Build(args)); } } @@ -455,6 +485,11 @@ internal override BsonDocument Build() schema.Add("properties", properties); } + if (patternProperties != null) + { + schema.Add("patternProperties", patternProperties); + } + return schema; } } From 670c663761cf5306a7942bbaef39349fab9d143e Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:28:37 +0200 Subject: [PATCH 20/38] Fixing schema builder --- .../Encryption/CsfleSchemaBuilder.cs | 506 ++++-------------- .../Encryption/CsfleSchemaBuilderTests.cs | 142 ++--- 2 files changed, 190 insertions(+), 458 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 306d3be2201..93045f1f0a8 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -23,20 +23,20 @@ namespace MongoDB.Driver.Encryption { /// - /// + /// Entry point for building CSFLE (Client-Side Field Level Encryption) schemas. /// public class CsfleSchemaBuilder { - private readonly Dictionary _typedBuilders = []; + private readonly Dictionary _schemas = new(); + private CsfleSchemaBuilder() { + } /// - /// + /// Creates a new CSFLE schema builder and configures it using the specified action. /// - /// - /// public static CsfleSchemaBuilder Create(Action configure) { var builder = new CsfleSchemaBuilder(); @@ -45,72 +45,148 @@ public static CsfleSchemaBuilder Create(Action configure) } /// - /// + /// Defines encryption rules for a given collection. /// - /// - /// - /// - public void Encrypt(CollectionNamespace collectionNamespace, Action> configure) + public CsfleSchemaBuilder Encrypt(string collectionNamespace, Action> configure) { - var typedBuilder = new TypedBuilder(); - configure(typedBuilder); - _typedBuilders.Add(collectionNamespace.FullName, typedBuilder); + var builder = new EncryptedCollectionBuilder(); + configure(builder); + _schemas.Add(collectionNamespace, builder.Build()); + return this; } /// - /// + /// Builds and returns the resulting CSFLE schema. /// - /// - public IReadOnlyDictionary Build() => _typedBuilders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Build()); + public IDictionary Build() => _schemas; } /// - /// + /// //TODO /// - /// - public class ElementBuilder where TSelf : ElementBuilder + public class EncryptedCollectionBuilder { - private protected CsfleEncryptionAlgorithm? _algorithm; - private protected Guid? _keyId; + private readonly BsonDocument _schema = new("bsonType", "object"); + private readonly RenderArgs _args = new(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); //TODO Create those two in the constructor...? /// - /// + /// //TODO /// - /// - /// - public TSelf WithKeyId(Guid keyId) + internal EncryptedCollectionBuilder() //TODO Probably this should be internal { - _keyId = keyId; - return (TSelf)this; } /// - /// + /// //TODO /// - /// - /// - public TSelf WithAlgorithm(CsfleEncryptionAlgorithm algorithm) + public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null) { - _algorithm = algorithm; - return (TSelf)this; + _schema["encryptMetadata"] = new BsonDocument + { + { "keyId", () => new BsonArray { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }, keyId is not null }, + { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null } + }; + return this; } - internal static BsonDocument GetEncryptBsonDocument(Guid? keyId, CsfleEncryptionAlgorithm? algorithm, List bsonTypes) + /// + /// //TODO + /// + public EncryptedCollectionBuilder Property( + Expression> path, + BsonType bsonType, + CsfleEncryptionAlgorithm? algorithm = null, + Guid? keyId = null) + => Property(path, new[] { bsonType }, algorithm, keyId); + + /// + /// //TODO + /// + public EncryptedCollectionBuilder Property( + Expression> path, + BsonType[] bsonTypes, + CsfleEncryptionAlgorithm? algorithm = null, + Guid? keyId = null) + => Property(new ExpressionFieldDefinition(path), bsonTypes, algorithm, keyId); + + /// + /// //TODO + /// + public EncryptedCollectionBuilder Property( + FieldDefinition path, + BsonType bsonType, + CsfleEncryptionAlgorithm? algorithm = null, + Guid? keyId = null) + => Property(path, new[] { bsonType }, algorithm, keyId); + + /// + /// //TODO + /// + public EncryptedCollectionBuilder Property( + FieldDefinition path, + BsonType[] bsonTypes, + CsfleEncryptionAlgorithm? algorithm = null, + Guid? keyId = null) { - var bsonType = bsonTypes?.First(); //TODO need to support multiple types + _schema["properties"] ??= new BsonDocument(); + var properties = _schema["properties"].AsBsonDocument; - return new BsonDocument + var fieldName = path.Render(_args).FieldName; + + var convertedBsonTypes = bsonTypes.Select(MapBsonTypeToString).ToList(); + BsonValue bsonTypeVal = convertedBsonTypes.Count == 1 + ? convertedBsonTypes[0] + : new BsonArray(convertedBsonTypes); + + properties[fieldName] = new BsonDocument { - { "bsonType", () => MapBsonTypeToString(bsonType!.Value), bsonType is not null }, - { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null }, - { - "keyId", - () => new BsonArray(new[] { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }), - keyId is not null - }, + { "encrypt", new BsonDocument + { + { "bsonType", bsonTypeVal }, + { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null }, + { + "keyId", + () => new BsonArray(new[] { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }), + keyId is not null + }, + } + } }; + return this; } + /// + /// //TODO + /// + public EncryptedCollectionBuilder Property( + Expression> path, + Action> configure) + => Property(new ExpressionFieldDefinition(path), configure); + + /// + /// //TODO + /// + public EncryptedCollectionBuilder Property( + FieldDefinition path, + Action> configure) + { + var nestedBuilder = new EncryptedCollectionBuilder(); + configure(nestedBuilder); + + var fieldName = path.Render(_args).FieldName; + + _schema["properties"] ??= new BsonDocument(); + var properties = _schema["properties"].AsBsonDocument; + properties[fieldName] = nestedBuilder.Build(); + + return this; + } + + /// + /// //TODO + /// + public BsonDocument Build() => _schema; + private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstTypeFilterOperation, do we have a common place where this could go? { return type switch @@ -150,350 +226,6 @@ private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorith } } - /// - /// - /// - public class EncryptMetadataBuilder : ElementBuilder - { - internal BsonDocument Build() => new("encryptMetadata", GetEncryptBsonDocument(_keyId, _algorithm, null)); - } - - - /// - /// - /// - /// - public abstract class SinglePropertyBuilder : ElementBuilder> - { - internal abstract BsonDocument Build(RenderArgs args); - } - - /// - /// - /// - /// - /// - public abstract class SinglePropertyBuilder - : ElementBuilder - where TBuilder : SinglePropertyBuilder - { - private protected List _bsonTypes; - - /// - /// - /// - /// - /// - public TBuilder WithBsonType(BsonType bsonType) - { - _bsonTypes = [bsonType]; - return (TBuilder)this; - } - - /// - /// - /// - /// - /// - public TBuilder WithBsonTypes(IEnumerable bsonTypes) - { - _bsonTypes = [..bsonTypes]; - return (TBuilder)this; - } - - internal abstract BsonDocument Build(RenderArgs args); - } - - /// - /// - /// - /// - public class PropertyBuilder : SinglePropertyBuilder, TDocument> - { - private readonly FieldDefinition _path; - - /// - /// - /// - /// - public PropertyBuilder(FieldDefinition path) - { - _path = path; - } - - internal override BsonDocument Build(RenderArgs args) - { - return new BsonDocument(_path.Render(args).FieldName, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes))); - } - } - - /// - /// - /// - /// - public class PatternPropertyBuilder : SinglePropertyBuilder, TDocument> - { - private readonly string _pattern; - - /// - /// - /// - /// - public PatternPropertyBuilder(string pattern) - { - _pattern = pattern; - } - - internal override BsonDocument Build(RenderArgs args) - { - return new BsonDocument(_pattern, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes))); - } - } - - /// - /// - /// - public abstract class NestedPropertyBuilderBase - { - internal abstract BsonDocument Build(RenderArgs args); - } - - /// - /// - /// - /// - /// - public class NestedPropertyBuilder : NestedPropertyBuilderBase - { - private readonly FieldDefinition _path; - private readonly Action> _configure; - - /// - /// - /// - /// - /// - public NestedPropertyBuilder(FieldDefinition path, Action> configure) - { - _path = path; - _configure = configure; - } - - internal override BsonDocument Build(RenderArgs args) - { - var fieldBuilder = new TypedBuilder(); - _configure(fieldBuilder); - return new BsonDocument(_path.Render(args).FieldName, fieldBuilder.Build()); - } - } - - /// - /// - /// - public abstract class NestedPatternPropertyBuilderBase - { - internal abstract BsonDocument Build(RenderArgs args); - } - - /// - /// - /// - /// - /// - public class NestedPatternPropertyBuilder : NestedPatternPropertyBuilderBase - { - private readonly FieldDefinition _path; - private readonly Action> _configure; - - /// - /// - /// - /// - /// - public NestedPatternPropertyBuilder(FieldDefinition path, Action> configure) - { - _path = path; - _configure = configure; - } - - internal override BsonDocument Build(RenderArgs args) - { - var fieldBuilder = new TypedBuilder(); - _configure(fieldBuilder); - return new BsonDocument(_path.Render(args).FieldName, fieldBuilder.Build()); - } - } - - /// - /// - /// - public abstract class TypedBuilder - { - internal abstract BsonDocument Build(); - } - - /// - /// - /// - /// - public class TypedBuilder : TypedBuilder - { - private readonly List> _nestedProperties = []; - private readonly List> _nestedPatternProperties = []; - private readonly List> _properties = []; - private readonly List> _patternProperties = []; - private EncryptMetadataBuilder _metadata; - - /// - /// - /// - /// - public EncryptMetadataBuilder EncryptMetadata() - { - _metadata = new EncryptMetadataBuilder(); - return _metadata; - } - - /// - /// - /// - /// - /// - public PropertyBuilder Property(FieldDefinition path) - { - var property = new PropertyBuilder(path); - _properties.Add(property); - return property; - } - - /// - /// - /// - /// - /// - public PropertyBuilder Property(Expression> path) - { - return Property(new ExpressionFieldDefinition(path)); - } - - /// - /// - /// - /// - /// - public PatternPropertyBuilder PatternProperty(string pattern) - { - var property = new PatternPropertyBuilder(pattern); - _patternProperties.Add(property); - return property; - } - - /// - /// - /// - /// - /// - /// - public NestedPropertyBuilder NestedProperty(FieldDefinition path, Action> configure) - { - var nestedProperty = new NestedPropertyBuilder(path, configure); - _nestedProperties.Add(nestedProperty); - return nestedProperty; - } - - /// - /// - /// - /// - /// - /// - public NestedPropertyBuilder NestedProperty(Expression> path, Action> configure) - { - return NestedProperty(new ExpressionFieldDefinition(path), configure); - } - - /// - /// - /// - /// - /// - /// - public NestedPatternPropertyBuilder NestedPatternProperty(string pattern, Action> configure) - { - var nestedProperty = new NestedPatternPropertyBuilder(pattern, configure); - _nestedPatternProperties.Add(nestedProperty); - return nestedProperty; - } - - internal override BsonDocument Build() - { - var schema = new BsonDocument("bsonType", "object"); - - if (_metadata is not null) - { - schema.Merge(_metadata.Build()); - } - - var args = new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); - - - BsonDocument properties = null; - BsonDocument patternProperties = null; - - if (_properties.Any()) - { - properties ??= new BsonDocument(); - - foreach (var property in _properties) - { - properties.Merge(property.Build(args)); - } - } - - if (_nestedProperties.Any()) - { - properties ??= new BsonDocument(); - - foreach (var nestedProperty in _nestedProperties) - { - properties.Merge(nestedProperty.Build(args)); - } - } - - if (_patternProperties.Any()) - { - patternProperties ??= new BsonDocument(); - - foreach (var patternProperty in _patternProperties) - { - patternProperties.Merge(patternProperty.Build(args)); - } - } - - if (_nestedPatternProperties.Any()) - { - patternProperties ??= new BsonDocument(); - - foreach (var nestedPatternProperty in _nestedPatternProperties) - { - patternProperties.Merge(nestedPatternProperty.Build(args)); - } - } - - if (properties != null) - { - schema.Add("properties", properties); - } - - if (patternProperties != null) - { - schema.Add("patternProperties", patternProperties); - } - - return schema; - } - } - /// /// The type of possible encryption algorithms. /// diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 54b7a4ea865..90854f1694b 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -26,77 +26,77 @@ public class CsfleSchemaBuilderTests { private readonly Guid _keyIdExample = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); - [Fact] - public void BasicCompleteTest() - { - const string collectionName = "medicalRecords.patients"; - - var builder = CsfleSchemaBuilder.Create(schemaBuilder => - { - schemaBuilder.Encrypt(CollectionNamespace.FromFullName(collectionName), builder1 => - { - builder1.EncryptMetadata().WithKeyId(_keyIdExample); - - builder1.NestedProperty(p => p.Insurance, typedBuilder1 => - { - typedBuilder1.Property(i => i.PolicyNumber).WithBsonType(BsonType.Int32) - .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - }); - - builder1.Property(p => p.MedicalRecords).WithBsonType(BsonType.Array) - .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); - builder1.Property("bloodType").WithBsonType(BsonType.String) - .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); - builder1.Property(p => p.Ssn).WithBsonType(BsonType.Int32) - .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - } ); - }); - - var expected = new Dictionary - { - [collectionName] = """ - { - "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" - } - } - }, - } - """ - }; - - AssertOutcomeBuilder2(builder, expected); - } + // [Fact] + // public void BasicCompleteTest() + // { + // const string collectionName = "medicalRecords.patients"; + // + // var builder = CsfleSchemaBuilder.Create(schemaBuilder => + // { + // schemaBuilder.Encrypt(CollectionNamespace.FromFullName(collectionName), builder1 => + // { + // builder1.EncryptMetadata().WithKeyId(_keyIdExample); + // + // builder1.NestedProperty(p => p.Insurance, typedBuilder1 => + // { + // typedBuilder1.Property(i => i.PolicyNumber).WithBsonType(BsonType.Int32) + // .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + // }); + // + // builder1.Property(p => p.MedicalRecords).WithBsonType(BsonType.Array) + // .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + // builder1.Property("bloodType").WithBsonType(BsonType.String) + // .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + // builder1.Property(p => p.Ssn).WithBsonType(BsonType.Int32) + // .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + // } ); + // }); + // + // var expected = new Dictionary + // { + // [collectionName] = """ + // { + // "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" + // } + // } + // }, + // } + // """ + // }; + // + // AssertOutcomeBuilder2(builder, expected); + // } // [Fact] // public void BasicPatternTest() From 517db94c25de5f53f98da112fd04f4ae348c65e4 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 1 May 2025 14:18:30 +0200 Subject: [PATCH 21/38] Various improvements plus fixed tests --- .../Encryption/CsfleSchemaBuilder.cs | 132 +++++++-- .../Encryption/CsfleSchemaBuilderTests.cs | 279 +++++++++--------- 2 files changed, 242 insertions(+), 169 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 93045f1f0a8..93195ac6d78 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -31,7 +31,6 @@ public class CsfleSchemaBuilder private CsfleSchemaBuilder() { - } /// @@ -72,7 +71,7 @@ public class EncryptedCollectionBuilder /// /// //TODO /// - internal EncryptedCollectionBuilder() //TODO Probably this should be internal + internal EncryptedCollectionBuilder() { } @@ -89,6 +88,53 @@ public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, return this; } + /// + /// //TODO + /// + public EncryptedCollectionBuilder PatternProperty( + string pattern, + BsonType bsonType, + CsfleEncryptionAlgorithm? algorithm = null, + Guid? keyId = null) + => PatternProperty(pattern, [bsonType], algorithm, keyId); + + /// + /// //TODO + /// + public EncryptedCollectionBuilder PatternProperty( + string pattern, + BsonType[] bsonTypes, + CsfleEncryptionAlgorithm? algorithm = null, + Guid? keyId = null) + { + AddToPatternProperties(pattern, CreateEncryptDocument(bsonTypes, algorithm, keyId)); + return this; + } + + /// + /// //TODO + /// + public EncryptedCollectionBuilder PatternProperty( + Expression> path, + Action> configure) + => PatternProperty(new ExpressionFieldDefinition(path), configure); + + /// + /// //TODO + /// + public EncryptedCollectionBuilder PatternProperty( + FieldDefinition path, + Action> configure) + { + var nestedBuilder = new EncryptedCollectionBuilder(); + configure(nestedBuilder); + + var fieldName = path.Render(_args).FieldName; + + AddToPatternProperties(fieldName, nestedBuilder.Build()); + return this; + } + /// /// //TODO /// @@ -97,7 +143,7 @@ public EncryptedCollectionBuilder Property( BsonType bsonType, CsfleEncryptionAlgorithm? algorithm = null, Guid? keyId = null) - => Property(path, new[] { bsonType }, algorithm, keyId); + => Property(path, [bsonType], algorithm, keyId); /// /// //TODO @@ -117,7 +163,7 @@ public EncryptedCollectionBuilder Property( BsonType bsonType, CsfleEncryptionAlgorithm? algorithm = null, Guid? keyId = null) - => Property(path, new[] { bsonType }, algorithm, keyId); + => Property(path, [bsonType], algorithm, keyId); /// /// //TODO @@ -128,30 +174,8 @@ public EncryptedCollectionBuilder Property( CsfleEncryptionAlgorithm? algorithm = null, Guid? keyId = null) { - _schema["properties"] ??= new BsonDocument(); - var properties = _schema["properties"].AsBsonDocument; - var fieldName = path.Render(_args).FieldName; - - var convertedBsonTypes = bsonTypes.Select(MapBsonTypeToString).ToList(); - BsonValue bsonTypeVal = convertedBsonTypes.Count == 1 - ? convertedBsonTypes[0] - : new BsonArray(convertedBsonTypes); - - properties[fieldName] = new BsonDocument - { - { "encrypt", new BsonDocument - { - { "bsonType", bsonTypeVal }, - { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null }, - { - "keyId", - () => new BsonArray(new[] { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }), - keyId is not null - }, - } - } - }; + AddToProperties(fieldName, CreateEncryptDocument(bsonTypes, algorithm, keyId)); return this; } @@ -174,11 +198,7 @@ public EncryptedCollectionBuilder Property( configure(nestedBuilder); var fieldName = path.Render(_args).FieldName; - - _schema["properties"] ??= new BsonDocument(); - var properties = _schema["properties"].AsBsonDocument; - properties[fieldName] = nestedBuilder.Build(); - + AddToProperties(fieldName, nestedBuilder.Build()); return this; } @@ -187,6 +207,54 @@ public EncryptedCollectionBuilder Property( /// public BsonDocument Build() => _schema; + private static BsonDocument CreateEncryptDocument( + BsonType[] bsonTypes, + CsfleEncryptionAlgorithm? algorithm = null, + Guid? keyId = null) + { + var convertedBsonTypes = bsonTypes.Select(MapBsonTypeToString).ToList(); + BsonValue bsonTypeVal = convertedBsonTypes.Count == 1 + ? convertedBsonTypes[0] + : new BsonArray(convertedBsonTypes); + + return new BsonDocument + { + { "encrypt", new BsonDocument + { + { "bsonType", bsonTypeVal }, + { "algorithm", () => MapCsfleEncyptionAlgorithmToString(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 MapBsonTypeToString(BsonType type) //TODO Taken from AstTypeFilterOperation, do we have a common place where this could go? { return type switch diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 90854f1694b..29b710f9a3e 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using FluentAssertions; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver.Encryption; @@ -26,151 +27,155 @@ public class CsfleSchemaBuilderTests { private readonly Guid _keyIdExample = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); - // [Fact] - // public void BasicCompleteTest() - // { - // const string collectionName = "medicalRecords.patients"; - // - // var builder = CsfleSchemaBuilder.Create(schemaBuilder => - // { - // schemaBuilder.Encrypt(CollectionNamespace.FromFullName(collectionName), builder1 => - // { - // builder1.EncryptMetadata().WithKeyId(_keyIdExample); - // - // builder1.NestedProperty(p => p.Insurance, typedBuilder1 => - // { - // typedBuilder1.Property(i => i.PolicyNumber).WithBsonType(BsonType.Int32) - // .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - // }); - // - // builder1.Property(p => p.MedicalRecords).WithBsonType(BsonType.Array) - // .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); - // builder1.Property("bloodType").WithBsonType(BsonType.String) - // .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); - // builder1.Property(p => p.Ssn).WithBsonType(BsonType.Int32) - // .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - // } ); - // }); - // - // var expected = new Dictionary - // { - // [collectionName] = """ - // { - // "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" - // } - // } - // }, - // } - // """ - // }; - // - // AssertOutcomeBuilder2(builder, expected); - // } - - // [Fact] - // public void BasicPatternTest() - // { - // const string collectionName = "medicalRecords.patients"; - // - // var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() - // .PatternProperty("_PIIString$", bsonType: BsonType.String, - // algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - // .PatternProperty("_PIIArray$", bsonType: BsonType.Array, - // algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - // .PatternProperty(p => p.Insurance, builder => builder - // .PatternProperty("_PIINumber$", bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - // .PatternProperty("_PIIString$", bsonType: BsonType.String, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - // ); - // - // var encryptionSchemaBuilder = new CsfleSchemaBuilder() - // .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); - // - // var expected = new Dictionary - // { - // [collectionName] = """ - // { - // "bsonType": "object", - // "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", - // }, - // }, - // }, - // }, - // }, - // } - // """ - // }; - // - // AssertOutcomeBuilder(encryptionSchemaBuilder, expected); - // } - - private void AssertOutcomeBuilder2(CsfleSchemaBuilder builder, Dictionary expected) + [Fact] + public void BasicPropertyTest() + { + const string collectionName = "medicalRecords.patients"; + + var builder = CsfleSchemaBuilder.Create(schemaBuilder => + { + schemaBuilder.Encrypt(collectionName, builder => + { + builder + .EncryptMetadata(keyId: _keyIdExample) + .Property(p => p.MedicalRecords, BsonType.Array, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .Property("bloodType", BsonType.String, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .Property(p => p.Ssn, BsonType.Int32, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .Property(p => p.Insurance, innerBuilder => + { + innerBuilder + .Property(i => i.PolicyNumber, BsonType.Int32, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + }); + } ); + }); + + var expected = new Dictionary + { + [collectionName] = """ + { + "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" + } + } + }, + } + """ + }; + + AssertOutcomeBuilder(builder, expected); + } + + [Fact] + public void BasicPatternTest() + { + const string collectionName = "medicalRecords.patients"; + + var builder = CsfleSchemaBuilder.Create(schemaBuilder => + { + schemaBuilder.Encrypt(collectionName, builder => + { + builder + .PatternProperty("_PIIString$", BsonType.String, CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperty("_PIIArray$", BsonType.Array, CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .PatternProperty(p => p.Insurance, innerBuilder => + { + innerBuilder + .PatternProperty("_PIIString$", BsonType.String, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperty("_PIINumber$", BsonType.Int32, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + }); + } ); + }); + + var expected = new Dictionary + { + [collectionName] = """ + { + "bsonType": "object", + "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", + }, + }, + }, + }, + }, + } + """ + }; + + AssertOutcomeBuilder(builder, expected); + } + + private void AssertOutcomeBuilder(CsfleSchemaBuilder builder, Dictionary expected) { var builtSchema = builder.Build(); - Assert.Equal(expected.Count, builtSchema.Count); + expected.Should().HaveCount(builtSchema.Count); foreach (var collectionNamespace in expected.Keys) { var parsed = BsonDocument.Parse(expected[collectionNamespace]); - Assert.Equal(parsed, builtSchema[collectionNamespace]); + parsed.Should().BeEquivalentTo(builtSchema[collectionNamespace]); } } - internal class Patient { [BsonId] From 8f10903adc685b3aa7bd5bfb4c72d11658cb2e3f Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 1 May 2025 14:31:19 +0200 Subject: [PATCH 22/38] Small fix --- src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 93195ac6d78..03b252406f7 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -23,7 +23,7 @@ namespace MongoDB.Driver.Encryption { /// - /// Entry point for building CSFLE (Client-Side Field Level Encryption) schemas. + /// //TODO /// public class CsfleSchemaBuilder { @@ -34,7 +34,7 @@ private CsfleSchemaBuilder() } /// - /// Creates a new CSFLE schema builder and configures it using the specified action. + /// //TODO /// public static CsfleSchemaBuilder Create(Action configure) { @@ -44,7 +44,7 @@ public static CsfleSchemaBuilder Create(Action configure) } /// - /// Defines encryption rules for a given collection. + /// //TODO /// public CsfleSchemaBuilder Encrypt(string collectionNamespace, Action> configure) { @@ -66,7 +66,7 @@ public CsfleSchemaBuilder Encrypt(string collectionNamespace, Action { private readonly BsonDocument _schema = new("bsonType", "object"); - private readonly RenderArgs _args = new(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); //TODO Create those two in the constructor...? + private readonly RenderArgs _args = new(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); /// /// //TODO @@ -295,7 +295,7 @@ private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorith } /// - /// The type of possible encryption algorithms. + /// The type of possible encryption algorithms. //TODO Maybe we need a more generic name? /// public enum CsfleEncryptionAlgorithm { From ef5faa960f879fa91f41696e174c79592fe32988 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 2 May 2025 08:43:13 +0200 Subject: [PATCH 23/38] Added first basic test --- .../Encryption/CsfleSchemaBuilder.cs | 5 +- .../Encryption/CsfleSchemaBuilderTests.cs | 63 ++++++++++++++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 03b252406f7..1f6ec4d9497 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -202,10 +202,7 @@ public EncryptedCollectionBuilder Property( return this; } - /// - /// //TODO - /// - public BsonDocument Build() => _schema; + internal BsonDocument Build() => _schema; private static BsonDocument CreateEncryptDocument( BsonType[] bsonTypes, diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 29b710f9a3e..d18a9d9e17e 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -25,7 +25,44 @@ namespace MongoDB.Driver.Tests.Encryption { public class CsfleSchemaBuilderTests { - private readonly Guid _keyIdExample = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); + private static Guid _keyIdExample = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); + private const string _keyIdString = "6f4af470-00d1-401f-ac39-f45902a0c0c8"; + + [Theory] + [InlineData(BsonType.Array, + CsfleEncryptionAlgorithm.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, + CsfleEncryptionAlgorithm.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_PropertyWithExpression_works_as_expected(BsonType bsonType, CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + { + Guid? keyId = keyString is null ? null : Guid.Parse(keyString); + var builder = new EncryptedCollectionBuilder(); + + builder.Property(p => p.MedicalRecords, bsonType, algorithm, keyId); + + var expected = $$""" + { + "bsonType": "object", + "properties": { + "medicalRecords": { + "encrypt": { + {{expectedContent}} + } + } + } + } + """; + + AssertOutcomeCollectionBuilder(builder, expected); + } [Fact] public void BasicPropertyTest() @@ -176,6 +213,30 @@ private void AssertOutcomeBuilder(CsfleSchemaBuilder builder, Dictionary(EncryptedCollectionBuilder builder, string expected) + { + var builtSchema = builder.Build(); + var expectedSchema = BsonDocument.Parse(expected); + expectedSchema.Should().BeEquivalentTo(builtSchema); + } + + /** To test: + * - Metadata + * - Property with expression + * - Property with string + * - Property with single bsonType + * - Property with multiple bsonType + * - Nested property with expression + * - Nested property with string + * - Pattern property with string + * - Pattern property with multiple bsonType + * - Nested pattern property with expression + * - Nested pattern property with string + * - Multiple types in schema + * - Property and pattern property together + */ + internal class Patient { [BsonId] From 601169e9db838d3f6be6f8538d061913668ad224 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 2 May 2025 10:55:34 +0200 Subject: [PATCH 24/38] Corrections plus more tests --- .../Encryption/CsfleSchemaBuilder.cs | 8 +- .../Encryption/CsfleSchemaBuilderTests.cs | 297 +++++++++++++++++- 2 files changed, 294 insertions(+), 11 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index 1f6ec4d9497..c91943fa57b 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -103,7 +103,7 @@ public EncryptedCollectionBuilder PatternProperty( /// public EncryptedCollectionBuilder PatternProperty( string pattern, - BsonType[] bsonTypes, + IEnumerable bsonTypes, CsfleEncryptionAlgorithm? algorithm = null, Guid? keyId = null) { @@ -150,7 +150,7 @@ public EncryptedCollectionBuilder Property( /// public EncryptedCollectionBuilder Property( Expression> path, - BsonType[] bsonTypes, + IEnumerable bsonTypes, CsfleEncryptionAlgorithm? algorithm = null, Guid? keyId = null) => Property(new ExpressionFieldDefinition(path), bsonTypes, algorithm, keyId); @@ -170,7 +170,7 @@ public EncryptedCollectionBuilder Property( /// public EncryptedCollectionBuilder Property( FieldDefinition path, - BsonType[] bsonTypes, + IEnumerable bsonTypes, CsfleEncryptionAlgorithm? algorithm = null, Guid? keyId = null) { @@ -205,7 +205,7 @@ public EncryptedCollectionBuilder Property( internal BsonDocument Build() => _schema; private static BsonDocument CreateEncryptDocument( - BsonType[] bsonTypes, + IEnumerable bsonTypes, CsfleEncryptionAlgorithm? algorithm = null, Guid? keyId = null) { diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index d18a9d9e17e..7a71a6fe326 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -25,8 +25,209 @@ namespace MongoDB.Driver.Tests.Encryption { public class CsfleSchemaBuilderTests { - private static Guid _keyIdExample = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); private const string _keyIdString = "6f4af470-00d1-401f-ac39-f45902a0c0c8"; + private static Guid _keyIdExample = Guid.Parse(_keyIdString); //TODO Check if I should remove this + + [Theory] + [InlineData( + CsfleEncryptionAlgorithm.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(CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + { + Guid? keyId = keyString is null ? null : Guid.Parse(keyString); + var builder = new EncryptedCollectionBuilder(); + + builder.EncryptMetadata(keyId, algorithm); + + var expected = $$""" + { + "bsonType": "object", + "encryptMetadata": { + {{expectedContent}} + } + } + """; + + AssertOutcomeCollectionBuilder(builder, expected); + } + + [Theory] + [InlineData(BsonType.Array, + CsfleEncryptionAlgorithm.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, + CsfleEncryptionAlgorithm.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, CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + { + Guid? keyId = keyString is null ? null : Guid.Parse(keyString); + var builder = new EncryptedCollectionBuilder(); + + builder.PatternProperty("randomRegex*", bsonType, algorithm, keyId); + + var expected = $$""" + { + "bsonType": "object", + "patternProperties": { + "randomRegex*": { + "encrypt": { + {{expectedContent}} + } + } + } + } + """; + + AssertOutcomeCollectionBuilder(builder, expected); + } + + [Theory] + [InlineData(new[] {BsonType.Array, BsonType.String}, + CsfleEncryptionAlgorithm.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}, + CsfleEncryptionAlgorithm.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_PatternPropertyWithMultipleBsonTypes_works_as_expected(IEnumerable bsonTypes, CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + { + Guid? keyId = keyString is null ? null : Guid.Parse(keyString); + var builder = new EncryptedCollectionBuilder(); + + builder.PatternProperty("randomRegex*", bsonTypes, algorithm, keyId); + + var expected = $$""" + { + "bsonType": "object", + "patternProperties": { + "randomRegex*": { + "encrypt": { + {{expectedContent}} + } + } + } + } + """; + + AssertOutcomeCollectionBuilder(builder, expected); + } + + [Fact] + public void EncryptedCollection_PatternPropertyNested_works_as_expected() + { + Guid? keyId = Guid.Parse(_keyIdString); + var builder = new EncryptedCollectionBuilder(); + + builder.PatternProperty(p => p.Insurance, innerBuilder => + { + innerBuilder + .EncryptMetadata(keyId) + .Property("policyNumber", BsonType.Int32, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperty("randomRegex*", BsonType.String, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + }); + + var expected = """ + { + "bsonType": "object", + "patternProperties": { + "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_PatternPropertyNestedWithString_works_as_expected() + { + Guid? keyId = Guid.Parse(_keyIdString); + var builder = new EncryptedCollectionBuilder(); + + builder.PatternProperty("insurance", innerBuilder => + { + innerBuilder + .EncryptMetadata(keyId) + .Property("policyNumber", BsonType.Int32, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperty("randomRegex*", BsonType.String, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + }); + + var expected = """ + { + "bsonType": "object", + "patternProperties": { + "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, @@ -64,6 +265,78 @@ public void EncryptedCollection_PropertyWithExpression_works_as_expected(BsonTyp AssertOutcomeCollectionBuilder(builder, expected); } + [Theory] + [InlineData(new[] {BsonType.Array, BsonType.String}, + CsfleEncryptionAlgorithm.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}, + CsfleEncryptionAlgorithm.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_PropertyWithMultipleBsonTypes_works_as_expected(IEnumerable bsonTypes, CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + { + Guid? keyId = keyString is null ? null : Guid.Parse(keyString); + var builder = new EncryptedCollectionBuilder(); + + builder.Property(p => p.MedicalRecords, bsonTypes, algorithm, keyId); + + var expected = $$""" + { + "bsonType": "object", + "properties": { + "medicalRecords": { + "encrypt": { + {{expectedContent}} + } + } + } + } + """; + + AssertOutcomeCollectionBuilder(builder, expected); + } + + [Theory] + [InlineData(BsonType.Array, + CsfleEncryptionAlgorithm.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, + CsfleEncryptionAlgorithm.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_PropertyWithString_works_as_expected(BsonType bsonType, CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + { + Guid? keyId = keyString is null ? null : Guid.Parse(keyString); + var builder = new EncryptedCollectionBuilder(); + + builder.Property("medicalRecords", bsonType, algorithm, keyId); + + var expected = $$""" + { + "bsonType": "object", + "properties": { + "medicalRecords": { + "encrypt": { + {{expectedContent}} + } + } + } + } + """; + + AssertOutcomeCollectionBuilder(builder, expected); + } + [Fact] public void BasicPropertyTest() { @@ -223,18 +496,28 @@ private void AssertOutcomeCollectionBuilder(EncryptedCollectionBuilder bui /** To test: * - Metadata - * - Property with expression - * - Property with string - * - Property with single bsonType - * - Property with multiple bsonType + * - *Property with expression + * - *Property with string + * - *Property with single bsonType + * - *Property with multiple bsonType + * - *Pattern property with string + * - *Pattern property with multiple bsonType + * * - Nested property with expression * - Nested property with string - * - Pattern property with string - * - Pattern property with multiple bsonType + * - Nested pattern property with expression * - Nested pattern property with string + * * - Multiple types in schema * - Property and pattern property together + * - Do it with BsonDocument....? + * + * ERRORS + * - No empty properties and empty pattern properties? + * - No empty schema + * - Wrong string + * - Empty bson type array */ internal class Patient From 4ddfdf821e68e42bf780ed23ada360df0799f0e6 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 2 May 2025 10:57:47 +0200 Subject: [PATCH 25/38] Corrected order --- .../Encryption/CsfleSchemaBuilderTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 7a71a6fe326..bb9481c5ecc 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -175,7 +175,7 @@ public void EncryptedCollection_PatternPropertyNested_works_as_expected() AssertOutcomeCollectionBuilder(builder, expected); } - [Fact] + [Fact] public void EncryptedCollection_PatternPropertyNestedWithString_works_as_expected() { Guid? keyId = Guid.Parse(_keyIdString); @@ -475,14 +475,14 @@ public void BasicPatternTest() AssertOutcomeBuilder(builder, expected); } - private void AssertOutcomeBuilder(CsfleSchemaBuilder builder, Dictionary expected) + private void AssertOutcomeBuilder(CsfleSchemaBuilder builder, Dictionary expectedSchema) { var builtSchema = builder.Build(); - expected.Should().HaveCount(builtSchema.Count); - foreach (var collectionNamespace in expected.Keys) + expectedSchema.Should().HaveCount(builtSchema.Count); + foreach (var collectionNamespace in expectedSchema.Keys) { - var parsed = BsonDocument.Parse(expected[collectionNamespace]); - parsed.Should().BeEquivalentTo(builtSchema[collectionNamespace]); + var parsed = BsonDocument.Parse(expectedSchema[collectionNamespace]); + builtSchema[collectionNamespace].Should().BeEquivalentTo(parsed); } } @@ -491,7 +491,7 @@ private void AssertOutcomeCollectionBuilder(EncryptedCollectionBuilder bui { var builtSchema = builder.Build(); var expectedSchema = BsonDocument.Parse(expected); - expectedSchema.Should().BeEquivalentTo(builtSchema); + builtSchema.Should().BeEquivalentTo(expectedSchema); } /** To test: From f31c705f2ee60fe20c9866bc2caca6c0016b613a Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 2 May 2025 14:27:42 +0200 Subject: [PATCH 26/38] Improved tests --- .../Encryption/CsfleSchemaBuilderTests.cs | 281 ++++++++++++++---- 1 file changed, 224 insertions(+), 57 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index bb9481c5ecc..2bf90fc2f93 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -26,7 +26,80 @@ namespace MongoDB.Driver.Tests.Encryption public class CsfleSchemaBuilderTests { private const string _keyIdString = "6f4af470-00d1-401f-ac39-f45902a0c0c8"; - private static Guid _keyIdExample = Guid.Parse(_keyIdString); //TODO Check if I should remove this + private static Guid _keyId = Guid.Parse(_keyIdString); + + [Fact] + public void BasicPropertyTest() + { + const string collectionName = "medicalRecords.patients"; + + var builder = CsfleSchemaBuilder.Create(schemaBuilder => + { + schemaBuilder.Encrypt(collectionName, builder => + { + builder + .EncryptMetadata(keyId: _keyId) + .Property(p => p.MedicalRecords, BsonType.Array, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .Property("bloodType", BsonType.String, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .Property(p => p.Ssn, BsonType.Int32, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .Property(p => p.Insurance, innerBuilder => + { + innerBuilder + .Property(i => i.PolicyNumber, BsonType.Int32, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + }); + } ); + }); + + var expected = new Dictionary + { + [collectionName] = """ + { + "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" + } + } + }, + } + """ + }; + + AssertOutcomeBuilder(builder, expected); + } + [Theory] [InlineData( @@ -148,23 +221,25 @@ public void EncryptedCollection_PatternPropertyNested_works_as_expected() { "bsonType": "object", "patternProperties": { - "bsonType": "object", - "encryptMetadata": { - "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] - }, - "properties": { - "policyNumber": { - "encrypt": { - "bsonType": "int", - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + "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" + }, + "patternProperties": { + "randomRegex*": { + "encrypt": { + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" + } } } } @@ -192,39 +267,34 @@ public void EncryptedCollection_PatternPropertyNestedWithString_works_as_expecte }); var expected = """ - { - "bsonType": "object", - "patternProperties": { - "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" - } - } - } - } - } - """; + { + "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); } @@ -337,6 +407,105 @@ public void EncryptedCollection_PropertyWithString_works_as_expected(BsonType bs AssertOutcomeCollectionBuilder(builder, expected); } + [Fact] + public void EncryptedCollection_PropertyNested_works_as_expected() + { + Guid? keyId = Guid.Parse(_keyIdString); + var builder = new EncryptedCollectionBuilder(); + + builder.Property(p => p.Insurance, innerBuilder => + { + innerBuilder + .EncryptMetadata(keyId) + .Property("policyNumber", BsonType.Int32, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperty("randomRegex*", BsonType.String, + CsfleEncryptionAlgorithm.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_PropertyNestedWithString_works_as_expected() + { + Guid? keyId = Guid.Parse(_keyIdString); + var builder = new EncryptedCollectionBuilder(); + + builder.Property("insurance", innerBuilder => + { + innerBuilder + .EncryptMetadata(keyId) + .Property("policyNumber", BsonType.Int32, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperty("randomRegex*", BsonType.String, + CsfleEncryptionAlgorithm.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 BasicPropertyTest() { @@ -347,7 +516,7 @@ public void BasicPropertyTest() schemaBuilder.Encrypt(collectionName, builder => { builder - .EncryptMetadata(keyId: _keyIdExample) + .EncryptMetadata(keyId: _keyId) .Property(p => p.MedicalRecords, BsonType.Array, CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) .Property("bloodType", BsonType.String, @@ -502,12 +671,10 @@ private void AssertOutcomeCollectionBuilder(EncryptedCollectionBuilder bui * - *Property with multiple bsonType * - *Pattern property with string * - *Pattern property with multiple bsonType - * - * - Nested property with expression - * - Nested property with string - - * - Nested pattern property with expression - * - Nested pattern property with string + * - *Nested property with expression + * - *Nested property with string + * - *Nested pattern property with expression + * - *Nested pattern property with string * * - Multiple types in schema * - Property and pattern property together From 0069760c9ad70572fc48396c679331ca51159d23 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 08:29:03 +0200 Subject: [PATCH 27/38] Added exceptions and improved testing --- .../Encryption/CsfleSchemaBuilder.cs | 18 +- .../Encryption/CsfleSchemaBuilderTests.cs | 281 ++++++++---------- 2 files changed, 143 insertions(+), 156 deletions(-) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs index c91943fa57b..828b17acfca 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -80,6 +80,11 @@ internal EncryptedCollectionBuilder() /// public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, CsfleEncryptionAlgorithm? 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 }, @@ -209,7 +214,18 @@ private static BsonDocument CreateEncryptDocument( CsfleEncryptionAlgorithm? algorithm = null, Guid? keyId = null) { + if (bsonTypes == null) + { + throw new ArgumentNullException(nameof(bsonTypes)); + } + var convertedBsonTypes = bsonTypes.Select(MapBsonTypeToString).ToList(); + + if (convertedBsonTypes.Count == 0) + { + throw new ArgumentException("At least one BSON type must be specified.", nameof(bsonTypes)); + } + BsonValue bsonTypeVal = convertedBsonTypes.Count == 1 ? convertedBsonTypes[0] : new BsonArray(convertedBsonTypes); @@ -292,7 +308,7 @@ private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorith } /// - /// The type of possible encryption algorithms. //TODO Maybe we need a more generic name? + /// The type of possible encryption algorithms. //TODO Maybe we need a more generic name but EncryptionAlgorithm is already taken (it's a superset of these values) /// public enum CsfleEncryptionAlgorithm { diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 2bf90fc2f93..6e75dbceef7 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -28,8 +28,8 @@ public class CsfleSchemaBuilderTests private const string _keyIdString = "6f4af470-00d1-401f-ac39-f45902a0c0c8"; private static Guid _keyId = Guid.Parse(_keyIdString); - [Fact] - public void BasicPropertyTest() + [Fact] + public void CsfleSchemaBuilder_works_as_expected() { const string collectionName = "medicalRecords.patients"; @@ -50,7 +50,18 @@ public void BasicPropertyTest() innerBuilder .Property(i => i.PolicyNumber, BsonType.Int32, CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + }) + .PatternProperty("_PIIString$", BsonType.String, CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperty("_PIIArray$", BsonType.Array, CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .PatternProperty(p => p.Insurance, innerBuilder => + { + innerBuilder + .PatternProperty("_PIIString$", BsonType.String, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + .PatternProperty("_PIINumber$", BsonType.Int32, + algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); }); + } ); }); @@ -93,13 +104,100 @@ public void BasicPropertyTest() } } }, + "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", + }, + }, + }, + }, + }, } """ }; - AssertOutcomeBuilder(builder, expected); + AssertOutcomeCsfleSchemaBuilder(builder, expected); } + [Fact] + public void CsfleSchemaBuilder_WithMultipleTypes_works_as_expected() + { + const string patientCollectionName = "medicalRecords.patients"; + const string testClassCollectionName = "test.class"; + + var builder = CsfleSchemaBuilder.Create(schemaBuilder => + { + schemaBuilder.Encrypt(patientCollectionName, builder => + { + builder + .EncryptMetadata(keyId: _keyId) + .Property(p => p.MedicalRecords, BsonType.Array, + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + }); + + schemaBuilder.Encrypt(testClassCollectionName, builder => + { + builder.Property(t => t.TestString, BsonType.String); + }); + }); + + var expected = new Dictionary + { + [patientCollectionName] = """ + { + "bsonType": "object", + "encryptMetadata": { + "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] + }, + "properties": { + "medicalRecords": { + "encrypt": { + "bsonType": "array", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" + } + }, + }, + } + """, + [testClassCollectionName] = """ + { + "bsonType": "object", + "properties": { + "TestString": { + "encrypt": { + "bsonType": "string", + } + }, + } + } + """ + }; + + AssertOutcomeCsfleSchemaBuilder(builder, expected); + } [Theory] [InlineData( @@ -505,146 +603,37 @@ public void EncryptedCollection_PropertyNestedWithString_works_as_expected() AssertOutcomeCollectionBuilder(builder, expected); } - [Fact] - public void BasicPropertyTest() + public void EncryptedCollection_Property_with_null_bson_types_throws() { - const string collectionName = "medicalRecords.patients"; - - var builder = CsfleSchemaBuilder.Create(schemaBuilder => - { - schemaBuilder.Encrypt(collectionName, builder => - { - builder - .EncryptMetadata(keyId: _keyId) - .Property(p => p.MedicalRecords, BsonType.Array, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .Property("bloodType", BsonType.String, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .Property(p => p.Ssn, BsonType.Int32, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - .Property(p => p.Insurance, innerBuilder => - { - innerBuilder - .Property(i => i.PolicyNumber, BsonType.Int32, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - }); - } ); - }); - - var expected = new Dictionary - { - [collectionName] = """ - { - "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" - } - } - }, - } - """ - }; + var builder = new EncryptedCollectionBuilder(); - AssertOutcomeBuilder(builder, expected); + var recordedException = Record.Exception(() => builder.Property("test", null)); + recordedException.Should().NotBeNull(); + recordedException.Should().BeOfType(); } [Fact] - public void BasicPatternTest() + public void EncryptedCollection_Property_with_empty_bson_types_throws() { - const string collectionName = "medicalRecords.patients"; + var builder = new EncryptedCollectionBuilder(); - var builder = CsfleSchemaBuilder.Create(schemaBuilder => - { - schemaBuilder.Encrypt(collectionName, builder => - { - builder - .PatternProperty("_PIIString$", BsonType.String, CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - .PatternProperty("_PIIArray$", BsonType.Array, CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) - .PatternProperty(p => p.Insurance, innerBuilder => - { - innerBuilder - .PatternProperty("_PIIString$", BsonType.String, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - .PatternProperty("_PIINumber$", BsonType.Int32, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); - }); - } ); - }); + var recordedException = Record.Exception(() => builder.Property("test", [])); + recordedException.Should().NotBeNull(); + recordedException.Should().BeOfType(); + } - var expected = new Dictionary - { - [collectionName] = """ - { - "bsonType": "object", - "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", - }, - }, - }, - }, - }, - } - """ - }; + [Fact] + public void EncryptedCollection_Metadata_with_empty_algorithm_and_key_throws() + { + var builder = new EncryptedCollectionBuilder(); - AssertOutcomeBuilder(builder, expected); + var recordedException = Record.Exception(() => builder.EncryptMetadata(null, null)); + recordedException.Should().NotBeNull(); + recordedException.Should().BeOfType(); } - private void AssertOutcomeBuilder(CsfleSchemaBuilder builder, Dictionary expectedSchema) + private void AssertOutcomeCsfleSchemaBuilder(CsfleSchemaBuilder builder, Dictionary expectedSchema) { var builtSchema = builder.Build(); expectedSchema.Should().HaveCount(builtSchema.Count); @@ -655,7 +644,6 @@ private void AssertOutcomeBuilder(CsfleSchemaBuilder builder, Dictionary(EncryptedCollectionBuilder builder, string expected) { var builtSchema = builder.Build(); @@ -663,29 +651,12 @@ private void AssertOutcomeCollectionBuilder(EncryptedCollectionBuilder bui builtSchema.Should().BeEquivalentTo(expectedSchema); } - /** To test: - * - Metadata - * - *Property with expression - * - *Property with string - * - *Property with single bsonType - * - *Property with multiple bsonType - * - *Pattern property with string - * - *Pattern property with multiple bsonType - * - *Nested property with expression - * - *Nested property with string - * - *Nested pattern property with expression - * - *Nested pattern property with string - * - * - Multiple types in schema - * - Property and pattern property together - * - Do it with BsonDocument....? - * - * ERRORS - * - No empty properties and empty pattern properties? - * - No empty schema - * - Wrong string - * - Empty bson type array - */ + internal class TestClass + { + public ObjectId Id { get; set; } + + public string TestString { get; set; } + } internal class Patient { From bd88e8bcc35c841493f2db500a97f0243e61417f Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 08:37:07 +0200 Subject: [PATCH 28/38] Moved builder to encryption project and removed CsfleEncryptionEnum (using the one that's already there) --- .../CsfleSchemaBuilder.cs | 38 +++-------- .../Encryption/CsfleSchemaBuilderTests.cs | 68 +++++++++---------- 2 files changed, 45 insertions(+), 61 deletions(-) rename src/{MongoDB.Driver/Encryption => MongoDB.Driver.Encryption}/CsfleSchemaBuilder.cs (89%) diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs similarity index 89% rename from src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs rename to src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs index 828b17acfca..a3d6072787b 100644 --- a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs @@ -78,7 +78,7 @@ internal EncryptedCollectionBuilder() /// /// //TODO /// - public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, CsfleEncryptionAlgorithm? algorithm = null) + public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, EncryptionAlgorithm? algorithm = null) { if (keyId is null && algorithm is null) { @@ -99,7 +99,7 @@ public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, public EncryptedCollectionBuilder PatternProperty( string pattern, BsonType bsonType, - CsfleEncryptionAlgorithm? algorithm = null, + EncryptionAlgorithm? algorithm = null, Guid? keyId = null) => PatternProperty(pattern, [bsonType], algorithm, keyId); @@ -109,7 +109,7 @@ public EncryptedCollectionBuilder PatternProperty( public EncryptedCollectionBuilder PatternProperty( string pattern, IEnumerable bsonTypes, - CsfleEncryptionAlgorithm? algorithm = null, + EncryptionAlgorithm? algorithm = null, Guid? keyId = null) { AddToPatternProperties(pattern, CreateEncryptDocument(bsonTypes, algorithm, keyId)); @@ -146,7 +146,7 @@ public EncryptedCollectionBuilder PatternProperty( public EncryptedCollectionBuilder Property( Expression> path, BsonType bsonType, - CsfleEncryptionAlgorithm? algorithm = null, + EncryptionAlgorithm? algorithm = null, Guid? keyId = null) => Property(path, [bsonType], algorithm, keyId); @@ -156,7 +156,7 @@ public EncryptedCollectionBuilder Property( public EncryptedCollectionBuilder Property( Expression> path, IEnumerable bsonTypes, - CsfleEncryptionAlgorithm? algorithm = null, + EncryptionAlgorithm? algorithm = null, Guid? keyId = null) => Property(new ExpressionFieldDefinition(path), bsonTypes, algorithm, keyId); @@ -166,7 +166,7 @@ public EncryptedCollectionBuilder Property( public EncryptedCollectionBuilder Property( FieldDefinition path, BsonType bsonType, - CsfleEncryptionAlgorithm? algorithm = null, + EncryptionAlgorithm? algorithm = null, Guid? keyId = null) => Property(path, [bsonType], algorithm, keyId); @@ -176,7 +176,7 @@ public EncryptedCollectionBuilder Property( public EncryptedCollectionBuilder Property( FieldDefinition path, IEnumerable bsonTypes, - CsfleEncryptionAlgorithm? algorithm = null, + EncryptionAlgorithm? algorithm = null, Guid? keyId = null) { var fieldName = path.Render(_args).FieldName; @@ -211,7 +211,7 @@ public EncryptedCollectionBuilder Property( private static BsonDocument CreateEncryptDocument( IEnumerable bsonTypes, - CsfleEncryptionAlgorithm? algorithm = null, + EncryptionAlgorithm? algorithm = null, Guid? keyId = null) { if (bsonTypes == null) @@ -296,30 +296,14 @@ private void AddToProperties(string field, BsonDocument document) }; } - private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorithm algorithm) + private static string MapCsfleEncyptionAlgorithmToString(EncryptionAlgorithm algorithm) { return algorithm switch { - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random => "AEAD_AES_256_CBC_HMAC_SHA_512-Random", - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic => "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + 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)) }; } } - - /// - /// The type of possible encryption algorithms. //TODO Maybe we need a more generic name but EncryptionAlgorithm is already taken (it's a superset of these values) - /// - public enum CsfleEncryptionAlgorithm - { - /// - /// Randomized encryption algorithm. - /// - AEAD_AES_256_CBC_HMAC_SHA_512_Random, - - /// - /// Deterministic encryption algorithm. - /// - AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic - } } \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 6e75dbceef7..db72a3d6ac4 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -40,26 +40,26 @@ public void CsfleSchemaBuilder_works_as_expected() builder .EncryptMetadata(keyId: _keyId) .Property(p => p.MedicalRecords, BsonType.Array, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) .Property("bloodType", BsonType.String, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + algorithm: EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) .Property(p => p.Ssn, BsonType.Int32, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) .Property(p => p.Insurance, innerBuilder => { innerBuilder .Property(i => i.PolicyNumber, BsonType.Int32, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); }) - .PatternProperty("_PIIString$", BsonType.String, CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) - .PatternProperty("_PIIArray$", BsonType.Array, CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + .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, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) .PatternProperty("_PIINumber$", BsonType.Int32, - algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + algorithm: EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); }); } ); @@ -155,7 +155,7 @@ public void CsfleSchemaBuilder_WithMultipleTypes_works_as_expected() builder .EncryptMetadata(keyId: _keyId) .Property(p => p.MedicalRecords, BsonType.Array, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); }); schemaBuilder.Encrypt(testClassCollectionName, builder => @@ -201,14 +201,14 @@ public void CsfleSchemaBuilder_WithMultipleTypes_works_as_expected() [Theory] [InlineData( - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + 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(CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + 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(); @@ -229,7 +229,7 @@ public void EncryptedCollection_Metadata_works_as_expected(CsfleEncryptionAlgori [Theory] [InlineData(BsonType.Array, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, null, """ "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)] [InlineData(BsonType.Array, @@ -237,10 +237,10 @@ public void EncryptedCollection_Metadata_works_as_expected(CsfleEncryptionAlgori _keyIdString, """ "bsonType": "array", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)] [InlineData(BsonType.Array, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + 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, CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + 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(); @@ -265,7 +265,7 @@ public void EncryptedCollection_PatternProperty_works_as_expected(BsonType bsonT [Theory] [InlineData(new[] {BsonType.Array, BsonType.String}, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + 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}, @@ -273,10 +273,10 @@ public void EncryptedCollection_PatternProperty_works_as_expected(BsonType bsonT _keyIdString, """ "bsonType": ["array", "string"], "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)] [InlineData(new[] {BsonType.Array, BsonType.String}, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + 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_PatternPropertyWithMultipleBsonTypes_works_as_expected(IEnumerable bsonTypes, CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + public void EncryptedCollection_PatternPropertyWithMultipleBsonTypes_works_as_expected(IEnumerable bsonTypes, EncryptionAlgorithm? algorithm, string keyString, string expectedContent) { Guid? keyId = keyString is null ? null : Guid.Parse(keyString); var builder = new EncryptedCollectionBuilder(); @@ -310,9 +310,9 @@ public void EncryptedCollection_PatternPropertyNested_works_as_expected() innerBuilder .EncryptMetadata(keyId) .Property("policyNumber", BsonType.Int32, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) .PatternProperty("randomRegex*", BsonType.String, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); }); var expected = """ @@ -359,9 +359,9 @@ public void EncryptedCollection_PatternPropertyNestedWithString_works_as_expecte innerBuilder .EncryptMetadata(keyId) .Property("policyNumber", BsonType.Int32, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) .PatternProperty("randomRegex*", BsonType.String, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); }); var expected = """ @@ -399,7 +399,7 @@ public void EncryptedCollection_PatternPropertyNestedWithString_works_as_expecte [Theory] [InlineData(BsonType.Array, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, null, """ "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)] [InlineData(BsonType.Array, @@ -407,10 +407,10 @@ public void EncryptedCollection_PatternPropertyNestedWithString_works_as_expecte _keyIdString, """ "bsonType": "array", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)] [InlineData(BsonType.Array, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + 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_PropertyWithExpression_works_as_expected(BsonType bsonType, CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + public void EncryptedCollection_PropertyWithExpression_works_as_expected(BsonType bsonType, EncryptionAlgorithm? algorithm, string keyString, string expectedContent) { Guid? keyId = keyString is null ? null : Guid.Parse(keyString); var builder = new EncryptedCollectionBuilder(); @@ -435,7 +435,7 @@ public void EncryptedCollection_PropertyWithExpression_works_as_expected(BsonTyp [Theory] [InlineData(new[] {BsonType.Array, BsonType.String}, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + 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}, @@ -443,10 +443,10 @@ public void EncryptedCollection_PropertyWithExpression_works_as_expected(BsonTyp _keyIdString, """ "bsonType": ["array", "string"], "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)] [InlineData(new[] {BsonType.Array, BsonType.String}, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + 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_PropertyWithMultipleBsonTypes_works_as_expected(IEnumerable bsonTypes, CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + public void EncryptedCollection_PropertyWithMultipleBsonTypes_works_as_expected(IEnumerable bsonTypes, EncryptionAlgorithm? algorithm, string keyString, string expectedContent) { Guid? keyId = keyString is null ? null : Guid.Parse(keyString); var builder = new EncryptedCollectionBuilder(); @@ -471,7 +471,7 @@ public void EncryptedCollection_PropertyWithMultipleBsonTypes_works_as_expected( [Theory] [InlineData(BsonType.Array, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, null, """ "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)] [InlineData(BsonType.Array, @@ -479,10 +479,10 @@ public void EncryptedCollection_PropertyWithMultipleBsonTypes_works_as_expected( _keyIdString, """ "bsonType": "array", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)] [InlineData(BsonType.Array, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, + 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_PropertyWithString_works_as_expected(BsonType bsonType, CsfleEncryptionAlgorithm? algorithm, string keyString, string expectedContent) + public void EncryptedCollection_PropertyWithString_works_as_expected(BsonType bsonType, EncryptionAlgorithm? algorithm, string keyString, string expectedContent) { Guid? keyId = keyString is null ? null : Guid.Parse(keyString); var builder = new EncryptedCollectionBuilder(); @@ -516,9 +516,9 @@ public void EncryptedCollection_PropertyNested_works_as_expected() innerBuilder .EncryptMetadata(keyId) .Property("policyNumber", BsonType.Int32, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) .PatternProperty("randomRegex*", BsonType.String, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); }); var expected = """ @@ -565,9 +565,9 @@ public void EncryptedCollection_PropertyNestedWithString_works_as_expected() innerBuilder .EncryptMetadata(keyId) .Property("policyNumber", BsonType.Int32, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) .PatternProperty("randomRegex*", BsonType.String, - CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); }); var expected = """ From 20379df9ab0c3626167fafccaee3ce15be197654 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 08:52:25 +0200 Subject: [PATCH 29/38] Put bsontypes to be nullable and removed unsupported bsonTypes --- .../CsfleSchemaBuilder.cs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs index a3d6072787b..4000a30cd9b 100644 --- a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs @@ -98,17 +98,17 @@ public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, /// public EncryptedCollectionBuilder PatternProperty( string pattern, - BsonType bsonType, + BsonType? bsonType = null, EncryptionAlgorithm? algorithm = null, Guid? keyId = null) - => PatternProperty(pattern, [bsonType], algorithm, keyId); + => PatternProperty(pattern, bsonType is null? [] : [bsonType.Value], algorithm, keyId); /// /// //TODO /// public EncryptedCollectionBuilder PatternProperty( string pattern, - IEnumerable bsonTypes, + IEnumerable bsonTypes = null, EncryptionAlgorithm? algorithm = null, Guid? keyId = null) { @@ -145,17 +145,17 @@ public EncryptedCollectionBuilder PatternProperty( /// public EncryptedCollectionBuilder Property( Expression> path, - BsonType bsonType, + BsonType? bsonType = null, EncryptionAlgorithm? algorithm = null, Guid? keyId = null) - => Property(path, [bsonType], algorithm, keyId); + => Property(path, bsonType is null? null : [bsonType.Value], algorithm, keyId); /// /// //TODO /// public EncryptedCollectionBuilder Property( Expression> path, - IEnumerable bsonTypes, + IEnumerable bsonTypes = null, EncryptionAlgorithm? algorithm = null, Guid? keyId = null) => Property(new ExpressionFieldDefinition(path), bsonTypes, algorithm, keyId); @@ -165,17 +165,17 @@ public EncryptedCollectionBuilder Property( /// public EncryptedCollectionBuilder Property( FieldDefinition path, - BsonType bsonType, + BsonType? bsonType = null, EncryptionAlgorithm? algorithm = null, Guid? keyId = null) - => Property(path, [bsonType], algorithm, keyId); + => Property(path, bsonType is null? null : [bsonType.Value], algorithm, keyId); /// /// //TODO /// public EncryptedCollectionBuilder Property( FieldDefinition path, - IEnumerable bsonTypes, + IEnumerable bsonTypes = null, EncryptionAlgorithm? algorithm = null, Guid? keyId = null) { @@ -210,7 +210,7 @@ public EncryptedCollectionBuilder Property( internal BsonDocument Build() => _schema; private static BsonDocument CreateEncryptDocument( - IEnumerable bsonTypes, + IEnumerable bsonTypes = null, EncryptionAlgorithm? algorithm = null, Guid? keyId = null) { @@ -268,7 +268,7 @@ private void AddToProperties(string field, BsonDocument document) properties[field] = document; } - private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstTypeFilterOperation, do we have a common place where this could go? + private static string MapBsonTypeToString(BsonType type) { return type switch { @@ -283,16 +283,12 @@ private void AddToProperties(string field, BsonDocument document) 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)) + _ => throw new ArgumentException($"Unsupported BSON type: {type}.", nameof(type)) }; } From a8390d3a5a2817c88c9080ced4e6a24ce23a1d24 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 09:11:00 +0200 Subject: [PATCH 30/38] Corrected test naming --- .../Encryption/CsfleSchemaBuilderTests.cs | 30 +++++++------------ ...ncryptionCreateCollectionExceptionTests.cs | 2 +- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index db72a3d6ac4..a18aa1b5737 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -143,7 +143,7 @@ public void CsfleSchemaBuilder_works_as_expected() } [Fact] - public void CsfleSchemaBuilder_WithMultipleTypes_works_as_expected() + public void CsfleSchemaBuilder_with_multiple_types_works_as_expected() { const string patientCollectionName = "medicalRecords.patients"; const string testClassCollectionName = "test.class"; @@ -276,7 +276,7 @@ public void EncryptedCollection_PatternProperty_works_as_expected(BsonType bsonT 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_PatternPropertyWithMultipleBsonTypes_works_as_expected(IEnumerable bsonTypes, EncryptionAlgorithm? algorithm, string keyString, string expectedContent) + public void EncryptedCollection_PatternProperty_with_multiple_bson_types_works_as_expected(IEnumerable bsonTypes, EncryptionAlgorithm? algorithm, string keyString, string expectedContent) { Guid? keyId = keyString is null ? null : Guid.Parse(keyString); var builder = new EncryptedCollectionBuilder(); @@ -300,7 +300,7 @@ public void EncryptedCollection_PatternPropertyWithMultipleBsonTypes_works_as_ex } [Fact] - public void EncryptedCollection_PatternPropertyNested_works_as_expected() + public void EncryptedCollection_PatternProperty_nested_works_as_expected() { Guid? keyId = Guid.Parse(_keyIdString); var builder = new EncryptedCollectionBuilder(); @@ -349,7 +349,7 @@ public void EncryptedCollection_PatternPropertyNested_works_as_expected() } [Fact] - public void EncryptedCollection_PatternPropertyNestedWithString_works_as_expected() + public void EncryptedCollection_PatternProperty_nested_with_string_works_as_expected() { Guid? keyId = Guid.Parse(_keyIdString); var builder = new EncryptedCollectionBuilder(); @@ -410,7 +410,7 @@ public void EncryptedCollection_PatternPropertyNestedWithString_works_as_expecte 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_PropertyWithExpression_works_as_expected(BsonType bsonType, EncryptionAlgorithm? algorithm, string keyString, string expectedContent) + 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(); @@ -446,7 +446,7 @@ public void EncryptedCollection_PropertyWithExpression_works_as_expected(BsonTyp 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_PropertyWithMultipleBsonTypes_works_as_expected(IEnumerable bsonTypes, EncryptionAlgorithm? algorithm, string keyString, string expectedContent) + public void EncryptedCollection_Property_with_multiple_bson_types_works_as_expected(IEnumerable bsonTypes, EncryptionAlgorithm? algorithm, string keyString, string expectedContent) { Guid? keyId = keyString is null ? null : Guid.Parse(keyString); var builder = new EncryptedCollectionBuilder(); @@ -482,7 +482,7 @@ public void EncryptedCollection_PropertyWithMultipleBsonTypes_works_as_expected( 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_PropertyWithString_works_as_expected(BsonType bsonType, EncryptionAlgorithm? algorithm, string keyString, string expectedContent) + 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(); @@ -505,8 +505,8 @@ public void EncryptedCollection_PropertyWithString_works_as_expected(BsonType bs AssertOutcomeCollectionBuilder(builder, expected); } - [Fact] - public void EncryptedCollection_PropertyNested_works_as_expected() + [Fact] + public void EncryptedCollection_Property_nested_works_as_expected() { Guid? keyId = Guid.Parse(_keyIdString); var builder = new EncryptedCollectionBuilder(); @@ -555,7 +555,7 @@ public void EncryptedCollection_PropertyNested_works_as_expected() } [Fact] - public void EncryptedCollection_PropertyNestedWithString_works_as_expected() + public void EncryptedCollection_Property_nested_with_string_works_as_expected() { Guid? keyId = Guid.Parse(_keyIdString); var builder = new EncryptedCollectionBuilder(); @@ -603,16 +603,6 @@ public void EncryptedCollection_PropertyNestedWithString_works_as_expected() AssertOutcomeCollectionBuilder(builder, expected); } - [Fact] - public void EncryptedCollection_Property_with_null_bson_types_throws() - { - var builder = new EncryptedCollectionBuilder(); - - var recordedException = Record.Exception(() => builder.Property("test", null)); - recordedException.Should().NotBeNull(); - recordedException.Should().BeOfType(); - } - [Fact] public void EncryptedCollection_Property_with_empty_bson_types_throws() { diff --git a/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs b/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs index 5f282702bb0..9e53c873b86 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs @@ -1 +1 @@ - \ No newline at end of file +//TODO Do we need to keep this empty file...? \ No newline at end of file From d30739d37803c12264bff405e321abcdb46e7c05 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 09:59:24 +0200 Subject: [PATCH 31/38] Fixed API for overloads --- .../CsfleSchemaBuilder.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs index 4000a30cd9b..6c703853609 100644 --- a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs @@ -98,10 +98,10 @@ public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, /// public EncryptedCollectionBuilder PatternProperty( string pattern, - BsonType? bsonType = null, + BsonType bsonType, EncryptionAlgorithm? algorithm = null, Guid? keyId = null) - => PatternProperty(pattern, bsonType is null? [] : [bsonType.Value], algorithm, keyId); + => PatternProperty(pattern, [bsonType], algorithm, keyId); /// /// //TODO @@ -145,10 +145,10 @@ public EncryptedCollectionBuilder PatternProperty( /// public EncryptedCollectionBuilder Property( Expression> path, - BsonType? bsonType = null, + BsonType bsonType, EncryptionAlgorithm? algorithm = null, Guid? keyId = null) - => Property(path, bsonType is null? null : [bsonType.Value], algorithm, keyId); + => Property(path, [bsonType], algorithm, keyId); /// /// //TODO @@ -165,10 +165,10 @@ public EncryptedCollectionBuilder Property( /// public EncryptedCollectionBuilder Property( FieldDefinition path, - BsonType? bsonType = null, + BsonType bsonType, EncryptionAlgorithm? algorithm = null, Guid? keyId = null) - => Property(path, bsonType is null? null : [bsonType.Value], algorithm, keyId); + => Property(path, [bsonType], algorithm, keyId); /// /// //TODO @@ -214,27 +214,27 @@ private static BsonDocument CreateEncryptDocument( EncryptionAlgorithm? algorithm = null, Guid? keyId = null) { - if (bsonTypes == null) + BsonValue bsonTypeVal = null; + + if (bsonTypes != null) { - throw new ArgumentNullException(nameof(bsonTypes)); - } + var convertedBsonTypes = bsonTypes.Select(MapBsonTypeToString).ToList(); - var convertedBsonTypes = bsonTypes.Select(MapBsonTypeToString).ToList(); + if (convertedBsonTypes.Count == 0) + { + throw new ArgumentException("At least one BSON type must be specified.", nameof(bsonTypes)); + } - 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); } - BsonValue bsonTypeVal = convertedBsonTypes.Count == 1 - ? convertedBsonTypes[0] - : new BsonArray(convertedBsonTypes); - return new BsonDocument { { "encrypt", new BsonDocument { - { "bsonType", bsonTypeVal }, + { "bsonType", () => bsonTypeVal, bsonTypeVal is not null }, { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null }, { "keyId", From fa3143cf2ca72544ca093032a0b049ea1db2cc4c Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 10:36:22 +0200 Subject: [PATCH 32/38] Added exception for empty schema --- .../CsfleSchemaBuilder.cs | 10 ++++++++- .../Encryption/CsfleSchemaBuilderTests.cs | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs index 6c703853609..808da558855 100644 --- a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs @@ -57,7 +57,15 @@ public CsfleSchemaBuilder Encrypt(string collectionNamespace, Action /// Builds and returns the resulting CSFLE schema. /// - public IDictionary Build() => _schemas; + public IDictionary Build() + { + if (!_schemas.Any()) + { + throw new InvalidOperationException("No schemas were added. Use Encrypt to add a schema."); + } + + return _schemas; + } } /// diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index a18aa1b5737..869434cd145 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -199,6 +199,20 @@ public void CsfleSchemaBuilder_with_multiple_types_works_as_expected() 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(); + } + [Theory] [InlineData( EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, @@ -264,6 +278,10 @@ public void EncryptedCollection_PatternProperty_works_as_expected(BsonType bsonT } [Theory] + [InlineData(null, + null, + null, + "")] [InlineData(new[] {BsonType.Array, BsonType.String}, EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, null, @@ -434,6 +452,10 @@ public void EncryptedCollection_Property_with_expression_works_as_expected(BsonT } [Theory] + [InlineData(null, + null, + null, + "")] [InlineData(new[] {BsonType.Array, BsonType.String}, EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random, null, From 134622e2916a99d44bef2f3712e4c8c7804a57f1 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 10:55:34 +0200 Subject: [PATCH 33/38] Added docs --- .../CsfleSchemaBuilder.cs | 93 +++++++++++++++---- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs index 808da558855..b8ff38f7c44 100644 --- a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs @@ -23,7 +23,7 @@ namespace MongoDB.Driver.Encryption { /// - /// //TODO + /// A builder class for creating Client-Side Field Level Encryption (CSFLE) schemas. /// public class CsfleSchemaBuilder { @@ -34,8 +34,9 @@ private CsfleSchemaBuilder() } /// - /// //TODO + /// Creates a new instance of the and configures it using the provided action. /// + /// An action to configure the schema builder. public static CsfleSchemaBuilder Create(Action configure) { var builder = new CsfleSchemaBuilder(); @@ -44,8 +45,12 @@ public static CsfleSchemaBuilder Create(Action configure) } /// - /// //TODO + /// Adds an encrypted collection schema for a specific collection namespace. /// + /// The type of the document in the collection. + /// The namespace of the collection. + /// An action to configure the encrypted collection builder. + /// The current instance. public CsfleSchemaBuilder Encrypt(string collectionNamespace, Action> configure) { var builder = new EncryptedCollectionBuilder(); @@ -69,30 +74,31 @@ public IDictionary Build() } /// - /// //TODO + /// A builder class for creating encrypted collection schemas. /// + /// The type of the document in the collection. public class EncryptedCollectionBuilder { private readonly BsonDocument _schema = new("bsonType", "object"); private readonly RenderArgs _args = new(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); - /// - /// //TODO - /// internal EncryptedCollectionBuilder() { } /// - /// //TODO + /// Configures encryption metadata for the collection. /// + /// The key ID to use for encryption. + /// The encryption algorithm to use. + /// The current instance. public EncryptedCollectionBuilder 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 }, @@ -102,8 +108,13 @@ public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, } /// - /// //TODO + /// Adds a pattern property to the schema with encryption settings. /// + /// The regex pattern for the property. + /// The BSON type of the property. + /// The encryption algorithm to use. + /// The key ID to use for encryption. + /// The current instance. public EncryptedCollectionBuilder PatternProperty( string pattern, BsonType bsonType, @@ -112,8 +123,13 @@ public EncryptedCollectionBuilder PatternProperty( => PatternProperty(pattern, [bsonType], algorithm, keyId); /// - /// //TODO + /// Adds a pattern property to the schema with encryption settings. /// + /// The regex pattern for the property. + /// The BSON types of the property. + /// The encryption algorithm to use. + /// The key ID to use for encryption. + /// The current instance. public EncryptedCollectionBuilder PatternProperty( string pattern, IEnumerable bsonTypes = null, @@ -125,16 +141,24 @@ public EncryptedCollectionBuilder PatternProperty( } /// - /// //TODO + /// Adds a nested pattern property to the schema. /// + /// The type of the nested field. + /// The field. + /// An action to configure the nested builder. + /// The current instance. public EncryptedCollectionBuilder PatternProperty( Expression> path, Action> configure) => PatternProperty(new ExpressionFieldDefinition(path), configure); /// - /// //TODO + /// Adds a nested pattern property to the schema. /// + /// The type of the nested field. + /// The field. + /// An action to configure the nested builder. + /// The current instance. public EncryptedCollectionBuilder PatternProperty( FieldDefinition path, Action> configure) @@ -149,8 +173,14 @@ public EncryptedCollectionBuilder PatternProperty( } /// - /// //TODO + /// Adds a property to the schema with encryption settings. /// + /// The type of the field. + /// The field. + /// The BSON type of the property. + /// The encryption algorithm to use. + /// The key ID to use for encryption. + /// The current instance. public EncryptedCollectionBuilder Property( Expression> path, BsonType bsonType, @@ -159,8 +189,14 @@ public EncryptedCollectionBuilder Property( => Property(path, [bsonType], algorithm, keyId); /// - /// //TODO + /// Adds a property to the schema with encryption settings. /// + /// The type of the field. + /// The field. + /// The BSON types of the property. + /// The encryption algorithm to use. + /// The key ID to use for encryption. + /// The current instance. public EncryptedCollectionBuilder Property( Expression> path, IEnumerable bsonTypes = null, @@ -169,8 +205,13 @@ public EncryptedCollectionBuilder Property( => Property(new ExpressionFieldDefinition(path), bsonTypes, algorithm, keyId); /// - /// //TODO + /// Adds a property to the schema with encryption settings. /// + /// The field. + /// The BSON type of the property. + /// The encryption algorithm to use. + /// The key ID to use for encryption. + /// The current instance. public EncryptedCollectionBuilder Property( FieldDefinition path, BsonType bsonType, @@ -179,8 +220,13 @@ public EncryptedCollectionBuilder Property( => Property(path, [bsonType], algorithm, keyId); /// - /// //TODO + /// Adds a property to the schema with encryption settings. /// + /// The field. + /// The BSON types of the property. + /// The encryption algorithm to use. + /// The key ID to use for encryption. + /// The current instance. public EncryptedCollectionBuilder Property( FieldDefinition path, IEnumerable bsonTypes = null, @@ -193,16 +239,25 @@ public EncryptedCollectionBuilder Property( } /// - /// //TODO + /// Adds a nested property to the schema. /// + /// The type of the nested field. + /// The field. + /// An action to configure the nested builder. + /// The current instance. public EncryptedCollectionBuilder Property( Expression> path, Action> configure) => Property(new ExpressionFieldDefinition(path), configure); + /// - /// //TODO + /// Adds a nested property to the schema. /// + /// The type of the nested field. + /// The field. + /// An action to configure the nested builder. + /// The current instance. public EncryptedCollectionBuilder Property( FieldDefinition path, Action> configure) From 6098c29d10b0d44fd7b42cd82a58d4e156cc9b9a Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 11:29:24 +0200 Subject: [PATCH 34/38] Small fix --- .../Encryption/CsfleSchemaBuilderTests.cs | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 869434cd145..2ba14f85d9d 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -25,17 +25,16 @@ 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() { - const string collectionName = "medicalRecords.patients"; - var builder = CsfleSchemaBuilder.Create(schemaBuilder => { - schemaBuilder.Encrypt(collectionName, builder => + schemaBuilder.Encrypt(_collectionNamespace, builder => { builder .EncryptMetadata(keyId: _keyId) @@ -67,7 +66,7 @@ public void CsfleSchemaBuilder_works_as_expected() var expected = new Dictionary { - [collectionName] = """ + [_collectionNamespace.FullName] = """ { "bsonType": "object", "encryptMetadata": { @@ -145,12 +144,11 @@ public void CsfleSchemaBuilder_works_as_expected() [Fact] public void CsfleSchemaBuilder_with_multiple_types_works_as_expected() { - const string patientCollectionName = "medicalRecords.patients"; - const string testClassCollectionName = "test.class"; + var testCollectionNamespace = CollectionNamespace.FromFullName("test.class"); var builder = CsfleSchemaBuilder.Create(schemaBuilder => { - schemaBuilder.Encrypt(patientCollectionName, builder => + schemaBuilder.Encrypt(_collectionNamespace, builder => { builder .EncryptMetadata(keyId: _keyId) @@ -158,7 +156,7 @@ public void CsfleSchemaBuilder_with_multiple_types_works_as_expected() EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); }); - schemaBuilder.Encrypt(testClassCollectionName, builder => + schemaBuilder.Encrypt(_collectionNamespace, builder => { builder.Property(t => t.TestString, BsonType.String); }); @@ -166,34 +164,34 @@ public void CsfleSchemaBuilder_with_multiple_types_works_as_expected() var expected = new Dictionary { - [patientCollectionName] = """ - { - "bsonType": "object", - "encryptMetadata": { - "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] - }, - "properties": { - "medicalRecords": { - "encrypt": { - "bsonType": "array", - "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" - } - }, - }, - } - """, - [testClassCollectionName] = """ - { - "bsonType": "object", - "properties": { - "TestString": { - "encrypt": { - "bsonType": "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); @@ -277,6 +275,8 @@ public void EncryptedCollection_PatternProperty_works_as_expected(BsonType bsonT AssertOutcomeCollectionBuilder(builder, expected); } + //TODO Remember about using the same algorithm... + [Theory] [InlineData(null, null, From 7a4f46a56f5628cafaa527e5c56c7dc9f97efbce Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 11:29:31 +0200 Subject: [PATCH 35/38] Fix --- src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs index b8ff38f7c44..ed369f49fa1 100644 --- a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs @@ -51,11 +51,11 @@ public static CsfleSchemaBuilder Create(Action configure) /// The namespace of the collection. /// An action to configure the encrypted collection builder. /// The current instance. - public CsfleSchemaBuilder Encrypt(string collectionNamespace, Action> configure) + public CsfleSchemaBuilder Encrypt(CollectionNamespace collectionNamespace, Action> configure) { var builder = new EncryptedCollectionBuilder(); configure(builder); - _schemas.Add(collectionNamespace, builder.Build()); + _schemas.Add(collectionNamespace.FullName, builder.Build()); return this; } From a25ad560ab2387c6929efde1b618d336bfd346a6 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 11:35:47 +0200 Subject: [PATCH 36/38] Removed unnecessary --- .../Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs index f05cc3f7ff9..f44ae90b500 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs @@ -59,7 +59,7 @@ public override BsonValue Render() } } - private string MapBsonTypeToString(BsonType type) //TODO Is this the only place where we do this conversion? + private string MapBsonTypeToString(BsonType type) { switch (type) { From 84ddfbd4851becd9c81310683c862e75b4f412d4 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 11:39:20 +0200 Subject: [PATCH 37/38] Name fix --- src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs index ed369f49fa1..b49eacb8147 100644 --- a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs +++ b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs @@ -102,7 +102,7 @@ public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, _schema["encryptMetadata"] = new BsonDocument { { "keyId", () => new BsonArray { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }, keyId is not null }, - { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null } + { "algorithm", () => MapCsfleEncryptionAlgorithmToString(algorithm!.Value), algorithm is not null } }; return this; } @@ -298,7 +298,7 @@ private static BsonDocument CreateEncryptDocument( { "encrypt", new BsonDocument { { "bsonType", () => bsonTypeVal, bsonTypeVal is not null }, - { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null }, + { "algorithm", () => MapCsfleEncryptionAlgorithmToString(algorithm!.Value), algorithm is not null }, { "keyId", () => new BsonArray(new[] { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }), @@ -355,7 +355,7 @@ private static string MapBsonTypeToString(BsonType type) }; } - private static string MapCsfleEncyptionAlgorithmToString(EncryptionAlgorithm algorithm) + private static string MapCsfleEncryptionAlgorithmToString(EncryptionAlgorithm algorithm) { return algorithm switch { From eca0ace7b393e112971fa425d437e47b7c891973 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 5 May 2025 11:41:29 +0200 Subject: [PATCH 38/38] Removed unnecessaty --- .../MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs index 2ba14f85d9d..90d8364b712 100644 --- a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -275,8 +275,6 @@ public void EncryptedCollection_PatternProperty_works_as_expected(BsonType bsonT AssertOutcomeCollectionBuilder(builder, expected); } - //TODO Remember about using the same algorithm... - [Theory] [InlineData(null, null,