From dea57469422c26ac6a43ff5c693f67588c54828a Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 12 Dec 2025 15:36:14 -0500 Subject: [PATCH 1/2] Support Prefix/Suffix/Substring Indexes. --- .../client_side_encryption_prose_test.go | 317 ++++++++++++++++++ internal/spectest/skip.go | 9 - mongo/client_encryption.go | 21 ++ mongo/options/encryptoptions.go | 116 ++++++- .../encryptedFields-prefix-suffix.json | 38 +++ x/mongo/driver/mongocrypt/mongocrypt.go | 49 +++ .../options/mongocrypt_context_options.go | 35 ++ 7 files changed, 575 insertions(+), 10 deletions(-) create mode 100644 testdata/client-side-encryption-prose/encryptedFields-prefix-suffix.json diff --git a/internal/integration/client_side_encryption_prose_test.go b/internal/integration/client_side_encryption_prose_test.go index d2f84a1c2b..faab62276b 100644 --- a/internal/integration/client_side_encryption_prose_test.go +++ b/internal/integration/client_side_encryption_prose_test.go @@ -3144,6 +3144,323 @@ 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")} + bar := 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}, + }}, + }}, + }) + 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.Equal(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.Equal(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(), bar, eo) + require.NoError(mt, err, "error in Encrypt: %v", err) + coll := encryptedClient.Database("db").Collection("substring") + res := coll.FindOne(context.Background(), bson.D{ + {"$expr", bson.D{ + {"$encStrContains", bson.D{ + {"input", "$encryptedText"}, + {"substring", 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 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("substring") + _, err = coll.FindOne(context.Background(), bson.D{ + {"$expr", bson.D{ + {"$encStrContains", bson.D{ + {"input", "$encryptedText"}, + {"substring", payload}, + }}, + }}, + }).Raw() + require.Equal(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/spectest/skip.go b/internal/spectest/skip.go index 2c90d732b1..4a563354a1 100644 --- a/internal/spectest/skip.go +++ b/internal/spectest/skip.go @@ -816,15 +816,6 @@ 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", 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..d4d02b843a 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 queryable // 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,108 @@ func (ro *RangeOptionsBuilder) SetPrecision(precision int32) *RangeOptionsBuilde return ro } +// TextOptions specifies index options for a Queryable Encryption field supporting "text" queries. +// +// See corresponding setter methods for documentation. +type TextOptions struct { + Substring *SubstringOptions + Prefix *PrefixOptions + Suffix *SuffixOptions + CaseSensitive bool + DiacriticSensitive bool +} + +// SubstringOptions specifies options to support substring queries. +type SubstringOptions struct { + StrMaxLength int32 + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + +// PrefixOptions specifies options to support prefix queries. +type PrefixOptions struct { + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + +// SuffixOptions specifies options to support suffix queries. +type SuffixOptions struct { + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + +// TextOptionsBuilder contains options to configure TextOptions for queryable +// 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 +211,7 @@ type EncryptOptions struct { QueryType string ContentionFactor *int64 RangeOptions *RangeOptionsBuilder + TextOptions *TextOptionsBuilder } // EncryptOptionsBuilder contains options to configure Encryptopts for @@ -203,3 +306,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..8655f40b12 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,34 @@ 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 +} + +// SubstringOptions specifies options to support substring queries. +type SubstringOptions struct { + StrMaxLength int32 + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + +// PrefixOptions specifies options to support prefix queries. +type PrefixOptions struct { + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + +// SuffixOptions specifies options to support suffix queries. +type SuffixOptions struct { + StrMinQueryLength int32 + StrMaxQueryLength int32 +} + // ExplicitEncryption creates a new ExplicitEncryptionOptions instance. func ExplicitEncryption() *ExplicitEncryptionOptions { return &ExplicitEncryptionOptions{} @@ -109,6 +138,12 @@ func (eeo *ExplicitEncryptionOptions) SetRangeOptions(ro ExplicitRangeOptions) * return eeo } +// SetTextOptions specifies the text 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 { From 915f59eb6068812041beff5b42238b761c55a230 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Thu, 18 Dec 2025 18:41:52 -0500 Subject: [PATCH 2/2] update tasks --- .evergreen/config.yml | 76 ++++--------------- Taskfile.yml | 10 +-- .../client_side_encryption_prose_test.go | 19 +---- 3 files changed, 22 insertions(+), 83 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 97fccf15ce..af8a519b79 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -21,10 +21,6 @@ timeout: binary: bash 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 @@ -502,21 +498,6 @@ functions: params: binary: bash args: ["${DRIVERS_TOOLS}/.evergreen/csfle/await-servers.sh"] - run-kms-tls-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" - include_expansions_in_env: [KMS_TLS_TESTCASE] - args: [*task-runner, evg-test-kms] run-kmip-tests: - command: subprocess.exec params: @@ -533,13 +514,13 @@ functions: env: KMS_MOCK_SERVERS_RUNNING: "true" args: [*task-runner, evg-test-kmip] - run-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] + include_expansions_in_env: [AUTH, SSL, MONGODB_URI, TOPOLOGY, MONGO_GO_DRIVER_COMPRESSOR, CRYPT_SHARED_LIB_PATH] args: [*task-runner, setup-test] - command: subprocess.exec type: test @@ -547,9 +528,10 @@ functions: params: binary: "bash" env: + KMS_MOCK_SERVERS_RUNNING: "true" KMS_FAILPOINT_CA_FILE: "${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem" KMS_FAILPOINT_SERVER_RUNNING: "true" - args: [*task-runner, evg-test-retry-kms-requests] + args: [*task-runner, evg-test-client-side-encryption] run-fuzz-tests: - command: subprocess.exec type: test @@ -1408,36 +1390,6 @@ tasks: TOPOLOGY: "server" AUTH: "noauth" SSL: "nossl" - - name: "test-kms-tls-invalid-cert" - tags: ["kms-test"] - commands: - - func: bootstrap-mongo-orchestration - vars: - TOPOLOGY: "server" - AUTH: "noauth" - SSL: "nossl" - - func: start-cse-servers - - func: run-kms-tls-test - vars: - KMS_TLS_TESTCASE: "INVALID_CERT" - TOPOLOGY: "server" - AUTH: "noauth" - SSL: "nossl" - - name: "test-kms-tls-invalid-hostname" - tags: ["kms-test"] - commands: - - func: bootstrap-mongo-orchestration - vars: - TOPOLOGY: "server" - AUTH: "noauth" - SSL: "nossl" - - func: start-cse-servers - - func: run-kms-tls-test - vars: - KMS_TLS_TESTCASE: "INVALID_HOSTNAME" - TOPOLOGY: "server" - AUTH: "noauth" - SSL: "nossl" - name: "test-kms-kmip" tags: ["kms-kmip"] commands: @@ -1452,16 +1404,20 @@ tasks: TOPOLOGY: "server" AUTH: "noauth" SSL: "nossl" - - name: "test-retry-kms-requests" - tags: ["kms-test"] + - name: "test-client-side-encryption" + tags: ["client-side-encryption-test"] commands: - func: bootstrap-mongo-orchestration vars: - TOPOLOGY: "server" + TOPOLOGY: "replica_set" AUTH: "noauth" SSL: "nossl" - func: start-cse-servers - - func: run-retry-kms-requests + - func: run-client-side-encryption-test + vars: + TOPOLOGY: "replica_set" + AUTH: "noauth" + SSL: "nossl" - name: "testgcpkms-task" commands: - command: subprocess.exec @@ -2088,11 +2044,11 @@ buildvariants: display_name: "API Version ${version} ${os-ssl-40}" tasks: - name: ".versioned-api" - - matrix_name: "kms-test" - matrix_spec: {version: ["7.0"], os-ssl-40: ["rhel87-64"]} - display_name: "KMS TEST ${os-ssl-40}" + - 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: ".kms-test" + - 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 73842ab9c9..19fac1dbb4 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -94,14 +94,8 @@ tasks: - go run -race ./internal/cmd/testoidcauth/main.go evg-test-kmip: - 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 TestClientSideEncryptionSpec/kmipKMS >> test.suite - - 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/data_key_and_double_encryption >> test.suite - - 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/corpus >> test.suite - - 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/custom_endpoint >> test.suite - - 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/kms_tls_options_test >> test.suite - evg-test-kms: - - 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_tls_tests >> test.suite - 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/internal/integration/client_side_encryption_prose_test.go b/internal/integration/client_side_encryption_prose_test.go index faab62276b..d8ca756b7a 100644 --- a/internal/integration/client_side_encryption_prose_test.go +++ b/internal/integration/client_side_encryption_prose_test.go @@ -1385,42 +1385,33 @@ func TestClientSideEncryptionProse(t *testing.T) { // running. See specification for port numbers and necessary arguments: // https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.md#10-kms-tls-tests mt.RunOpts("10. kms tls tests", noClientOpts, func(mt *mtest.T) { - kmsTlsTestcase := os.Getenv("KMS_TLS_TESTCASE") - if kmsTlsTestcase == "" { - mt.Skipf("Skipping test as KMS_TLS_TESTCASE is not set") + if os.Getenv("KMS_MOCK_SERVERS_RUNNING") == "" { + mt.Skipf("Skipping test as KMS_MOCK_SERVERS_RUNNING is not set") } testcases := []struct { name string port int - envValue string errMessage string }{ { "invalid certificate", 9000, - "INVALID_CERT", "expired", }, { "invalid hostname", 9001, - "INVALID_HOSTNAME", "SANs", }, } for _, tc := range testcases { mt.Run(tc.name, func(mt *mtest.T) { - // Only run test if correct KMS mock server is running. - if kmsTlsTestcase != tc.envValue { - mt.Skipf("Skipping test as KMS_TLS_TESTCASE is set to %q, expected %v", kmsTlsTestcase, tc.envValue) - } - ceo := options.ClientEncryption(). SetKmsProviders(fullKmsProvidersMap). SetKeyVaultNamespace(kvNamespace) - cpt := setup(mt, nil, nil, ceo) + cpt := setup(mt, nil, defaultKvClientOptions, ceo) defer cpt.teardown(mt) _, err := cpt.clientEnc.CreateDataKey(context.Background(), "aws", options.DataKey().SetMasterKey( @@ -1430,9 +1421,7 @@ func TestClientSideEncryptionProse(t *testing.T) { {"endpoint", fmt.Sprintf("127.0.0.1:%d", tc.port)}, }, )) - assert.NotNil(mt, err, "expected CreateDataKey error, got nil") - assert.True(mt, strings.Contains(err.Error(), tc.errMessage), - "expected CreateDataKey error to contain %v, got %v", tc.errMessage, err.Error()) + assert.ErrorContains(mt, err, tc.errMessage) }) } })