Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
492 changes: 492 additions & 0 deletions data/command-monitoring/unified/redacted-commands.json

Large diffs are not rendered by default.

252 changes: 252 additions & 0 deletions data/command-monitoring/unified/redacted-commands.yml
Original file line number Diff line number Diff line change
@@ -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" }
65 changes: 50 additions & 15 deletions mongo/integration/unified/client_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package unified
import (
"context"
"fmt"
"strings"
"sync/atomic"
"time"

Expand All @@ -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{}
Expand All @@ -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)

Expand Down Expand Up @@ -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)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using startedEvents() to assert that an expected set of started events is published during a test? If so, why are we duplicating the command redaction logic in the test runner as well as in the driver? It seems more correct to adjust the expected set of started events in the test case, not redact them in the test runner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two places in our specifications where we redact security-sensitive commands.

In the command monitoring spec, we require that drivers redact security-sensitive commands and replies at the operation level. This prevents security-sensitive information from appearing in logs or test output due to command monitoring.

In the unified test format:

ignoreCommandMonitoringEvents: Optional array of one or more strings. Command names for which the test runner MUST ignore any observed command monitoring events. The command(s) will be ignored in addition to configureFailPoint and any commands containing sensitive information (per the Command Monitoring spec) unless observeSensitiveCommands is true.

Writing tests and making sure that the expected set of events contains the correct auth-related commands can be problematic. Drivers differ on when certain auth-related commands are sent (particularly in setup), so a unified test runner that always monitors security-sensitive commands might produce different monitoring outputs depending on the driver and prevent a unified test.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks for the detailed explanation!

}
Expand Down
13 changes: 7 additions & 6 deletions mongo/integration/unified/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
10 changes: 10 additions & 0 deletions mongo/integration/unified/event_verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading