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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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;