Skip to content

Commit bb37ecd

Browse files
authored
GODRIVER-2391 add encryptedFields to CreateCollection and Drop (#913)
1 parent c779c7d commit bb37ecd

File tree

12 files changed

+3641
-18
lines changed

12 files changed

+3641
-18
lines changed

data/client-side-encryption/fle2-CreateCollection.json

Lines changed: 2059 additions & 0 deletions
Large diffs are not rendered by default.

data/client-side-encryption/fle2-CreateCollection.yml

Lines changed: 1217 additions & 0 deletions
Large diffs are not rendered by default.

mongo/client.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,13 @@ type Client struct {
6868
sessionPool *session.Pool
6969

7070
// client-side encryption fields
71-
keyVaultClientFLE *Client
72-
keyVaultCollFLE *Collection
73-
mongocryptdFLE *mcryptClient
74-
cryptFLE driver.Crypt
75-
metadataClientFLE *Client
76-
internalClientFLE *Client
71+
keyVaultClientFLE *Client
72+
keyVaultCollFLE *Collection
73+
mongocryptdFLE *mcryptClient
74+
cryptFLE driver.Crypt
75+
metadataClientFLE *Client
76+
internalClientFLE *Client
77+
encryptedFieldsMap map[string]interface{}
7778
}
7879

7980
// Connect creates a new Client and then initializes it using the Connect method. This is equivalent to calling
@@ -708,6 +709,7 @@ func (c *Client) configure(opts *options.ClientOptions) error {
708709
}
709710

710711
func (c *Client) configureAutoEncryption(clientOpts *options.ClientOptions) error {
712+
c.encryptedFieldsMap = clientOpts.AutoEncryptionOptions.EncryptedFieldsMap
711713
if err := c.configureKeyVaultClientFLE(clientOpts); err != nil {
712714
return err
713715
}

mongo/collection.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,6 +1734,68 @@ func (coll *Collection) Indexes() IndexView {
17341734
// Drop drops the collection on the server. This method ignores "namespace not found" errors so it is safe to drop
17351735
// a collection that does not exist on the server.
17361736
func (coll *Collection) Drop(ctx context.Context) error {
1737+
// Follow Client-Side Encryption specification to check for encryptedFields.
1738+
// Drop does not have an encryptedFields option. See: GODRIVER-2413.
1739+
// Check for encryptedFields from the client EncryptedFieldsMap.
1740+
// Check for encryptedFields from the server if EncryptedFieldsMap is set.
1741+
ef := coll.db.getEncryptedFieldsFromMap(coll.name)
1742+
if ef == nil && coll.db.client.encryptedFieldsMap != nil {
1743+
var err error
1744+
if ef, err = coll.db.getEncryptedFieldsFromServer(ctx, coll.name); err != nil {
1745+
return err
1746+
}
1747+
}
1748+
1749+
if ef != nil {
1750+
return coll.dropEncryptedCollection(ctx, ef)
1751+
}
1752+
1753+
return coll.drop(ctx)
1754+
}
1755+
1756+
// dropEncryptedCollection drops a collection with EncryptedFields.
1757+
func (coll *Collection) dropEncryptedCollection(ctx context.Context, ef interface{}) error {
1758+
efBSON, err := transformBsoncoreDocument(coll.registry, ef, true /* mapAllowed */, "encryptedFields")
1759+
if err != nil {
1760+
return fmt.Errorf("error transforming document: %v", err)
1761+
}
1762+
1763+
// Drop the data collection.
1764+
if err := coll.drop(ctx); err != nil {
1765+
return err
1766+
}
1767+
// Drop the three encryption-related, associated collections: `escCollection`, `eccCollection` and `ecocCollection`.
1768+
// Drop ESCCollection.
1769+
escCollection, err := getEncryptedStateCollectionName(efBSON, coll.name, "esc")
1770+
if err != nil {
1771+
return err
1772+
}
1773+
if err := coll.db.Collection(escCollection).drop(ctx); err != nil {
1774+
return err
1775+
}
1776+
1777+
// Drop ECCCollection.
1778+
eccCollection, err := getEncryptedStateCollectionName(efBSON, coll.name, "ecc")
1779+
if err != nil {
1780+
return err
1781+
}
1782+
if err := coll.db.Collection(eccCollection).drop(ctx); err != nil {
1783+
return err
1784+
}
1785+
1786+
// Drop ECOCCollection.
1787+
ecocCollection, err := getEncryptedStateCollectionName(efBSON, coll.name, "ecoc")
1788+
if err != nil {
1789+
return err
1790+
}
1791+
if err := coll.db.Collection(ecocCollection).drop(ctx); err != nil {
1792+
return err
1793+
}
1794+
return nil
1795+
}
1796+
1797+
// drop drops a collection without EncryptedFields.
1798+
func (coll *Collection) drop(ctx context.Context) error {
17371799
if ctx == nil {
17381800
ctx = context.Background()
17391801
}

mongo/database.go

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,158 @@ func (db *Database) Watch(ctx context.Context, pipeline interface{},
507507
//
508508
// For more information about the command, see https://www.mongodb.com/docs/manual/reference/command/create/.
509509
func (db *Database) CreateCollection(ctx context.Context, name string, opts ...*options.CreateCollectionOptions) error {
510+
cco := options.MergeCreateCollectionOptions(opts...)
511+
// Follow Client-Side Encryption specification to check for encryptedFields.
512+
// Check for encryptedFields from create options.
513+
ef := cco.EncryptedFields
514+
// Check for encryptedFields from the client EncryptedFieldsMap.
515+
if ef == nil {
516+
ef = db.getEncryptedFieldsFromMap(name)
517+
}
518+
if ef != nil {
519+
return db.createCollectionWithEncryptedFields(ctx, name, ef, opts...)
520+
}
521+
522+
return db.createCollection(ctx, name, opts...)
523+
}
524+
525+
// getEncryptedFieldsFromServer tries to get an "encryptedFields" document associated with collectionName by running the "listCollections" command.
526+
// Returns nil and no error if the listCollections command succeeds, but "encryptedFields" is not present.
527+
func (db *Database) getEncryptedFieldsFromServer(ctx context.Context, collectionName string) (interface{}, error) {
528+
// Check if collection has an EncryptedFields configured server-side.
529+
collSpecs, err := db.ListCollectionSpecifications(ctx, bson.D{{"name", collectionName}})
530+
if err != nil {
531+
return nil, err
532+
}
533+
if len(collSpecs) == 0 {
534+
return nil, nil
535+
}
536+
if len(collSpecs) > 1 {
537+
return nil, fmt.Errorf("expected 1 or 0 results from listCollections, got %v", len(collSpecs))
538+
}
539+
collSpec := collSpecs[0]
540+
rawValue, err := collSpec.Options.LookupErr("encryptedFields")
541+
if err == bsoncore.ErrElementNotFound {
542+
return nil, nil
543+
} else if err != nil {
544+
return nil, err
545+
}
546+
547+
encryptedFields, ok := rawValue.DocumentOK()
548+
if !ok {
549+
return nil, fmt.Errorf("expected encryptedFields of %v to be document, got %v", collectionName, rawValue.Type)
550+
}
551+
552+
return encryptedFields, nil
553+
}
554+
555+
// getEncryptedFieldsFromServer tries to get an "encryptedFields" document associated with collectionName by checking the client EncryptedFieldsMap.
556+
// Returns nil and no error if an EncryptedFieldsMap is not configured, or does not contain an entry for collectionName.
557+
func (db *Database) getEncryptedFieldsFromMap(collectionName string) interface{} {
558+
// Check the EncryptedFieldsMap
559+
efMap := db.client.encryptedFieldsMap
560+
if efMap == nil {
561+
return nil
562+
}
563+
564+
namespace := db.name + "." + collectionName
565+
566+
ef, ok := efMap[namespace]
567+
if ok {
568+
return ef
569+
}
570+
return nil
571+
}
572+
573+
// getEncryptedStateCollectionName returns the encrypted state collection name associated with dataCollectionName.
574+
func getEncryptedStateCollectionName(efBSON bsoncore.Document, dataCollectionName string, stateCollectionSuffix string) (string, error) {
575+
if stateCollectionSuffix != "esc" && stateCollectionSuffix != "ecc" && stateCollectionSuffix != "ecoc" {
576+
return "", fmt.Errorf("expected stateCollectionSuffix: esc, ecc, or ecoc. got %v", stateCollectionSuffix)
577+
}
578+
fieldName := stateCollectionSuffix + "Collection"
579+
var val bsoncore.Value
580+
var err error
581+
if val, err = efBSON.LookupErr(fieldName); err != nil {
582+
if err != bsoncore.ErrElementNotFound {
583+
return "", err
584+
}
585+
// Return default name.
586+
defaultName := "enxcol_." + dataCollectionName + "." + stateCollectionSuffix
587+
return defaultName, nil
588+
}
589+
590+
var stateCollectionName string
591+
var ok bool
592+
if stateCollectionName, ok = val.StringValueOK(); !ok {
593+
return "", fmt.Errorf("expected string for '%v', got: %v", fieldName, val.Type)
594+
}
595+
return stateCollectionName, nil
596+
}
597+
598+
// createCollectionWithEncryptedFields creates a collection with an EncryptedFields.
599+
func (db *Database) createCollectionWithEncryptedFields(ctx context.Context, name string, ef interface{}, opts ...*options.CreateCollectionOptions) error {
600+
efBSON, err := transformBsoncoreDocument(db.registry, ef, true /* mapAllowed */, "encryptedFields")
601+
if err != nil {
602+
return fmt.Errorf("error transforming document: %v", err)
603+
}
604+
605+
// Create the three encryption-related, associated collections: `escCollection`, `eccCollection` and `ecocCollection`.
606+
// Create ESCCollection.
607+
escCollection, err := getEncryptedStateCollectionName(efBSON, name, "esc")
608+
if err != nil {
609+
return err
610+
}
611+
if err := db.createCollection(ctx, escCollection); err != nil {
612+
return err
613+
}
614+
615+
// Create ECCCollection.
616+
eccCollection, err := getEncryptedStateCollectionName(efBSON, name, "ecc")
617+
if err != nil {
618+
return err
619+
}
620+
if err := db.createCollection(ctx, eccCollection); err != nil {
621+
return err
622+
}
623+
624+
// Create ECOCCollection.
625+
ecocCollection, err := getEncryptedStateCollectionName(efBSON, name, "ecoc")
626+
if err != nil {
627+
return err
628+
}
629+
if err := db.createCollection(ctx, ecocCollection); err != nil {
630+
return err
631+
}
632+
633+
// Create a data collection with the 'encryptedFields' option.
634+
op, err := db.createCollectionOperation(name, opts...)
635+
if err != nil {
636+
return err
637+
}
638+
639+
op.EncryptedFields(efBSON)
640+
if err := db.executeCreateOperation(ctx, op); err != nil {
641+
return err
642+
}
643+
644+
// Create an index on the __safeContent__ field in the collection @collectionName.
645+
if _, err := db.Collection(name).Indexes().CreateOne(ctx, IndexModel{Keys: bson.D{{"__safeContent__", 1}}}); err != nil {
646+
return fmt.Errorf("error creating safeContent index: %v", err)
647+
}
648+
649+
return nil
650+
}
651+
652+
// createCollection creates a collection without EncryptedFields.
653+
func (db *Database) createCollection(ctx context.Context, name string, opts ...*options.CreateCollectionOptions) error {
654+
op, err := db.createCollectionOperation(name, opts...)
655+
if err != nil {
656+
return err
657+
}
658+
return db.executeCreateOperation(ctx, op)
659+
}
660+
661+
func (db *Database) createCollectionOperation(name string, opts ...*options.CreateCollectionOptions) (*operation.Create, error) {
510662
cco := options.MergeCreateCollectionOptions(opts...)
511663
op := operation.NewCreate(name).ServerAPI(db.client.serverAPI)
512664

@@ -519,7 +671,7 @@ func (db *Database) CreateCollection(ctx context.Context, name string, opts ...*
519671
if cco.ChangeStreamPreAndPostImages != nil {
520672
csppi, err := transformBsoncoreDocument(db.registry, cco.ChangeStreamPreAndPostImages, true, "changeStreamPreAndPostImages")
521673
if err != nil {
522-
return err
674+
return nil, err
523675
}
524676
op.ChangeStreamPreAndPostImages(csppi)
525677
}
@@ -528,14 +680,14 @@ func (db *Database) CreateCollection(ctx context.Context, name string, opts ...*
528680
if cco.DefaultIndexOptions.StorageEngine != nil {
529681
storageEngine, err := transformBsoncoreDocument(db.registry, cco.DefaultIndexOptions.StorageEngine, true, "storageEngine")
530682
if err != nil {
531-
return err
683+
return nil, err
532684
}
533685

534686
doc = bsoncore.AppendDocumentElement(doc, "storageEngine", storageEngine)
535687
}
536688
doc, err := bsoncore.AppendDocumentEnd(doc, idx)
537689
if err != nil {
538-
return err
690+
return nil, err
539691
}
540692

541693
op.IndexOptionDefaults(doc)
@@ -549,7 +701,7 @@ func (db *Database) CreateCollection(ctx context.Context, name string, opts ...*
549701
if cco.StorageEngine != nil {
550702
storageEngine, err := transformBsoncoreDocument(db.registry, cco.StorageEngine, true, "storageEngine")
551703
if err != nil {
552-
return err
704+
return nil, err
553705
}
554706
op.StorageEngine(storageEngine)
555707
}
@@ -562,7 +714,7 @@ func (db *Database) CreateCollection(ctx context.Context, name string, opts ...*
562714
if cco.Validator != nil {
563715
validator, err := transformBsoncoreDocument(db.registry, cco.Validator, true, "validator")
564716
if err != nil {
565-
return err
717+
return nil, err
566718
}
567719
op.Validator(validator)
568720
}
@@ -582,13 +734,13 @@ func (db *Database) CreateCollection(ctx context.Context, name string, opts ...*
582734

583735
doc, err := bsoncore.AppendDocumentEnd(doc, idx)
584736
if err != nil {
585-
return err
737+
return nil, err
586738
}
587739

588740
op.TimeSeries(doc)
589741
}
590742

591-
return db.executeCreateOperation(ctx, op)
743+
return op, nil
592744
}
593745

594746
// CreateView executes a create command to explicitly create a view on the server. See

mongo/integration/client_side_encryption_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,82 @@ func TestClientSideEncryptionCustomCrypt(t *testing.T) {
381381
"expected 2 calls to BypassAutoEncryption, got %v", cc.numBypassAutoEncryptionCalls)
382382
})
383383
}
384+
385+
func TestFLE2CreateCollection(t *testing.T) {
386+
// FLE 2 is not supported on Standalone topology.
387+
mtOpts := mtest.NewOptions().
388+
MinServerVersion("6.0").
389+
Enterprise(true).
390+
CreateClient(false).
391+
Topologies(mtest.ReplicaSet,
392+
mtest.Sharded,
393+
mtest.LoadBalanced,
394+
mtest.ShardedReplicaSet)
395+
mt := mtest.New(t, mtOpts)
396+
defer mt.Close()
397+
398+
efJSON := `
399+
{
400+
"escCollection": "encryptedCollection.esc",
401+
"eccCollection": "encryptedCollection.ecc",
402+
"ecocCollection": "encryptedCollection.ecoc",
403+
"fields": [
404+
{
405+
"path": "firstName",
406+
"bsonType": "string",
407+
"keyId": {
408+
"$binary": {
409+
"subType": "04",
410+
"base64": "AAAAAAAAAAAAAAAAAAAAAA=="
411+
}
412+
}
413+
}
414+
]
415+
}
416+
`
417+
var efBSON bson.Raw
418+
err := bson.UnmarshalExtJSON([]byte(efJSON), true /* canonical */, &efBSON)
419+
assert.Nil(mt, err, "UnmarshalExtJSON error: %v", err)
420+
421+
// Test the behavior in the specification test fle2-CreateCollection.json: "CreateCollection from encryptedFields.".
422+
// The Go driver does not support encryptedFields as an option to Drop. See: GODRIVER-2413.
423+
mt.Run("CreateCollection from encryptedFields", func(mt *mtest.T) {
424+
// Drop data and state collections to clean up from a prior test run.
425+
{
426+
err := mt.DB.Collection("coll").Drop(context.Background())
427+
assert.Nil(mt, err, "error in Drop: %v", err)
428+
err = mt.DB.Collection("encryptedCollection.esc").Drop(context.Background())
429+
assert.Nil(mt, err, "error in Drop: %v", err)
430+
err = mt.DB.Collection("encryptedCollection.ecc").Drop(context.Background())
431+
assert.Nil(mt, err, "error in Drop: %v", err)
432+
err = mt.DB.Collection("encryptedCollection.ecoc").Drop(context.Background())
433+
assert.Nil(mt, err, "error in Drop: %v", err)
434+
}
435+
436+
mt.DB.CreateCollection(context.Background(), "coll", options.CreateCollection().SetEncryptedFields(efBSON))
437+
438+
// Check expected collections and index exist.
439+
{
440+
got, err := mt.DB.ListCollectionNames(context.Background(), bson.D{{"name", "coll"}})
441+
assert.Nil(mt, err, "error in ListCollectionNames")
442+
assert.Equal(mt, got, []string{"coll"}, "expected ['coll'], got: %v", got)
443+
444+
got, err = mt.DB.ListCollectionNames(context.Background(), bson.D{{"name", "encryptedCollection.esc"}})
445+
assert.Nil(mt, err, "error in ListCollectionNames")
446+
assert.Equal(mt, got, []string{"encryptedCollection.esc"}, "expected ['encryptedCollection.esc'], got: %v", got)
447+
448+
got, err = mt.DB.ListCollectionNames(context.Background(), bson.D{{"name", "encryptedCollection.ecc"}})
449+
assert.Nil(mt, err, "error in ListCollectionNames")
450+
assert.Equal(mt, got, []string{"encryptedCollection.ecc"}, "expected ['encryptedCollection.ecc'], got: %v", got)
451+
452+
got, err = mt.DB.ListCollectionNames(context.Background(), bson.D{{"name", "encryptedCollection.ecoc"}})
453+
assert.Nil(mt, err, "error in ListCollectionNames")
454+
assert.Equal(mt, got, []string{"encryptedCollection.ecoc"}, "expected ['encryptedCollection.ecoc'], got: %v", got)
455+
456+
indexSpecs, err := mt.DB.Collection("coll").Indexes().ListSpecifications(context.Background())
457+
assert.Nil(mt, err, "error in Indexes().ListSpecifications: %v", err)
458+
assert.Equal(mt, len(indexSpecs), 2, "expected two indexes on 'coll', got: %v", indexSpecs)
459+
assert.Equal(mt, indexSpecs[1].Name, "__safeContent___1", "expected second index to be '__safeContent___1', got %v", indexSpecs[1].Name)
460+
}
461+
})
462+
}

0 commit comments

Comments
 (0)