From c58ce8e9d21af7adcbecc22652a647239f34b2a5 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/19] 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 83f411cd970867bbf29d210cafa6ba4ec269adb5 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/19] 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 2d12643d7c92f864d9fee13a10ead739566e2eb4 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/19] 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 ea79c91a4ce7eb1c3ca4e22b274610c2b2f121d8 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/19] 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 58e34d33edc9b75afae20684b899fd4ba467e070 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/19] 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 a2c6b6654eaeedd616483995d2607c29278d77b8 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/19] 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 d79cf8e3b7ec66c1aedf72bf46b305013b6ee3cd 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/19] 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 62f93750a0441c6001c0659dfc1a689ef00696bb 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/19] 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 64604a82a7cce6d163917f664e8a0309d42f7d96 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/19] 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 c40086747af1d2bf02d2f0f07a1a2241cae35b32 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/19] 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 8f4104cc96d51ad88583cae8e5e807863a391d37 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/19] 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 aa9d0c4db474c0a6d651c7f8e4d186499755bfba 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/19] 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 1c8e55b1a564912c599431312c3e878c5370162b 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/19] 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 659cfd6468e3914da2fceccd1eefbed00ec4a7fa 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/19] 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/19] 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 aa67a779ecb72f3c041cbcd60193bafc70003f53 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/19] 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 161953c8c1d98489479d60ee884fbc13a1d2838b 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/19] 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 4ebf9309fe4aca1e856060559846de307ff5d576 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/19] 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 e6a30bb25dafad4a64ec4a65e43839353937123a 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/19] 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; } }