diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs new file mode 100644 index 00000000000..306d3be2201 --- /dev/null +++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs @@ -0,0 +1,512 @@ +/* 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 +{ + /// + /// + /// + public class CsfleSchemaBuilder + { + private readonly Dictionary _typedBuilders = []; + private CsfleSchemaBuilder() + { + } + + /// + /// + /// + /// + /// + public static CsfleSchemaBuilder Create(Action configure) + { + var builder = new CsfleSchemaBuilder(); + configure(builder); + return builder; + } + + /// + /// + /// + /// + /// + /// + public void Encrypt(CollectionNamespace collectionNamespace, Action> configure) + { + var typedBuilder = new TypedBuilder(); + configure(typedBuilder); + _typedBuilders.Add(collectionNamespace.FullName, typedBuilder); + } + + /// + /// + /// + /// + public IReadOnlyDictionary Build() => _typedBuilders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Build()); + } + + /// + /// + /// + /// + public class ElementBuilder where TSelf : ElementBuilder + { + private protected CsfleEncryptionAlgorithm? _algorithm; + private protected Guid? _keyId; + + /// + /// + /// + /// + /// + public TSelf WithKeyId(Guid keyId) + { + _keyId = keyId; + return (TSelf)this; + } + + /// + /// + /// + /// + /// + public TSelf WithAlgorithm(CsfleEncryptionAlgorithm algorithm) + { + _algorithm = algorithm; + return (TSelf)this; + } + + internal static BsonDocument GetEncryptBsonDocument(Guid? keyId, CsfleEncryptionAlgorithm? algorithm, List bsonTypes) + { + var bsonType = bsonTypes?.First(); //TODO need to support multiple types + + 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 + }, + }; + } + + private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstTypeFilterOperation, do we have a common place where this could go? + { + return type switch + { + BsonType.Array => "array", + BsonType.Binary => "binData", + BsonType.Boolean => "bool", + BsonType.DateTime => "date", + BsonType.Decimal128 => "decimal", + BsonType.Document => "object", + BsonType.Double => "double", + BsonType.Int32 => "int", + BsonType.Int64 => "long", + BsonType.JavaScript => "javascript", + BsonType.JavaScriptWithScope => "javascriptWithScope", + BsonType.MaxKey => "maxKey", + BsonType.MinKey => "minKey", + BsonType.Null => "null", + BsonType.ObjectId => "objectId", + BsonType.RegularExpression => "regex", + BsonType.String => "string", + BsonType.Symbol => "symbol", + BsonType.Timestamp => "timestamp", + BsonType.Undefined => "undefined", + _ => throw new ArgumentException($"Unexpected BSON type: {type}.", nameof(type)) + }; + } + + private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorithm algorithm) + { + return algorithm switch + { + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random => "AEAD_AES_256_CBC_HMAC_SHA_512-Random", + CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic => "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + _ => throw new ArgumentException($"Unexpected algorithm type: {algorithm}.", nameof(algorithm)) + }; + } + } + + /// + /// + /// + public class EncryptMetadataBuilder : ElementBuilder + { + internal BsonDocument Build() => new("encryptMetadata", GetEncryptBsonDocument(_keyId, _algorithm, null)); + } + + + /// + /// + /// + /// + public abstract class SinglePropertyBuilder : ElementBuilder> + { + internal abstract BsonDocument Build(RenderArgs args); + } + + /// + /// + /// + /// + /// + public abstract class SinglePropertyBuilder + : ElementBuilder + where TBuilder : SinglePropertyBuilder + { + private protected List _bsonTypes; + + /// + /// + /// + /// + /// + public TBuilder WithBsonType(BsonType bsonType) + { + _bsonTypes = [bsonType]; + return (TBuilder)this; + } + + /// + /// + /// + /// + /// + public TBuilder WithBsonTypes(IEnumerable bsonTypes) + { + _bsonTypes = [..bsonTypes]; + return (TBuilder)this; + } + + internal abstract BsonDocument Build(RenderArgs args); + } + + /// + /// + /// + /// + public class PropertyBuilder : SinglePropertyBuilder, TDocument> + { + private readonly FieldDefinition _path; + + /// + /// + /// + /// + public PropertyBuilder(FieldDefinition path) + { + _path = path; + } + + internal override BsonDocument Build(RenderArgs args) + { + return new BsonDocument(_path.Render(args).FieldName, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes))); + } + } + + /// + /// + /// + /// + public class PatternPropertyBuilder : SinglePropertyBuilder, TDocument> + { + private readonly string _pattern; + + /// + /// + /// + /// + public PatternPropertyBuilder(string pattern) + { + _pattern = pattern; + } + + internal override BsonDocument Build(RenderArgs args) + { + return new BsonDocument(_pattern, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes))); + } + } + + /// + /// + /// + public abstract class NestedPropertyBuilderBase + { + internal abstract BsonDocument Build(RenderArgs args); + } + + /// + /// + /// + /// + /// + public class NestedPropertyBuilder : NestedPropertyBuilderBase + { + private readonly FieldDefinition _path; + private readonly Action> _configure; + + /// + /// + /// + /// + /// + public NestedPropertyBuilder(FieldDefinition path, Action> configure) + { + _path = path; + _configure = configure; + } + + internal override BsonDocument Build(RenderArgs args) + { + var fieldBuilder = new TypedBuilder(); + _configure(fieldBuilder); + return new BsonDocument(_path.Render(args).FieldName, fieldBuilder.Build()); + } + } + + /// + /// + /// + public abstract class NestedPatternPropertyBuilderBase + { + internal abstract BsonDocument Build(RenderArgs args); + } + + /// + /// + /// + /// + /// + public class NestedPatternPropertyBuilder : NestedPatternPropertyBuilderBase + { + private readonly FieldDefinition _path; + private readonly Action> _configure; + + /// + /// + /// + /// + /// + public NestedPatternPropertyBuilder(FieldDefinition path, Action> configure) + { + _path = path; + _configure = configure; + } + + internal override BsonDocument Build(RenderArgs args) + { + var fieldBuilder = new TypedBuilder(); + _configure(fieldBuilder); + return new BsonDocument(_path.Render(args).FieldName, fieldBuilder.Build()); + } + } + + /// + /// + /// + public abstract class TypedBuilder + { + internal abstract BsonDocument Build(); + } + + /// + /// + /// + /// + public class TypedBuilder : TypedBuilder + { + private readonly List> _nestedProperties = []; + private readonly List> _nestedPatternProperties = []; + private readonly List> _properties = []; + private readonly List> _patternProperties = []; + private EncryptMetadataBuilder _metadata; + + /// + /// + /// + /// + public EncryptMetadataBuilder EncryptMetadata() + { + _metadata = new EncryptMetadataBuilder(); + return _metadata; + } + + /// + /// + /// + /// + /// + public PropertyBuilder Property(FieldDefinition path) + { + var property = new PropertyBuilder(path); + _properties.Add(property); + return property; + } + + /// + /// + /// + /// + /// + public PropertyBuilder Property(Expression> path) + { + return Property(new ExpressionFieldDefinition(path)); + } + + /// + /// + /// + /// + /// + public PatternPropertyBuilder PatternProperty(string pattern) + { + var property = new PatternPropertyBuilder(pattern); + _patternProperties.Add(property); + return property; + } + + /// + /// + /// + /// + /// + /// + public NestedPropertyBuilder NestedProperty(FieldDefinition path, Action> configure) + { + var nestedProperty = new NestedPropertyBuilder(path, configure); + _nestedProperties.Add(nestedProperty); + return nestedProperty; + } + + /// + /// + /// + /// + /// + /// + public NestedPropertyBuilder NestedProperty(Expression> path, Action> configure) + { + return NestedProperty(new ExpressionFieldDefinition(path), configure); + } + + /// + /// + /// + /// + /// + /// + public NestedPatternPropertyBuilder NestedPatternProperty(string pattern, Action> configure) + { + var nestedProperty = new NestedPatternPropertyBuilder(pattern, configure); + _nestedPatternProperties.Add(nestedProperty); + return nestedProperty; + } + + internal override BsonDocument Build() + { + var schema = new BsonDocument("bsonType", "object"); + + if (_metadata is not null) + { + schema.Merge(_metadata.Build()); + } + + var args = new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry); + + + BsonDocument properties = null; + BsonDocument patternProperties = null; + + if (_properties.Any()) + { + properties ??= new BsonDocument(); + + foreach (var property in _properties) + { + properties.Merge(property.Build(args)); + } + } + + if (_nestedProperties.Any()) + { + properties ??= new BsonDocument(); + + foreach (var nestedProperty in _nestedProperties) + { + properties.Merge(nestedProperty.Build(args)); + } + } + + if (_patternProperties.Any()) + { + patternProperties ??= new BsonDocument(); + + foreach (var patternProperty in _patternProperties) + { + patternProperties.Merge(patternProperty.Build(args)); + } + } + + if (_nestedPatternProperties.Any()) + { + patternProperties ??= new BsonDocument(); + + foreach (var nestedPatternProperty in _nestedPatternProperties) + { + patternProperties.Merge(nestedPatternProperty.Build(args)); + } + } + + if (properties != null) + { + schema.Add("properties", properties); + } + + if (patternProperties != null) + { + schema.Add("patternProperties", patternProperties); + } + + return schema; + } + } + + /// + /// The type of possible encryption algorithms. + /// + public enum CsfleEncryptionAlgorithm + { + /// + /// Randomized encryption algorithm. + /// + AEAD_AES_256_CBC_HMAC_SHA_512_Random, + + /// + /// Deterministic encryption algorithm. + /// + AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic + } +} \ No newline at end of file diff --git a/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..54b7a4ea865 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs @@ -0,0 +1,213 @@ +/* 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 + { + private readonly Guid _keyIdExample = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8"); + + [Fact] + public void BasicCompleteTest() + { + const string collectionName = "medicalRecords.patients"; + + var builder = CsfleSchemaBuilder.Create(schemaBuilder => + { + schemaBuilder.Encrypt(CollectionNamespace.FromFullName(collectionName), builder1 => + { + builder1.EncryptMetadata().WithKeyId(_keyIdExample); + + builder1.NestedProperty(p => p.Insurance, typedBuilder1 => + { + typedBuilder1.Property(i => i.PolicyNumber).WithBsonType(BsonType.Int32) + .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + }); + + builder1.Property(p => p.MedicalRecords).WithBsonType(BsonType.Array) + .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + builder1.Property("bloodType").WithBsonType(BsonType.String) + .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random); + builder1.Property(p => p.Ssn).WithBsonType(BsonType.Int32) + .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); + } ); + }); + + var expected = new Dictionary + { + [collectionName] = """ + { + "bsonType": "object", + "encryptMetadata": { + "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] + }, + "properties": { + "insurance": { + "bsonType": "object", + "properties": { + "policyNumber": { + "encrypt": { + "bsonType": "int", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + } + } + } + }, + "medicalRecords": { + "encrypt": { + "bsonType": "array", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" + } + }, + "bloodType": { + "encrypt": { + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" + } + }, + "ssn": { + "encrypt": { + "bsonType": "int", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + } + } + }, + } + """ + }; + + AssertOutcomeBuilder2(builder, expected); + } + + // [Fact] + // public void BasicPatternTest() + // { + // const string collectionName = "medicalRecords.patients"; + // + // var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder() + // .PatternProperty("_PIIString$", bsonType: BsonType.String, + // algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + // .PatternProperty("_PIIArray$", bsonType: BsonType.Array, + // algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random) + // .PatternProperty(p => p.Insurance, builder => builder + // .PatternProperty("_PIINumber$", bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + // .PatternProperty("_PIIString$", bsonType: BsonType.String, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) + // ); + // + // var encryptionSchemaBuilder = new CsfleSchemaBuilder() + // .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder); + // + // var expected = new Dictionary + // { + // [collectionName] = """ + // { + // "bsonType": "object", + // "patternProperties": { + // "_PIIString$": { + // "encrypt": { + // "bsonType": "string", + // "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + // }, + // }, + // "_PIIArray$": { + // "encrypt": { + // "bsonType": "array", + // "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", + // }, + // }, + // "insurance": { + // "bsonType": "object", + // "patternProperties": { + // "_PIINumber$": { + // "encrypt": { + // "bsonType": "int", + // "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + // }, + // }, + // "_PIIString$": { + // "encrypt": { + // "bsonType": "string", + // "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic", + // }, + // }, + // }, + // }, + // }, + // } + // """ + // }; + // + // AssertOutcomeBuilder(encryptionSchemaBuilder, expected); + // } + + private void AssertOutcomeBuilder2(CsfleSchemaBuilder builder, Dictionary expected) + { + var builtSchema = builder.Build(); + Assert.Equal(expected.Count, builtSchema.Count); + foreach (var collectionNamespace in expected.Keys) + { + var parsed = BsonDocument.Parse(expected[collectionNamespace]); + Assert.Equal(parsed, builtSchema[collectionNamespace]); + } + } + + + 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