diff --git a/data/command-monitoring/unified/redacted-commands.json b/data/command-monitoring/unified/redacted-commands.json new file mode 100644 index 0000000000..c53f018d32 --- /dev/null +++ b/data/command-monitoring/unified/redacted-commands.json @@ -0,0 +1,492 @@ +{ + "description": "redacted-commands", + "schemaVersion": "1.5", + "runOnRequirements": [ + { + "minServerVersion": "5.0", + "auth": false + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent" + ], + "observeSensitiveCommands": true + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "command-monitoring-tests" + } + } + ], + "tests": [ + { + "description": "authenticate", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "authenticate", + "command": { + "authenticate": "private" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "authenticate", + "command": { + "authenticate": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "saslStart", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "saslStart", + "command": { + "saslStart": "private" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "saslStart", + "command": { + "saslStart": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "saslContinue", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "saslContinue", + "command": { + "saslContinue": "private" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "saslContinue", + "command": { + "saslContinue": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "getnonce", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "getnonce", + "command": { + "getnonce": "private" + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "getnonce", + "command": { + "getnonce": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "createUser", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "createUser", + "command": { + "createUser": "private" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "createUser", + "command": { + "createUser": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "updateUser", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "updateUser", + "command": { + "updateUser": "private" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "updateUser", + "command": { + "updateUser": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "copydbgetnonce", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "copydbgetnonce", + "command": { + "copydbgetnonce": "private" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "copydbgetnonce", + "command": { + "copydbgetnonce": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "copydbsaslstart", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "copydbsaslstart", + "command": { + "copydbsaslstart": "private" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "copydbsaslstart", + "command": { + "copydbsaslstart": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "copydb", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "copydb", + "command": { + "copydb": "private" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "copydb", + "command": { + "copydb": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "hello with speculative authenticate", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "hello", + "command": { + "hello": "private", + "speculativeAuthenticate": "foo" + } + }, + "expectError": { + "isError": true + } + }, + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "ismaster", + "command": { + "ismaster": "private", + "speculativeAuthenticate": "foo" + } + }, + "expectError": { + "isError": true + } + }, + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "isMaster", + "command": { + "isMaster": "private", + "speculativeAuthenticate": "foo" + } + }, + "expectError": { + "isError": true + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "hello", + "command": { + "hello": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "ismaster", + "command": { + "ismaster": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "isMaster", + "command": { + "isMaster": { + "$$exists": false + } + } + } + } + ] + } + ] + }, + { + "description": "hello without speculative authenticate is not redacted", + "operations": [ + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "hello", + "command": { + "hello": "public" + } + } + }, + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "ismaster", + "command": { + "ismaster": "public" + } + } + }, + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "isMaster", + "command": { + "isMaster": "public" + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "hello", + "command": { + "hello": "public" + } + } + }, + { + "commandStartedEvent": { + "commandName": "ismaster", + "command": { + "ismaster": "public" + } + } + }, + { + "commandStartedEvent": { + "commandName": "isMaster", + "command": { + "isMaster": "public" + } + } + } + ] + } + ] + } + ] +} diff --git a/data/command-monitoring/unified/redacted-commands.yml b/data/command-monitoring/unified/redacted-commands.yml new file mode 100644 index 0000000000..1d63fd0edf --- /dev/null +++ b/data/command-monitoring/unified/redacted-commands.yml @@ -0,0 +1,252 @@ +description: "redacted-commands" + +schemaVersion: "1.5" + +runOnRequirements: + - minServerVersion: "5.0" + auth: false + +createEntities: + - client: + id: &client client + observeEvents: + - commandStartedEvent + observeSensitiveCommands: true + - database: + id: &database database + client: *client + databaseName: &databaseName command-monitoring-tests + +tests: + - description: "authenticate" + operations: + - name: runCommand + object: *database + arguments: + commandName: authenticate + command: + authenticate: "private" + # Malformed authentication commands will fail against the server, but we + # just want to assert that security-related commands are redacted + # in command monitoring. + expectError: + isError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: authenticate + # This only asserts that the command name has been redacted from the published command; + # however, it's unlikely that a driver would redact this but leave other, sensitive fields. + # We cannot simply assert that command is an empty document because it's at root-level, so + # additional fields in the actual document will be permitted + command: { authenticate: { $$exists: false } } + + - description: "saslStart" + operations: + - name: runCommand + object: *database + arguments: + commandName: saslStart + command: + saslStart: "private" + expectError: + isError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: saslStart + command: { saslStart: { $$exists: false } } + + - description: "saslContinue" + operations: + - name: runCommand + object: *database + arguments: + commandName: saslContinue + command: + saslContinue: "private" + expectError: + isError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: saslContinue + command: { saslContinue: { $$exists: false } } + + - description: "getnonce" + operations: + - name: runCommand + object: *database + arguments: + commandName: getnonce + command: + getnonce: "private" + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: getnonce + command: { getnonce: { $$exists: false } } + + - description: "createUser" + operations: + - name: runCommand + object: *database + arguments: + commandName: createUser + command: + createUser: "private" + expectError: + isError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: createUser + command: { createUser: { $$exists: false } } + + - description: "updateUser" + operations: + - name: runCommand + object: *database + arguments: + commandName: updateUser + command: + updateUser: "private" + expectError: + isError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: updateUser + command: { updateUser: { $$exists: false } } + + - description: "copydbgetnonce" + operations: + - name: runCommand + object: *database + arguments: + commandName: copydbgetnonce + command: + copydbgetnonce: "private" + expectError: + isError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: copydbgetnonce + command: { copydbgetnonce: { $$exists: false } } + + - description: "copydbsaslstart" + operations: + - name: runCommand + object: *database + arguments: + commandName: copydbsaslstart + command: + copydbsaslstart: "private" + expectError: + isError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: copydbsaslstart + command: { copydbsaslstart: { $$exists: false } } + + - description: "copydb" + operations: + - name: runCommand + object: *database + arguments: + commandName: copydb + command: + copydb: "private" + expectError: + isError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: copydb + command: { copydb: { $$exists: false } } + + - description: "hello with speculative authenticate" + operations: + - name: runCommand + object: *database + arguments: + commandName: hello + command: + hello: "private" + speculativeAuthenticate: "foo" + expectError: + isError: true + - name: runCommand + object: *database + arguments: + commandName: ismaster + command: + ismaster: "private" + speculativeAuthenticate: "foo" + expectError: + isError: true + - name: runCommand + object: *database + arguments: + commandName: isMaster + command: + isMaster: "private" + speculativeAuthenticate: "foo" + expectError: + isError: true + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: hello + command: { hello: { $$exists: false } } + - commandStartedEvent: + commandName: ismaster + command: { ismaster: { $$exists: false } } + - commandStartedEvent: + commandName: isMaster + command: { isMaster: { $$exists: false } } + + - description: "hello without speculative authenticate is not redacted" + operations: + - name: runCommand + object: *database + arguments: + commandName: hello + command: + hello: "public" + - name: runCommand + object: *database + arguments: + commandName: ismaster + command: + ismaster: "public" + - name: runCommand + object: *database + arguments: + commandName: isMaster + command: + isMaster: "public" + expectEvents: + - client: *client + events: + - commandStartedEvent: + commandName: hello + command: { hello: "public" } + - commandStartedEvent: + commandName: ismaster + command: { ismaster: "public" } + - commandStartedEvent: + commandName: isMaster + command: { isMaster: "public" } diff --git a/mongo/integration/unified/client_entity.go b/mongo/integration/unified/client_entity.go index 9ef8b0832e..1f32ebc8de 100644 --- a/mongo/integration/unified/client_entity.go +++ b/mongo/integration/unified/client_entity.go @@ -9,6 +9,7 @@ package unified import ( "context" "fmt" + "strings" "sync/atomic" "time" @@ -22,18 +23,23 @@ import ( "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" ) +// Security-sensitive commands that should be ignored in command monitoring by default. +var securitySensitiveCommands = []string{"authenticate", "saslStart", "saslContinue", "getnonce", + "createUser", "updateUser", "copydbgetnonce", "copydbsaslstart", "copydb"} + // clientEntity is a wrapper for a mongo.Client object that also holds additional information required during test // execution. type clientEntity struct { *mongo.Client - recordEvents atomic.Value - started []*event.CommandStartedEvent - succeeded []*event.CommandSucceededEvent - failed []*event.CommandFailedEvent - pooled []*event.PoolEvent - ignoredCommands map[string]struct{} - numConnsCheckedOut int32 + recordEvents atomic.Value + started []*event.CommandStartedEvent + succeeded []*event.CommandSucceededEvent + failed []*event.CommandFailedEvent + pooled []*event.PoolEvent + ignoredCommands map[string]struct{} + observeSensitiveCommands *bool + numConnsCheckedOut int32 // These should not be changed after the clientEntity is initialized observedEvents map[monitoringEventType]struct{} @@ -43,14 +49,23 @@ type clientEntity struct { } func newClientEntity(ctx context.Context, em *EntityMap, entityOptions *entityOptions) (*clientEntity, error) { + // The "configureFailPoint" command should always be ignored. + ignoredCommands := map[string]struct{}{ + "configureFailPoint": {}, + } + // If not observing sensitive commands, add security-sensitive commands + // to ignoredCommands by default. + if entityOptions.ObserveSensitiveCommands == nil || !*entityOptions.ObserveSensitiveCommands { + for _, cmd := range securitySensitiveCommands { + ignoredCommands[cmd] = struct{}{} + } + } entity := &clientEntity{ - // The "configureFailPoint" command should always be ignored. - ignoredCommands: map[string]struct{}{ - "configureFailPoint": {}, - }, - observedEvents: make(map[monitoringEventType]struct{}), - storedEvents: make(map[monitoringEventType][]string), - entityMap: em, + ignoredCommands: ignoredCommands, + observedEvents: make(map[monitoringEventType]struct{}), + storedEvents: make(map[monitoringEventType][]string), + entityMap: em, + observeSensitiveCommands: entityOptions.ObserveSensitiveCommands, } entity.setRecordEvents(true) @@ -144,10 +159,30 @@ func (c *clientEntity) stopListeningForEvents() { c.setRecordEvents(false) } +func (c *clientEntity) isIgnoredEvent(event *event.CommandStartedEvent) bool { + // Check if command is in ignoredCommands. + if _, ok := c.ignoredCommands[event.CommandName]; ok { + return true + } + + if event.CommandName == "hello" || strings.ToLower(event.CommandName) == "ismaster" { + _, err := event.Command.LookupErr("speculativeAuthenticate") + speculativeAuth := err == nil + + // If observeSensitiveCommands is false (or unset) and hello command is with + // speculative authenticate, command should be ignored. + if (c.observeSensitiveCommands == nil || !*c.observeSensitiveCommands) && + speculativeAuth { + return true + } + } + return false +} + func (c *clientEntity) startedEvents() []*event.CommandStartedEvent { var events []*event.CommandStartedEvent for _, evt := range c.started { - if _, ok := c.ignoredCommands[evt.CommandName]; !ok { + if !c.isIgnoredEvent(evt) { events = append(events, evt) } } diff --git a/mongo/integration/unified/entity.go b/mongo/integration/unified/entity.go index 7ac594c00f..45dac885e4 100644 --- a/mongo/integration/unified/entity.go +++ b/mongo/integration/unified/entity.go @@ -34,12 +34,13 @@ type entityOptions struct { ID string `bson:"id"` // Options for client entities. - URIOptions bson.M `bson:"uriOptions"` - UseMultipleMongoses *bool `bson:"useMultipleMongoses"` - ObserveEvents []string `bson:"observeEvents"` - IgnoredCommands []string `bson:"ignoreCommandMonitoringEvents"` - StoreEventsAsEntities []storeEventsAsEntitiesConfig `bson:"storeEventsAsEntities"` - ServerAPIOptions *serverAPIOptions `bson:"serverApi"` + URIOptions bson.M `bson:"uriOptions"` + UseMultipleMongoses *bool `bson:"useMultipleMongoses"` + ObserveEvents []string `bson:"observeEvents"` + IgnoredCommands []string `bson:"ignoreCommandMonitoringEvents"` + ObserveSensitiveCommands *bool `bson:"observeSensitiveCommands"` + StoreEventsAsEntities []storeEventsAsEntitiesConfig `bson:"storeEventsAsEntities"` + ServerAPIOptions *serverAPIOptions `bson:"serverApi"` // Options for database entities. DatabaseName string `bson:"databaseName"` diff --git a/mongo/integration/unified/event_verification.go b/mongo/integration/unified/event_verification.go index 8e43cf5ecc..697aad862e 100644 --- a/mongo/integration/unified/event_verification.go +++ b/mongo/integration/unified/event_verification.go @@ -167,6 +167,16 @@ func verifyCommandEvents(ctx context.Context, client *clientEntity, expectedEven if expected.Command != nil { expectedDoc := documentToRawValue(expected.Command) actualDoc := documentToRawValue(actual.Command) + + // If actual.Command is empty, as is the case with redacted commands, + // verifyValuesMatch will return an error from DocumentOK() because + // there are not enough bytes to read a document from bson.RawValue{}. + // In the case of an empty Command, hardcode an empty bson.RawValue document. + if len(actual.Command) == 0 { + emptyDoc := []byte{5, 0, 0, 0, 0} + actualDoc = bson.RawValue{Type: bsontype.EmbeddedDocument, Value: emptyDoc} + } + if err := verifyValuesMatch(ctx, expectedDoc, actualDoc, true); err != nil { return newEventVerificationError(idx, client, "error comparing command documents: %v", err) } diff --git a/mongo/integration/unified/schema_version.go b/mongo/integration/unified/schema_version.go index 5cd98088ab..f817b1f7bf 100644 --- a/mongo/integration/unified/schema_version.go +++ b/mongo/integration/unified/schema_version.go @@ -16,7 +16,8 @@ import ( var ( supportedSchemaVersions = map[int]string{ - 1: "1.3", + // We do not fully support the 1.5 schema, but we need 1.5 to test with observeSensitiveCommands. + 1: "1.5", } ) diff --git a/mongo/integration/unified/unified_spec_test.go b/mongo/integration/unified/unified_spec_test.go index 1be49e5cbb..1a85a3cab7 100644 --- a/mongo/integration/unified/unified_spec_test.go +++ b/mongo/integration/unified/unified_spec_test.go @@ -22,6 +22,7 @@ var ( "transactions/unified", "load-balancers", "collection-management", + "command-monitoring/unified", } failDirectories = []string{ "unified-test-format/valid-fail", diff --git a/x/mongo/driver/operation.go b/x/mongo/driver/operation.go index c894a05c83..0bdeb049a8 100644 --- a/x/mongo/driver/operation.go +++ b/x/mongo/driver/operation.go @@ -1414,7 +1414,7 @@ func (op *Operation) redactCommand(cmd string, doc bsoncore.Document) bool { return true } - if strings.ToLower(cmd) != "ismaster" { + if strings.ToLower(cmd) != "ismaster" && cmd != "hello" { return false }