diff --git a/.evergreen/config.yml b/.evergreen/config.yml index f57ffb1e75..58e3378b38 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -26,11 +26,6 @@ timeout: args: [ls, -la] functions: - assume-test-secrets-ec2-role: - - command: ec2.assume_role - params: - role_arn: ${aws_test_secrets_role} - setup-system: # Executes clone and applies the submitted patch, if any - command: git.get_project @@ -598,6 +593,22 @@ functions: KMS_FAILPOINT_SERVER_RUNNING: "true" args: [*task-runner, evg-test-retry-kms-requests] + run-client-side-encryption-test: + - command: subprocess.exec + params: + binary: "bash" + env: + GO_BUILD_TAGS: cse + include_expansions_in_env: [AUTH, SSL, MONGODB_URI, TOPOLOGY, + MONGO_GO_DRIVER_COMPRESSOR] + args: [*task-runner, setup-test] + - command: subprocess.exec + type: test + retry_on_failure: true + params: + binary: "bash" + args: [*task-runner, evg-test-client-side-encryption] + run-fuzz-tests: - command: subprocess.exec type: test @@ -1572,6 +1583,21 @@ tasks: AUTH: "noauth" SSL: "nossl" + - name: "test-client-side-encryption" + tags: ["client-side-encryption-test"] + commands: + - func: bootstrap-mongo-orchestration + vars: + TOPOLOGY: "replica_set" + AUTH: "noauth" + SSL: "nossl" + - func: start-cse-servers + - func: run-client-side-encryption-test + vars: + TOPOLOGY: "replica_set" + AUTH: "noauth" + SSL: "nossl" + - name: "test-retry-kms-requests" tags: ["kms-test"] commands: @@ -2253,6 +2279,12 @@ buildvariants: tasks: - name: ".kms-test" + - matrix_name: "client-side-encryption-test" + matrix_spec: { version: ["latest"], os-ssl-40: ["rhel87-64"] } + display_name: "Client Side Encryption Tests ${os-ssl-40}" + tasks: + - name: ".client-side-encryption-test" + - matrix_name: "load-balancer-test" tags: ["pullrequest"] matrix_spec: { version: ["5.0", "6.0", "7.0", "8.0"], os-ssl-40: ["rhel87-64"] } diff --git a/Taskfile.yml b/Taskfile.yml index 5fd8210bda..9997318809 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -150,6 +150,9 @@ tasks: evg-test-retry-kms-requests: - go test -exec "env PKG_CONFIG_PATH=${PKG_CONFIG_PATH} LD_LIBRARY_PATH=${LD_LIBRARY_PATH}" ${BUILD_TAGS} -v -timeout {{.TEST_TIMEOUT}}s ./internal/integration -run TestClientSideEncryptionProse/kms_retry_tests >> test.suite + evg-test-client-side-encryption: + - go test -exec "env PKG_CONFIG_PATH=${PKG_CONFIG_PATH} LD_LIBRARY_PATH=${LD_LIBRARY_PATH} DYLD_LIBRARY_PATH=${MACOS_LIBRARY_PATH}" ${BUILD_TAGS} -v -timeout {{.TEST_TIMEOUT}}s ./internal/integration -run TestClientSideEncryptionProse >> test.suite + evg-test-load-balancers: # Load balancer should be tested with all unified tests as well as tests in the following # components: retryable reads, retryable writes, change streams, initial DNS seedlist discovery. diff --git a/etc/install-libmongocrypt.sh b/etc/install-libmongocrypt.sh index a94d648eae..9905cd62f1 100755 --- a/etc/install-libmongocrypt.sh +++ b/etc/install-libmongocrypt.sh @@ -3,7 +3,7 @@ # This script installs libmongocrypt into an "install" directory. set -eux -LIBMONGOCRYPT_TAG="1.12.0" +LIBMONGOCRYPT_TAG="1.15.1" # Install libmongocrypt based on OS. if [ "Windows_NT" = "${OS:-}" ]; then diff --git a/internal/integration/client_side_encryption_prose_test.go b/internal/integration/client_side_encryption_prose_test.go index d2f84a1c2b..28d3a7cbf5 100644 --- a/internal/integration/client_side_encryption_prose_test.go +++ b/internal/integration/client_side_encryption_prose_test.go @@ -3144,6 +3144,325 @@ func TestClientSideEncryptionProse(t *testing.T) { }) } }) + + mt.RunOpts("27. text Explicit Encryption", qeRunOpts.MinServerVersion("8.2"), func(mt *mtest.T) { + encryptedFields := readJSONFile(mt, "encryptedFields-prefix-suffix.json") + key1Document := readJSONFile(mt, "key1-document.json") + subtype, data := key1Document.Lookup("_id").Binary() + key1ID := bson.Binary{Subtype: subtype, Data: data} + + testSetup := func() (*mongo.Client, *mongo.ClientEncryption) { + for _, collName := range []string{"prefix-suffix", "substring"} { + mtest.DropEncryptedCollection(mt, mt.Client.Database("db").Collection(collName), encryptedFields) + cco := options.CreateCollection().SetEncryptedFields(encryptedFields) + err := mt.Client.Database("db").CreateCollection(context.Background(), collName, cco) + require.NoError(mt, err, "error on CreateCollection: %v", err) + } + err := mt.Client.Database("keyvault").Collection("datakeys").Drop(context.Background()) + require.NoError(mt, err, "error on Drop: %v", err) + opts := options.Client().ApplyURI(mtest.ClusterURI()) + integtest.AddTestServerAPIVersion(opts) + keyVaultClient, err := mongo.Connect(opts) + require.NoError(mt, err, "error on Connect: %v", err) + datakeysColl := keyVaultClient.Database("keyvault").Collection("datakeys", options.Collection().SetWriteConcern(mtest.MajorityWc)) + _, err = datakeysColl.InsertOne(context.Background(), key1Document) + require.NoError(mt, err, "error on InsertOne: %v", err) + kmsProvidersMap := map[string]map[string]any{ + "local": {"key": localMasterKey}, + } + // Create a ClientEncryption. + ceo := options.ClientEncryption(). + SetKeyVaultNamespace("keyvault.datakeys"). + SetKmsProviders(kmsProvidersMap) + clientEncryption, err := mongo.NewClientEncryption(keyVaultClient, ceo) + require.NoError(mt, err, "error on NewClientEncryption: %v", err) + + // Create a MongoClient with AutoEncryptionOpts and bypassQueryAnalysis=true. + aeo := options.AutoEncryption(). + SetKeyVaultNamespace("keyvault.datakeys"). + SetKmsProviders(kmsProvidersMap). + SetBypassQueryAnalysis(true) + co := options.Client().SetAutoEncryptionOptions(aeo).ApplyURI(mtest.ClusterURI()) + integtest.AddTestServerAPIVersion(co) + encryptedClient, err := mongo.Connect(co) + require.NoError(mt, err, "error on Connect: %v", err) + + foobarbaz := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, "foobarbaz")} + for _, c := range []struct { + collection string + textOpts *options.TextOptionsBuilder + }{ + { + collection: "prefix-suffix", + textOpts: options.Text(). + SetCaseSensitive(true). + SetDiacriticSensitive(true). + SetPrefix(options.PrefixOptions{ + StrMaxQueryLength: 10, + StrMinQueryLength: 2, + }). + SetSuffix(options.SuffixOptions{ + StrMaxQueryLength: 10, + StrMinQueryLength: 2, + }), + }, + { + collection: "substring", + textOpts: options.Text(). + SetCaseSensitive(true). + SetDiacriticSensitive(true). + SetSubstring(options.SubstringOptions{ + StrMaxLength: 10, + StrMaxQueryLength: 10, + StrMinQueryLength: 2, + }), + }, + } { + coll := encryptedClient.Database("db").Collection(c.collection, options.Collection().SetWriteConcern(mtest.MajorityWc)) + eo := options.Encrypt(). + SetKeyID(key1ID). + SetAlgorithm("TextPreview"). + SetContentionFactor(0). + SetTextOptions(c.textOpts) + insertPayload, err := clientEncryption.Encrypt(context.Background(), foobarbaz, eo) + require.NoError(mt, err, "error in Encrypt: %v", err) + _, err = coll.InsertOne(context.Background(), bson.D{{"_id", 0}, {"encryptedText", insertPayload}}) + require.NoError(mt, err, "error in InsertOne: %v", err) + } + + return encryptedClient, clientEncryption + } + + foo := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, "foo")} + _ = bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, "bar")} + baz := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, "baz")} + + mt.Run("Case 1: can find a document by prefix", func(mt *mtest.T) { + encryptedClient, clientEncryption := testSetup() + defer clientEncryption.Close(context.Background()) + defer encryptedClient.Disconnect(context.Background()) + + eo := options.Encrypt(). + SetKeyID(key1ID). + SetAlgorithm("TextPreview"). + SetQueryType("prefixPreview"). + SetContentionFactor(0). + SetTextOptions(options.Text(). + SetCaseSensitive(true). + SetDiacriticSensitive(true). + SetPrefix(options.PrefixOptions{ + StrMaxQueryLength: 10, + StrMinQueryLength: 2, + })) + payload, err := clientEncryption.Encrypt(context.Background(), foo, eo) + require.NoError(mt, err, "error in Encrypt: %v", err) + coll := encryptedClient.Database("db").Collection("prefix-suffix") + res := coll.FindOne(context.Background(), bson.D{ + {"$expr", bson.D{ + {"$encStrStartsWith", bson.D{ + {"input", "$encryptedText"}, + {"prefix", payload}, + }}, + }}, + }) + require.NoError(mt, err, "error in FindOne: %v", err) + var got struct { + Id int `bson:"_id"` + EncryptedText string `bson:"encryptedText"` + } + err = res.Decode(&got) + require.NoError(mt, err, "error decoding result: %v", err) + require.Equal(mt, 0, got.Id) + require.Equal(mt, "foobarbaz", got.EncryptedText) + }) + mt.Run("Case 2: find a document by suffix", func(mt *mtest.T) { + encryptedClient, clientEncryption := testSetup() + defer clientEncryption.Close(context.Background()) + defer encryptedClient.Disconnect(context.Background()) + + eo := options.Encrypt(). + SetKeyID(key1ID). + SetAlgorithm("TextPreview"). + SetQueryType("suffixPreview"). + SetContentionFactor(0). + SetTextOptions(options.Text(). + SetCaseSensitive(true). + SetDiacriticSensitive(true). + SetSuffix(options.SuffixOptions{ + StrMaxQueryLength: 10, + StrMinQueryLength: 2, + })) + payload, err := clientEncryption.Encrypt(context.Background(), baz, eo) + require.NoError(mt, err, "error in Encrypt: %v", err) + coll := encryptedClient.Database("db").Collection("prefix-suffix") + res := coll.FindOne(context.Background(), bson.D{ + {"$expr", bson.D{ + {"$encStrEndsWith", bson.D{ + {"input", "$encryptedText"}, + {"suffix", payload}, + }}, + }}, + }) + var got struct { + Id int `bson:"_id"` + EncryptedText string `bson:"encryptedText"` + } + err = res.Decode(&got) + require.NoError(mt, err, "error decoding result: %v", err) + require.Equal(mt, 0, got.Id) + require.Equal(mt, "foobarbaz", got.EncryptedText) + }) + mt.Run("Case 3: assert no document found by prefix", func(mt *mtest.T) { + encryptedClient, clientEncryption := testSetup() + defer clientEncryption.Close(context.Background()) + defer encryptedClient.Disconnect(context.Background()) + + eo := options.Encrypt(). + SetKeyID(key1ID). + SetAlgorithm("TextPreview"). + SetQueryType("prefixPreview"). + SetContentionFactor(0). + SetTextOptions(options.Text(). + SetCaseSensitive(true). + SetDiacriticSensitive(true). + SetPrefix(options.PrefixOptions{ + StrMaxQueryLength: 10, + StrMinQueryLength: 2, + })) + payload, err := clientEncryption.Encrypt(context.Background(), baz, eo) + require.NoError(mt, err, "error in Encrypt: %v", err) + coll := encryptedClient.Database("db").Collection("prefix-suffix") + _, err = coll.FindOne(context.Background(), bson.D{ + {"$expr", bson.D{ + {"$encStrStartsWith", bson.D{ + {"input", "$encryptedText"}, + {"prefix", payload}, + }}, + }}, + }).Raw() + require.Error(mt, err, mongo.ErrNoDocuments) + }) + mt.Run("Case 4: assert no document found by suffix", func(mt *mtest.T) { + encryptedClient, clientEncryption := testSetup() + defer clientEncryption.Close(context.Background()) + defer encryptedClient.Disconnect(context.Background()) + + eo := options.Encrypt(). + SetKeyID(key1ID). + SetAlgorithm("TextPreview"). + SetQueryType("suffixPreview"). + SetContentionFactor(0). + SetTextOptions(options.Text(). + SetCaseSensitive(true). + SetDiacriticSensitive(true). + SetSuffix(options.SuffixOptions{ + StrMaxQueryLength: 10, + StrMinQueryLength: 2, + })) + payload, err := clientEncryption.Encrypt(context.Background(), foo, eo) + require.NoError(mt, err, "error in Encrypt: %v", err) + coll := encryptedClient.Database("db").Collection("prefix-suffix") + _, err = coll.FindOne(context.Background(), bson.D{ + {"$expr", bson.D{ + {"$encStrEndsWith", bson.D{ + {"input", "$encryptedText"}, + {"suffix", payload}, + }}, + }}, + }).Raw() + require.Error(mt, err, mongo.ErrNoDocuments) + }) + mt.Run("Case 5: can find a document by substring", func(mt *mtest.T) { + encryptedClient, clientEncryption := testSetup() + defer clientEncryption.Close(context.Background()) + defer encryptedClient.Disconnect(context.Background()) + + eo := options.Encrypt(). + SetKeyID(key1ID). + SetAlgorithm("TextPreview"). + SetQueryType("substringPreview"). + SetContentionFactor(0). + SetTextOptions(options.Text(). + SetCaseSensitive(true). + SetDiacriticSensitive(true). + SetSubstring(options.SubstringOptions{ + StrMaxLength: 10, + StrMaxQueryLength: 10, + StrMinQueryLength: 2, + })) + payload, err := clientEncryption.Encrypt(context.Background(), baz, eo) + require.NoError(mt, err, "error in Encrypt: %v", err) + coll := encryptedClient.Database("db").Collection("prefix-suffix") + res := coll.FindOne(context.Background(), bson.D{ + {"$expr", bson.D{ + {"$encStrContains", bson.D{ + {"input", "$encryptedText"}, + {"substring", payload}, + }}, + }}, + }) + require.NoError(mt, err, "error in FindOne: %v", err) + var got struct { + Id int `bson:"_id"` + EncryptedText string `bson:"encryptedText"` + } + err = res.Decode(&got) + require.NoError(mt, err, "error decoding result: %v", err) + require.Equal(mt, 0, got.Id) + require.Equal(mt, "foobarbaz", got.EncryptedText) + }) + mt.Run("Case 6: assert no document found by substring", func(mt *mtest.T) { + encryptedClient, clientEncryption := testSetup() + defer clientEncryption.Close(context.Background()) + defer encryptedClient.Disconnect(context.Background()) + + qux := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, "qux")} + eo := options.Encrypt(). + SetKeyID(key1ID). + SetAlgorithm("TextPreview"). + SetQueryType("substringPreview"). + SetContentionFactor(0). + SetTextOptions(options.Text(). + SetCaseSensitive(true). + SetDiacriticSensitive(true). + SetSubstring(options.SubstringOptions{ + StrMaxLength: 10, + StrMaxQueryLength: 10, + StrMinQueryLength: 2, + })) + payload, err := clientEncryption.Encrypt(context.Background(), qux, eo) + require.NoError(mt, err, "error in Encrypt: %v", err) + coll := encryptedClient.Database("db").Collection("prefix-suffix") + _, err = coll.FindOne(context.Background(), bson.D{ + {"$expr", bson.D{ + {"$encStrContains", bson.D{ + {"input", "$encryptedText"}, + {"suffix", payload}, + }}, + }}, + }).Raw() + require.Error(mt, err, mongo.ErrNoDocuments) + }) + mt.Run("Case 7: assert contentionFactor is required", func(mt *mtest.T) { + encryptedClient, clientEncryption := testSetup() + defer clientEncryption.Close(context.Background()) + defer encryptedClient.Disconnect(context.Background()) + + eo := options.Encrypt(). + SetKeyID(key1ID). + SetAlgorithm("TextPreview"). + SetQueryType("prefixPreview"). + SetTextOptions(options.Text(). + SetCaseSensitive(true). + SetDiacriticSensitive(true). + SetPrefix(options.PrefixOptions{ + StrMaxQueryLength: 10, + StrMinQueryLength: 2, + })) + _, err := clientEncryption.Encrypt(context.Background(), baz, eo) + require.ErrorContains(mt, err, "contention factor is required for textPreview algorithm") + }) + }) } func getWatcher(mt *mtest.T, streamType mongo.StreamType, cpt *cseProseTest) watcher { diff --git a/internal/integration/mtest/mongotest.go b/internal/integration/mtest/mongotest.go index 04cfe16696..f8720bfe6a 100644 --- a/internal/integration/mtest/mongotest.go +++ b/internal/integration/mtest/mongotest.go @@ -797,12 +797,6 @@ func verifyRunOnBlockConstraint(rob RunOnBlock) error { return err } - // TODO(GODRIVER-3486): Once auto encryption is supported by the unified test - // format,this check should be removed. - if rob.CSFLEEnabled() && rob.CSFLE.Options != nil { - return fmt.Errorf("auto encryption required (GODRIVER-3486)") - } - if rob.CSFLEEnabled() && !IsCSFLEEnabled() { return fmt.Errorf("runOnBlock requires CSFLE to be enabled. Build with the cse tag to enable") } else if !rob.CSFLEEnabled() && IsCSFLEEnabled() { diff --git a/internal/integration/unified/client_entity.go b/internal/integration/unified/client_entity.go index 8f9df3d1c6..c3a5435041 100644 --- a/internal/integration/unified/client_entity.go +++ b/internal/integration/unified/client_entity.go @@ -8,6 +8,7 @@ package unified import ( "context" + "crypto/tls" "fmt" "strings" "sync" @@ -217,6 +218,13 @@ func newClientEntity(ctx context.Context, em *EntityMap, entityOptions *entityOp } else { integtest.AddTestServerAPIVersion(clientOpts) } + if entityOptions.AutoEncryptOpts != nil { + aeo, err := createAutoEncryptionOptions(entityOptions.AutoEncryptOpts) + if err != nil { + return nil, fmt.Errorf("error parsing auto encryption options: %w", err) + } + clientOpts.SetAutoEncryptionOptions(aeo) + } for _, cmd := range entityOptions.IgnoredCommands { entity.ignoredCommands[cmd] = struct{}{} } @@ -251,6 +259,82 @@ func getURIForClient(opts *entityOptions) string { } } +func createAutoEncryptionOptions(opts bson.Raw) (*options.AutoEncryptionOptions, error) { + aeo := options.AutoEncryption() + var kvnsFound bool + elems, err := opts.Elements() + if err != nil { + return nil, err + } + + for _, elem := range elems { + name := elem.Key() + opt := elem.Value() + + switch name { + case "kmsProviders": + providers := make(map[string]map[string]any) + elems, err := opt.Document().Elements() + if err != nil { + return nil, err + } + for _, elem := range elems { + key := elem.Key() + opt := elem.Value().Document() + provider, err := getKmsProvider(key, opt) + if err != nil { + return nil, err + } + if provider == nil { + continue + } + providers[key] = provider + if key == "kmip" && tlsClientCertificateKeyFile != "" && tlsCAFile != "" { + cfg, err := options.BuildTLSConfig(map[string]any{ + "tlsCertificateKeyFile": tlsClientCertificateKeyFile, + "tlsCAFile": tlsCAFile, + }) + if err != nil { + return nil, fmt.Errorf("error constructing tls config: %w", err) + } + aeo.SetTLSConfig(map[string]*tls.Config{ + "kmip": cfg, + }) + } + } + aeo.SetKmsProviders(providers) + case "schemaMap": + var schemaMap map[string]any + err := bson.Unmarshal(opt.Document(), &schemaMap) + if err != nil { + return nil, fmt.Errorf("error creating schema map: %v", err) + } + aeo.SetSchemaMap(schemaMap) + case "keyVaultNamespace": + kvnsFound = true + aeo.SetKeyVaultNamespace(opt.StringValue()) + case "bypassAutoEncryption": + aeo.SetBypassAutoEncryption(opt.Boolean()) + case "encryptedFieldsMap": + var encryptedFieldsMap map[string]any + err := bson.Unmarshal(opt.Document(), &encryptedFieldsMap) + if err != nil { + return nil, fmt.Errorf("error creating encryptedFieldsMap: %v", err) + } + aeo.SetEncryptedFieldsMap(encryptedFieldsMap) + case "bypassQueryAnalysis": + aeo.SetBypassQueryAnalysis(opt.Boolean()) + default: + return nil, fmt.Errorf("unrecognized option: %v", name) + } + } + if !kvnsFound { + aeo.SetKeyVaultNamespace("keyvault.datakeys") + } + + return aeo, nil +} + // disconnect disconnects the client associated with this entity. It is an // idempotent operation, unlike the mongo client's disconnect method. This // property will help avoid unnecessary errors when calling disconnect on a diff --git a/internal/integration/unified/collection_data.go b/internal/integration/unified/collection_data.go index d9480aa28d..63dd6ef032 100644 --- a/internal/integration/unified/collection_data.go +++ b/internal/integration/unified/collection_data.go @@ -27,8 +27,10 @@ type collectionData struct { } type createOptions struct { - Capped *bool `bson:"capped"` - SizeInBytes *int64 `bson:"size"` + Capped *bool `bson:"capped"` + SizeInBytes *int64 `bson:"size"` + EncryptedFields bson.Raw `bson:"encryptedFields"` + Validator bson.Raw `bson:"validator"` } // createCollection configures the collection represented by the receiver using the internal client. This function @@ -49,14 +51,18 @@ func (c *collectionData) createCollection(ctx context.Context) error { if c.Options.SizeInBytes != nil { createOpts = createOpts.SetSizeInBytes(*c.Options.SizeInBytes) } + if c.Options.EncryptedFields != nil { + createOpts = createOpts.SetEncryptedFields(c.Options.EncryptedFields) + } + if c.Options.Validator != nil { + createOpts = createOpts.SetValidator(c.Options.Validator) + } if err := db.CreateCollection(ctx, c.CollectionName, createOpts); err != nil { return fmt.Errorf("error creating collection: %w", err) } - } - - // If neither documents nor options are provided, still create the collection with write concern "majority". - if len(c.Documents) == 0 && c.Options == nil { + } else { + // If options are provided, still create the collection with write concern "majority". // The write concern has to be manually specified in the command document because RunCommand does not honor // the database's write concern. create := bson.D{ @@ -68,13 +74,15 @@ func (c *collectionData) createCollection(ctx context.Context) error { if err := db.RunCommand(ctx, create).Err(); err != nil { return fmt.Errorf("error creating collection: %w", err) } - return nil } - docs := bsonutil.RawToInterfaces(c.Documents...) - if _, err := coll.InsertMany(ctx, docs); err != nil { - return fmt.Errorf("error inserting data: %w", err) + if len(c.Documents) != 0 { + docs := bsonutil.RawToInterfaces(c.Documents...) + if _, err := coll.InsertMany(ctx, docs); err != nil { + return fmt.Errorf("error inserting data: %w", err) + } } + return nil } diff --git a/internal/integration/unified/database_operation_execution.go b/internal/integration/unified/database_operation_execution.go index 76b215f833..940bf37335 100644 --- a/internal/integration/unified/database_operation_execution.go +++ b/internal/integration/unified/database_operation_execution.go @@ -125,6 +125,10 @@ func executeCreateCollection(ctx context.Context, operation *operation) (*operat cco.SetTimeSeriesOptions(tso) case "clusteredIndex": cco.SetClusteredIndex(val.Document()) + case "validator": + cco.SetValidator(val.Document()) + case "encryptedFields": + cco.SetEncryptedFields(val.Document()) default: return nil, fmt.Errorf("unrecognized createCollection option %q", key) } @@ -156,6 +160,8 @@ func executeDropCollection(ctx context.Context, operation *operation) (*operatio return nil, err } + dco := options.DropCollection() + var collName string elems, _ := operation.Arguments.Elements() for _, elem := range elems { @@ -163,6 +169,8 @@ func executeDropCollection(ctx context.Context, operation *operation) (*operatio val := elem.Value() switch key { + case "encryptedFields": + dco.SetEncryptedFields(val.Document()) case "collection": collName = val.StringValue() default: @@ -173,7 +181,7 @@ func executeDropCollection(ctx context.Context, operation *operation) (*operatio return nil, newMissingArgumentError("collection") } - err = db.Collection(collName).Drop(ctx) + err = db.Collection(collName).Drop(ctx, dco) return newErrorResult(err), nil } diff --git a/internal/integration/unified/entity.go b/internal/integration/unified/entity.go index b1b827a124..96db273ba6 100644 --- a/internal/integration/unified/entity.go +++ b/internal/integration/unified/entity.go @@ -13,6 +13,7 @@ import ( "errors" "fmt" "os" + "regexp" "sync" "sync/atomic" "time" @@ -33,6 +34,8 @@ var ( tlsClientCertificateKeyFile = os.Getenv("CSFLE_TLS_CLIENT_CERT_FILE") ) +var placeholderDoc = bsoncore.NewDocumentBuilder().AppendInt32("$$placeholder", 1).Build() + type storeEventsAsEntitiesConfig struct { EventListID string `bson:"id"` Events []string `bson:"events"` @@ -52,6 +55,7 @@ type entityOptions struct { ID string `bson:"id"` // Options for client entities. + AutoEncryptOpts bson.Raw `bson:"autoEncryptOpts"` URIOptions bson.M `bson:"uriOptions"` UseMultipleMongoses *bool `bson:"useMultipleMongoses"` ObserveEvents []string `bson:"observeEvents"` @@ -503,6 +507,24 @@ func (em *EntityMap) close(ctx context.Context) []error { } } + // Clear automatically created collections used for queryable encryption + re := regexp.MustCompile("^enxcol_.*.e(sc|coc)$") + for id, db := range em.dbEntites { + colls, err := db.ListCollectionNames(ctx, bson.D{}) + if err != nil { + errs = append(errs, fmt.Errorf("error listing collections in database with ID %q: %w", id, err)) + continue + } + for _, coll := range colls { + if re.MatchString(coll) { + _, err = db.Collection(coll).DeleteMany(ctx, bson.D{}) + if err != nil { + errs = append(errs, fmt.Errorf("error clearing collection %q: %w", coll, err)) + } + } + } + } + for id, client := range em.clientEntities { if ok := em.keyVaultClientIDs[id]; ok { // Client will be closed in clientEncryption.Close() @@ -563,13 +585,13 @@ func (em *EntityMap) addDatabaseEntity(entityOptions *entityOptions) error { // getKmsCredential processes a value of an input KMS provider credential. // An empty document returns from the environment. // A string is returned as-is. -func getKmsCredential(kmsDocument bson.Raw, credentialName string, envVar string, defaultValue string) (string, error) { +func getKmsCredential(kmsDocument bson.Raw, credentialName string, envVar string, defaultValue string) (any, error) { credentialVal, err := kmsDocument.LookupErr(credentialName) if errors.Is(err, bsoncore.ErrElementNotFound) { - return "", nil + return nil, nil } if err != nil { - return "", err + return nil, err } if str, ok := credentialVal.StringValueOK(); ok { @@ -579,135 +601,135 @@ func getKmsCredential(kmsDocument bson.Raw, credentialName string, envVar string var ok bool var doc bson.Raw if doc, ok = credentialVal.DocumentOK(); !ok { - return "", fmt.Errorf("expected String or Document for %v, got: %v", credentialName, credentialVal) + return nil, fmt.Errorf("expected String or Document for %v, got: %v", credentialName, credentialVal) } - placeholderDoc := bsoncore.NewDocumentBuilder().AppendInt32("$$placeholder", 1).Build() - // Check if document is a placeholder. if !bytes.Equal(doc, placeholderDoc) { - return "", fmt.Errorf("unexpected non-empty document for %v: %v", credentialName, doc) + return nil, fmt.Errorf("unexpected non-empty document for %v: %v", credentialName, doc) } + if envVar == "" { return defaultValue, nil } - if os.Getenv(envVar) == "" { - if defaultValue != "" { - return defaultValue, nil - } - return "", fmt.Errorf("unable to get environment value for %v. Please set the CSFLE environment variable: %v", credentialName, envVar) + if value := os.Getenv(envVar); value != "" { + return value, nil } - return os.Getenv(envVar), nil - + if defaultValue != "" { + return defaultValue, nil + } + return nil, fmt.Errorf("unable to get environment value for %v. Please set the CSFLE environment variable: %v", credentialName, envVar) } -func (em *EntityMap) addClientEncryptionEntity(entityOptions *entityOptions) error { - // Construct KMS providers. - kmsProviders := make(map[string]map[string]any) - ceo := entityOptions.ClientEncryptionOpts - tlsconf := make(map[string]*tls.Config) - if aws, ok := ceo.KmsProviders["aws"]; ok { - kmsProviders["aws"] = make(map[string]any) +func getKmsProvider(key string, opt bson.Raw) (map[string]any, error) { + provider := make(map[string]any) + switch key { + case "aws": + accessKeyID := "FLE_AWS_KEY" + secretAccessKey := "FLE_AWS_SECRET" - awsSessionToken, err := getKmsCredential(aws, "sessionToken", "CSFLE_AWS_TEMP_SESSION_TOKEN", "") + // replace with temporary access, if sessionToken placeholder exists + v, err := getKmsCredential(opt, "sessionToken", "", "$$placeholder") if err != nil { - return err + return nil, err + } + if v == "$$placeholder" { + provider["sessionToken"] = os.Getenv("CSFLE_AWS_TEMP_SESSION_TOKEN") + accessKeyID = "CSFLE_AWS_TEMP_ACCESS_KEY_ID" + secretAccessKey = "CSFLE_AWS_TEMP_SECRET_ACCESS_KEY" + } else if v != nil { + provider["sessionToken"] = v } - if awsSessionToken != "" { - // Get temporary AWS credentials. - kmsProviders["aws"]["sessionToken"] = awsSessionToken - awsAccessKeyID, err := getKmsCredential(aws, "accessKeyId", "CSFLE_AWS_TEMP_ACCESS_KEY_ID", "") - if err != nil { - return err - } - if awsAccessKeyID != "" { - kmsProviders["aws"]["accessKeyId"] = awsAccessKeyID - } - awsSecretAccessKey, err := getKmsCredential(aws, "secretAccessKey", "CSFLE_AWS_TEMP_SECRET_ACCESS_KEY", "") + for _, e := range []struct { + key string + envVar string + }{ + {"accessKeyId", accessKeyID}, + {"secretAccessKey", secretAccessKey}, + } { + v, err = getKmsCredential(opt, e.key, e.envVar, "") if err != nil { - return err + return nil, err } - if awsSecretAccessKey != "" { - kmsProviders["aws"]["secretAccessKey"] = awsSecretAccessKey + if v != nil { + provider[e.key] = v } - } else { - awsAccessKeyID, err := getKmsCredential(aws, "accessKeyId", "FLE_AWS_KEY", "") + } + case "azure": + for _, e := range []struct { + key string + envVar string + }{ + {"tenantId", "FLE_AZURE_TENANTID"}, + {"clientId", "FLE_AZURE_CLIENTID"}, + {"clientSecret", "FLE_AZURE_CLIENTSECRET"}, + } { + v, err := getKmsCredential(opt, e.key, e.envVar, "") if err != nil { - return err + return nil, err } - if awsAccessKeyID != "" { - kmsProviders["aws"]["accessKeyId"] = awsAccessKeyID + if v != nil { + provider[e.key] = v } - - awsSecretAccessKey, err := getKmsCredential(aws, "secretAccessKey", "FLE_AWS_SECRET", "") + } + case "gcp": + for _, e := range []struct { + key string + envVar string + }{ + {"email", "FLE_GCP_EMAIL"}, + {"privateKey", "FLE_GCP_PRIVATEKEY"}, + } { + v, err := getKmsCredential(opt, e.key, e.envVar, "") if err != nil { - return err + return nil, err } - if awsSecretAccessKey != "" { - kmsProviders["aws"]["secretAccessKey"] = awsSecretAccessKey + if v != nil { + provider[e.key] = v } } - - } - - if azure, ok := ceo.KmsProviders["azure"]; ok { - kmsProviders["azure"] = make(map[string]any) - - azureTenantID, err := getKmsCredential(azure, "tenantId", "FLE_AZURE_TENANTID", "") - if err != nil { - return err - } - if azureTenantID != "" { - kmsProviders["azure"]["tenantId"] = azureTenantID - } - - azureClientID, err := getKmsCredential(azure, "clientId", "FLE_AZURE_CLIENTID", "") + case "kmip": + v, err := getKmsCredential(opt, "endpoint", "", "localhost:5698") if err != nil { - return err + return nil, err } - if azureClientID != "" { - kmsProviders["azure"]["clientId"] = azureClientID + if v != nil { + provider["endpoint"] = v } - - azureClientSecret, err := getKmsCredential(azure, "clientSecret", "FLE_AZURE_CLIENTSECRET", "") + case "local", "local:name2": + defaultLocalKeyBase64 := "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk" + v, err := getKmsCredential(opt, "key", "", defaultLocalKeyBase64) if err != nil { - return err + return nil, err } - if azureClientSecret != "" { - kmsProviders["azure"]["clientSecret"] = azureClientSecret + if v != nil { + provider["key"] = v } + default: + return nil, fmt.Errorf("unrecognized KMS provider: %s", key) } - - if gcp, ok := ceo.KmsProviders["gcp"]; ok { - kmsProviders["gcp"] = make(map[string]any) - - gcpEmail, err := getKmsCredential(gcp, "email", "FLE_GCP_EMAIL", "") - if err != nil { - return err - } - if gcpEmail != "" { - kmsProviders["gcp"]["email"] = gcpEmail - } - - gcpPrivateKey, err := getKmsCredential(gcp, "privateKey", "FLE_GCP_PRIVATEKEY", "") - if err != nil { - return err - } - if gcpPrivateKey != "" { - kmsProviders["gcp"]["privateKey"] = gcpPrivateKey - } + if len(provider) == 0 { + return nil, nil } + return provider, nil +} - if kmip, ok := ceo.KmsProviders["kmip"]; ok { - kmsProviders["kmip"] = make(map[string]any) - - kmipEndpoint, err := getKmsCredential(kmip, "endpoint", "", "localhost:5698") +func (em *EntityMap) addClientEncryptionEntity(entityOptions *entityOptions) error { + // Construct KMS providers. + kmsProviders := make(map[string]map[string]any) + ceo := entityOptions.ClientEncryptionOpts + tlsconf := make(map[string]*tls.Config) + for key, opt := range ceo.KmsProviders { + provider, err := getKmsProvider(key, opt) if err != nil { return err } - - if tlsClientCertificateKeyFile != "" && tlsCAFile != "" { + if provider == nil { + continue + } + kmsProviders[key] = provider + if key == "kmip" && tlsClientCertificateKeyFile != "" && tlsCAFile != "" { cfg, err := options.BuildTLSConfig(map[string]any{ "tlsCertificateKeyFile": tlsClientCertificateKeyFile, "tlsCAFile": tlsCAFile, @@ -717,23 +739,6 @@ func (em *EntityMap) addClientEncryptionEntity(entityOptions *entityOptions) err } tlsconf["kmip"] = cfg } - - if kmipEndpoint != "" { - kmsProviders["kmip"]["endpoint"] = kmipEndpoint - } - } - - if local, ok := ceo.KmsProviders["local"]; ok { - kmsProviders["local"] = make(map[string]any) - - defaultLocalKeyBase64 := "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk" - localKey, err := getKmsCredential(local, "key", "", defaultLocalKeyBase64) - if err != nil { - return err - } - if localKey != "" { - kmsProviders["local"]["key"] = localKey - } } em.keyVaultClientIDs[ceo.KeyVaultClient] = true diff --git a/internal/integration/unified/error.go b/internal/integration/unified/error.go index ca4e985433..c1f5ba04c3 100644 --- a/internal/integration/unified/error.go +++ b/internal/integration/unified/error.go @@ -42,7 +42,7 @@ type clientBulkWriteException struct { // will perform any other assertions required by the expectedError object. An error is returned if any checks fail. func verifyOperationError(ctx context.Context, expected *expectedError, result *operationResult) error { if expected == nil { - if result.Err != nil { + if result != nil && result.Err != nil { return fmt.Errorf("expected no error, but got %w", result.Err) } return nil diff --git a/internal/integration/unified/matches.go b/internal/integration/unified/matches.go index 6d34df1d95..8129f2188f 100644 --- a/internal/integration/unified/matches.go +++ b/internal/integration/unified/matches.go @@ -150,6 +150,19 @@ func verifyValuesMatchInner(ctx context.Context, expected, actual bson.RawValue) return nil } + if expected.Type == bson.TypeDecimal128 { + if actual.Type != bson.TypeDecimal128 { + return newMatchingError(keyPath, "expected value to be a decimal type but got a %s", actual.Type) + } + expectedDecimal := expected.Decimal128() + actualDecimal := actual.Decimal128() + eh, el := expectedDecimal.GetBytes() + ah, al := actualDecimal.GetBytes() + if eh != ah || el != al { + return newMatchingError(keyPath, "expected decimal value %v, got %v", expectedDecimal, actualDecimal) + } + return nil + } // Numeric values must be considered equal even if their types are different (e.g. if expected is an int32 and // actual is an int64). if expected.IsNumber() { diff --git a/internal/integration/unified/operation.go b/internal/integration/unified/operation.go index 1b591d66af..85fa98781c 100644 --- a/internal/integration/unified/operation.go +++ b/internal/integration/unified/operation.go @@ -275,8 +275,23 @@ func (op *operation) run(ctx context.Context, loopDone <-chan struct{}) (*operat case "decrypt": return executeDecrypt(ctx, op) + case "assertIndexNotExists": + db := lookupString(op.Arguments, "databaseName") + coll := lookupString(op.Arguments, "collectionName") + index := lookupString(op.Arguments, "indexName") + return newErrorResult(nil), verifyIndexExists(ctx, db, coll, index, false) + case "assertIndexExists": + db := lookupString(op.Arguments, "databaseName") + coll := lookupString(op.Arguments, "collectionName") + index := lookupString(op.Arguments, "indexName") + return newErrorResult(nil), verifyIndexExists(ctx, db, coll, index, true) + case "assertCollectionExists": + db := lookupString(op.Arguments, "databaseName") + coll := lookupString(op.Arguments, "collectionName") + return newErrorResult(nil), verifyCollectionExists(ctx, db, coll, true) + // Unsupported operations - case "count", "listIndexNames": + case "count", "listIndexNames", "mapReduce": return nil, newSkipTestError(fmt.Sprintf("the %q operation is not supported", op.Name)) default: return nil, fmt.Errorf("unrecognized entity operation %q", op.Name) diff --git a/internal/spectest/skip.go b/internal/spectest/skip.go index 25149749f4..6824681e27 100644 --- a/internal/spectest/skip.go +++ b/internal/spectest/skip.go @@ -395,19 +395,8 @@ var skipTests = map[string][]string{ "Extend Legacy Unified Spec Runner for client-side-encryption timeoutMS (GODRIVER-3521)": { "TestClientSideEncryptionSpec/timeoutMS.json/remaining_timeoutMS_applied_to_find_to_get_keyvault_data", "TestClientSideEncryptionSpec/timeoutMS.json/timeoutMS_applied_to_listCollections_to_get_collection_schema", - }, - - // TODO(GODRIVER-3486): Support auto encryption in unified tests. - "Support auto encryption in unified tests (GODRIVER-3486)": { - "TestUnifiedSpec/unified-test-format/tests/valid-pass/poc-queryable-encryption.json/insert,_replace,_and_find_with_queryable_encryption", - }, - - // TODO(DRIVERS-3106): Support auto encryption in unified tests. - "Support auto encryption in unified tests (DRIVERS-3106)": { - "TestUnifiedSpec/client-side-encryption/tests/unified/localSchema.json/A_local_schema_should_override", - "TestUnifiedSpec/client-side-encryption/tests/unified/localSchema.json/A_local_schema_with_no_encryption_is_an_error", - "TestUnifiedSpec/client-side-encryption/tests/unified/fle2v2-BypassQueryAnalysis.json/BypassQueryAnalysis_decrypts", - "TestUnifiedSpec/client-side-encryption/tests/unified/fle2v2-EncryptedFields-vs-EncryptedFieldsMap.json/encryptedFieldsMap_is_preferred_over_remote_encryptedFields", + "TestUnifiedSpec/client-side-encryption/tests/unified/timeoutMS.json/remaining_timeoutMS_applied_to_find_to_get_keyvault_data", + "TestUnifiedSpec/client-side-encryption/tests/unified/timeoutMS.json/timeoutMS_applied_to_listCollections_to_get_collection_schema", }, // TODO(GODRIVER-3076): CSFLE/QE Support for more than 1 KMS provider per @@ -833,18 +822,10 @@ var skipTests = map[string][]string{ "TestUnifiedSpec/client-side-operations-timeout/tests/tailable-awaitData.json/error_on_watch_if_maxAwaitTimeMS_is_equal_to_timeoutMS", }, - // TODO(GODRIVER-3620): Support text indexes with auto encryption. - "Support text indexes with auto encryption (GODRIVER-3620)": { - "TestUnifiedSpec/client-side-encryption/tests/unified/QE-Text-cleanupStructuredEncryptionData.json", - "TestUnifiedSpec/client-side-encryption/tests/unified/QE-Text-compactStructuredEncryptionData.json", - "TestUnifiedSpec/client-side-encryption/tests/unified/QE-Text-prefixPreview.json", - "TestUnifiedSpec/client-side-encryption/tests/unified/QE-Text-substringPreview.json", - "TestUnifiedSpec/client-side-encryption/tests/unified/QE-Text-suffixPreview.json", - }, - // TODO(GODRIVER-3403): Support queryable encryption in Client.BulkWrite. "Support queryable encryption in Client.BulkWrite (GODRIVER-3403)": { "TestUnifiedSpec/crud/tests/unified/client-bulkWrite-qe.json", + "TestUnifiedSpec/client-side-encryption/tests/unified/client-bulkWrite-qe.json", }, // Pre-4.2 SDAM tests diff --git a/mongo/client_encryption.go b/mongo/client_encryption.go index 32851ffffb..2edd2d89bc 100644 --- a/mongo/client_encryption.go +++ b/mongo/client_encryption.go @@ -243,6 +243,27 @@ func transformExplicitEncryptionOptions(opts ...options.Lister[options.EncryptOp } transformed.SetRangeOptions(transformedRange) } + if args.TextOptions != nil { + textArgs, _ := mongoutil.NewOptions[options.TextOptions](args.TextOptions) + + transformedText := mcopts.ExplicitTextOptions{ + CaseSensitive: textArgs.CaseSensitive, + DiacriticSensitive: textArgs.DiacriticSensitive, + } + if textArgs.Substring != nil { + substringOpts := mcopts.SubstringOptions(*textArgs.Substring) + transformedText.Substring = &substringOpts + } + if textArgs.Prefix != nil { + prefixOpts := mcopts.PrefixOptions(*textArgs.Prefix) + transformedText.Prefix = &prefixOpts + } + if textArgs.Suffix != nil { + suffixOpts := mcopts.SuffixOptions(*textArgs.Suffix) + transformedText.Suffix = &suffixOpts + } + transformed.SetTextOptions(transformedText) + } return transformed } diff --git a/mongo/options/encryptoptions.go b/mongo/options/encryptoptions.go index 5a45ac16ed..17c8860e8e 100644 --- a/mongo/options/encryptoptions.go +++ b/mongo/options/encryptoptions.go @@ -27,7 +27,7 @@ type RangeOptions struct { Precision *int32 } -// RangeOptionsBuilder contains options to configure Rangeopts for queryeable +// RangeOptionsBuilder contains options to configure RangeOptions for queryeable // encryption. Each option can be set through setter functions. See // documentation for each setter function for an explanation of the option. type RangeOptionsBuilder struct { @@ -99,6 +99,105 @@ func (ro *RangeOptionsBuilder) SetPrecision(precision int32) *RangeOptionsBuilde return ro } +// TextOptions specifies index options for a Queryable Encryption field supporting "test" queries. +// +// See corresponding setter methods for documentation. +type TextOptions struct { + Substring *SubstringOptions + Prefix *PrefixOptions + Suffix *SuffixOptions + CaseSensitive bool + DiacriticSensitive bool +} + +type SubstringOptions struct { + StrMaxLength int32 + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + +type PrefixOptions struct { + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + +type SuffixOptions struct { + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + +// TextOptionsBuilder contains options to configure TextOptions for queryeable +// encryption. Each option can be set through setter functions. See +// documentation for each setter function for an explanation of the option. +type TextOptionsBuilder struct { + Opts []func(*TextOptions) error +} + +// Text creates a new TextOptions instance. +func Text() *TextOptionsBuilder { + return &TextOptionsBuilder{} +} + +// List returns a list of TextOptions setter functions. +func (to *TextOptionsBuilder) List() []func(*TextOptions) error { + return to.Opts +} + +// SetSubstring sets the text index substring value. +func (to *TextOptionsBuilder) SetSubstring(substring SubstringOptions) *TextOptionsBuilder { + to.Opts = append(to.Opts, func(opts *TextOptions) error { + opts.Substring = &substring + + return nil + }) + + return to +} + +// SetPrefix sets the text index prefix value. +func (to *TextOptionsBuilder) SetPrefix(prefix PrefixOptions) *TextOptionsBuilder { + to.Opts = append(to.Opts, func(opts *TextOptions) error { + opts.Prefix = &prefix + + return nil + }) + + return to +} + +// SetSuffix sets the text index suffix value. +func (to *TextOptionsBuilder) SetSuffix(suffix SuffixOptions) *TextOptionsBuilder { + to.Opts = append(to.Opts, func(opts *TextOptions) error { + opts.Suffix = &suffix + + return nil + }) + + return to +} + +// SetCaseSensitive sets the text index caseSensitive value. +func (to *TextOptionsBuilder) SetCaseSensitive(caseSensitive bool) *TextOptionsBuilder { + to.Opts = append(to.Opts, func(opts *TextOptions) error { + opts.CaseSensitive = caseSensitive + + return nil + }) + + return to +} + +// SetDiacriticSensitive sets the text index diacriticSensitive value. +func (to *TextOptionsBuilder) SetDiacriticSensitive(diacriticSensitive bool) *TextOptionsBuilder { + to.Opts = append(to.Opts, func(opts *TextOptions) error { + opts.DiacriticSensitive = diacriticSensitive + + return nil + }) + + return to +} + // EncryptOptions represents arguments to explicitly encrypt a value. // // See corresponding setter methods for documentation. @@ -109,6 +208,7 @@ type EncryptOptions struct { QueryType string ContentionFactor *int64 RangeOptions *RangeOptionsBuilder + TextOptions *TextOptionsBuilder } // EncryptOptionsBuilder contains options to configure Encryptopts for @@ -203,3 +303,14 @@ func (e *EncryptOptionsBuilder) SetRangeOptions(ro *RangeOptionsBuilder) *Encryp return e } + +// SetTextOptions specifies the options to use for text queries. +func (e *EncryptOptionsBuilder) SetTextOptions(to *TextOptionsBuilder) *EncryptOptionsBuilder { + e.Opts = append(e.Opts, func(opts *EncryptOptions) error { + opts.TextOptions = to + + return nil + }) + + return e +} diff --git a/testdata/client-side-encryption-prose/encryptedFields-prefix-suffix.json b/testdata/client-side-encryption-prose/encryptedFields-prefix-suffix.json new file mode 100644 index 0000000000..ec4489fa09 --- /dev/null +++ b/testdata/client-side-encryption-prose/encryptedFields-prefix-suffix.json @@ -0,0 +1,38 @@ +{ + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "encryptedText", + "bsonType": "string", + "queries": [ + { + "queryType": "prefixPreview", + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "caseSensitive": true, + "diacriticSensitive": true + }, + { + "queryType": "suffixPreview", + "strMinQueryLength": { + "$numberInt": "2" + }, + "strMaxQueryLength": { + "$numberInt": "10" + }, + "caseSensitive": true, + "diacriticSensitive": true + } + ] + } + ] +} diff --git a/x/mongo/driver/mongocrypt/mongocrypt.go b/x/mongo/driver/mongocrypt/mongocrypt.go index c2c47a6334..8342057652 100644 --- a/x/mongo/driver/mongocrypt/mongocrypt.go +++ b/x/mongo/driver/mongocrypt/mongocrypt.go @@ -310,6 +310,55 @@ func (m *MongoCrypt) createExplicitEncryptionContext(opts *options.ExplicitEncry } } + if opts.TextOptions != nil { + idx, mongocryptDoc := bsoncore.AppendDocumentStart(nil) + if opts.TextOptions.Substring != nil { + substringIdx, substringDoc := bsoncore.AppendDocumentStart(nil) + substringDoc = bsoncore.AppendInt32Element(substringDoc, "strMaxLength", opts.TextOptions.Substring.StrMaxLength) + substringDoc = bsoncore.AppendInt32Element(substringDoc, "strMinQueryLength", opts.TextOptions.Substring.StrMinQueryLength) + substringDoc = bsoncore.AppendInt32Element(substringDoc, "strMaxQueryLength", opts.TextOptions.Substring.StrMaxQueryLength) + substringDoc, err := bsoncore.AppendDocumentEnd(substringDoc, substringIdx) + if err != nil { + return nil, err + } + mongocryptDoc = bsoncore.AppendDocumentElement(mongocryptDoc, "substring", substringDoc) + } + if opts.TextOptions.Prefix != nil { + prefixIdx, prefixDoc := bsoncore.AppendDocumentStart(nil) + prefixDoc = bsoncore.AppendInt32Element(prefixDoc, "strMinQueryLength", opts.TextOptions.Prefix.StrMinQueryLength) + prefixDoc = bsoncore.AppendInt32Element(prefixDoc, "strMaxQueryLength", opts.TextOptions.Prefix.StrMaxQueryLength) + prefixDoc, err := bsoncore.AppendDocumentEnd(prefixDoc, prefixIdx) + if err != nil { + return nil, err + } + mongocryptDoc = bsoncore.AppendDocumentElement(mongocryptDoc, "prefix", prefixDoc) + } + if opts.TextOptions.Suffix != nil { + suffixIdx, suffixDoc := bsoncore.AppendDocumentStart(nil) + suffixDoc = bsoncore.AppendInt32Element(suffixDoc, "strMinQueryLength", opts.TextOptions.Suffix.StrMinQueryLength) + suffixDoc = bsoncore.AppendInt32Element(suffixDoc, "strMaxQueryLength", opts.TextOptions.Suffix.StrMaxQueryLength) + suffixDoc, err := bsoncore.AppendDocumentEnd(suffixDoc, suffixIdx) + if err != nil { + return nil, err + } + mongocryptDoc = bsoncore.AppendDocumentElement(mongocryptDoc, "suffix", suffixDoc) + } + mongocryptDoc = bsoncore.AppendBooleanElement(mongocryptDoc, "caseSensitive", opts.TextOptions.CaseSensitive) + mongocryptDoc = bsoncore.AppendBooleanElement(mongocryptDoc, "diacriticSensitive", opts.TextOptions.DiacriticSensitive) + + mongocryptDoc, err := bsoncore.AppendDocumentEnd(mongocryptDoc, idx) + if err != nil { + return nil, err + } + + mongocryptBinary := newBinaryFromBytes(mongocryptDoc) + defer mongocryptBinary.close() + + if ok := C.mongocrypt_ctx_setopt_algorithm_text(ctx.wrapped, mongocryptBinary.wrapped); !ok { + return nil, ctx.createErrorFromStatus() + } + } + algoStr := C.CString(opts.Algorithm) defer C.free(unsafe.Pointer(algoStr)) diff --git a/x/mongo/driver/mongocrypt/options/mongocrypt_context_options.go b/x/mongo/driver/mongocrypt/options/mongocrypt_context_options.go index 174f3b04bf..08dba07d0d 100644 --- a/x/mongo/driver/mongocrypt/options/mongocrypt_context_options.go +++ b/x/mongo/driver/mongocrypt/options/mongocrypt_context_options.go @@ -57,6 +57,7 @@ type ExplicitEncryptionOptions struct { QueryType string ContentionFactor *int64 RangeOptions *ExplicitRangeOptions + TextOptions *ExplicitTextOptions } // ExplicitRangeOptions specifies options for the range index. @@ -68,6 +69,31 @@ type ExplicitRangeOptions struct { Precision *int32 } +// ExplicitTextOptions specifies options for the text query. +type ExplicitTextOptions struct { + Substring *SubstringOptions + Prefix *PrefixOptions + Suffix *SuffixOptions + CaseSensitive bool + DiacriticSensitive bool +} + +type SubstringOptions struct { + StrMaxLength int32 + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + +type PrefixOptions struct { + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + +type SuffixOptions struct { + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + // ExplicitEncryption creates a new ExplicitEncryptionOptions instance. func ExplicitEncryption() *ExplicitEncryptionOptions { return &ExplicitEncryptionOptions{} @@ -109,6 +135,12 @@ func (eeo *ExplicitEncryptionOptions) SetRangeOptions(ro ExplicitRangeOptions) * return eeo } +// SetTextOptions specifies the range options. +func (eeo *ExplicitEncryptionOptions) SetTextOptions(to ExplicitTextOptions) *ExplicitEncryptionOptions { + eeo.TextOptions = &to + return eeo +} + // RewrapManyDataKeyOptions represents all possible options used to decrypt and encrypt all matching data keys with a // possibly new masterKey. type RewrapManyDataKeyOptions struct {