diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 6e9f13c2ae75..27aa6e960cfe 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -65,6 +65,8 @@ https://github.com/elastic/beats/compare/v5.0.0...5.0[Check the HEAD diff] *Metricbeat* +- Add username and password config options to the MongoDB module. {pull}2889[2889] + *Packetbeat* *Topbeat* diff --git a/metricbeat/docs/modules/mongodb.asciidoc b/metricbeat/docs/modules/mongodb.asciidoc index c6cca6428386..9ee1239840eb 100644 --- a/metricbeat/docs/modules/mongodb.asciidoc +++ b/metricbeat/docs/modules/mongodb.asciidoc @@ -14,7 +14,7 @@ servers. When configuring the `hosts` option, you must use MongoDB URLs of the following format: ----------------------------------- -[mongodb://][user:pass@]host[:port] +[mongodb://host[:port][?options] ----------------------------------- The URL can be as simple as: @@ -33,6 +33,21 @@ Or more complex like: hosts: ["mongodb://myuser:mypass@localhost:40001", "otherhost:40001"] ---------------------------------------------------------------------- +WARNING: In case you use username and password in the hosts array, this +information will be sent with each event as part of the `metricset.host` field. +To prevent sending username and password the config options `username` and +`password` can be used. + +[source,yaml] +---- +- module: mongodb + metricsets: ["status"] + hosts: ["localhost:27017"] + username: root + password: test +---- + + [float] === Compatibility @@ -55,9 +70,18 @@ metricbeat.modules: #period: 10s # The hosts must be passed as MongoDB URLs in the format: - # [mongodb://][user:pass@]host[:port] + # [mongodb://][user:pass@]host[:port]. + # Warning: specifying the user/password in the hosts array is possible + # but it will result in the password being present in the output documents. + # We recommend using the username and password options instead. #hosts: ["localhost:27017"] + # Username to use when connecting to MongoDB. Empty by default. + #username: user + + # Password to use when connecting to MongoDB. Empty by default. + #password: pass + ---- [float] diff --git a/metricbeat/etc/beat.full.yml b/metricbeat/etc/beat.full.yml index adb58e0ebcdc..30b923e9b9f9 100644 --- a/metricbeat/etc/beat.full.yml +++ b/metricbeat/etc/beat.full.yml @@ -81,9 +81,18 @@ metricbeat.modules: #period: 10s # The hosts must be passed as MongoDB URLs in the format: - # [mongodb://][user:pass@]host[:port] + # [mongodb://][user:pass@]host[:port]. + # Warning: specifying the user/password in the hosts array is possible + # but it will result in the password being present in the output documents. + # We recommend using the username and password options instead. #hosts: ["localhost:27017"] + # Username to use when connecting to MongoDB. Empty by default. + #username: user + + # Password to use when connecting to MongoDB. Empty by default. + #password: pass + #-------------------------------- MySQL Module ------------------------------- #- module: mysql diff --git a/metricbeat/metricbeat.full.yml b/metricbeat/metricbeat.full.yml index 08d3e95a6c41..97ae95bef2af 100644 --- a/metricbeat/metricbeat.full.yml +++ b/metricbeat/metricbeat.full.yml @@ -81,9 +81,18 @@ metricbeat.modules: #period: 10s # The hosts must be passed as MongoDB URLs in the format: - # [mongodb://][user:pass@]host[:port] + # [mongodb://][user:pass@]host[:port]. + # Warning: specifying the user/password in the hosts array is possible + # but it will result in the password being present in the output documents. + # We recommend using the username and password options instead. #hosts: ["localhost:27017"] + # Username to use when connecting to MongoDB. Empty by default. + #username: user + + # Password to use when connecting to MongoDB. Empty by default. + #password: pass + #-------------------------------- MySQL Module ------------------------------- #- module: mysql diff --git a/metricbeat/module/mongodb/_meta/config.yml b/metricbeat/module/mongodb/_meta/config.yml index 1c4bc1dbf029..ab9ecadd7c6c 100644 --- a/metricbeat/module/mongodb/_meta/config.yml +++ b/metricbeat/module/mongodb/_meta/config.yml @@ -4,6 +4,15 @@ #period: 10s # The hosts must be passed as MongoDB URLs in the format: - # [mongodb://][user:pass@]host[:port] + # [mongodb://][user:pass@]host[:port]. + # Warning: specifying the user/password in the hosts array is possible + # but it will result in the password being present in the output documents. + # We recommend using the username and password options instead. #hosts: ["localhost:27017"] + # Username to use when connecting to MongoDB. Empty by default. + #username: user + + # Password to use when connecting to MongoDB. Empty by default. + #password: pass + diff --git a/metricbeat/module/mongodb/_meta/docs.asciidoc b/metricbeat/module/mongodb/_meta/docs.asciidoc index e34006f897c2..4e14b7eba937 100644 --- a/metricbeat/module/mongodb/_meta/docs.asciidoc +++ b/metricbeat/module/mongodb/_meta/docs.asciidoc @@ -9,7 +9,7 @@ servers. When configuring the `hosts` option, you must use MongoDB URLs of the following format: ----------------------------------- -[mongodb://][user:pass@]host[:port] +[mongodb://host[:port][?options] ----------------------------------- The URL can be as simple as: @@ -28,6 +28,21 @@ Or more complex like: hosts: ["mongodb://myuser:mypass@localhost:40001", "otherhost:40001"] ---------------------------------------------------------------------- +WARNING: In case you use username and password in the hosts array, this +information will be sent with each event as part of the `metricset.host` field. +To prevent sending username and password the config options `username` and +`password` can be used. + +[source,yaml] +---- +- module: mongodb + metricsets: ["status"] + hosts: ["localhost:27017"] + username: root + password: test +---- + + [float] === Compatibility diff --git a/metricbeat/module/mongodb/parseurl.go b/metricbeat/module/mongodb/parseurl.go new file mode 100644 index 000000000000..c8873558fbb4 --- /dev/null +++ b/metricbeat/module/mongodb/parseurl.go @@ -0,0 +1,123 @@ +package mongodb + +import ( + "errors" + "fmt" + "net/url" + "strings" + + mgo "gopkg.in/mgo.v2" +) + +/* + * Functions copied from the mgo driver to help with parsing the URL. + * + * http://bazaar.launchpad.net/+branch/mgo/v2/view/head:/session.go#L382 + */ + +type urlInfo struct { + addrs []string + user string + pass string + db string + options map[string]string +} + +func parseURL(s string) (*urlInfo, error) { + if strings.HasPrefix(s, "mongodb://") { + s = s[10:] + } + info := &urlInfo{options: make(map[string]string)} + if c := strings.Index(s, "?"); c != -1 { + for _, pair := range strings.FieldsFunc(s[c+1:], isOptSep) { + l := strings.SplitN(pair, "=", 2) + if len(l) != 2 || l[0] == "" || l[1] == "" { + return nil, errors.New("connection option must be key=value: " + pair) + } + info.options[l[0]] = l[1] + } + s = s[:c] + } + if c := strings.Index(s, "@"); c != -1 { + pair := strings.SplitN(s[:c], ":", 2) + if len(pair) > 2 || pair[0] == "" { + return nil, errors.New("credentials must be provided as user:pass@host") + } + var err error + info.user, err = url.QueryUnescape(pair[0]) + if err != nil { + return nil, fmt.Errorf("cannot unescape username in URL: %q", pair[0]) + } + if len(pair) > 1 { + info.pass, err = url.QueryUnescape(pair[1]) + if err != nil { + return nil, fmt.Errorf("cannot unescape password in URL") + } + } + s = s[c+1:] + } + if c := strings.Index(s, "/"); c != -1 { + info.db = s[c+1:] + s = s[:c] + } + info.addrs = strings.Split(s, ",") + return info, nil +} + +func isOptSep(c rune) bool { + return c == ';' || c == '&' +} + +// ParseURL parses the given URL and returns a DialInfo structure ready +// to be passed to DialWithInfo +func ParseURL(host, username, pass string) (*mgo.DialInfo, error) { + uinfo, err := parseURL(host) + if err != nil { + return nil, err + } + direct := false + mechanism := "" + service := "" + source := "" + for k, v := range uinfo.options { + switch k { + case "authSource": + source = v + case "authMechanism": + mechanism = v + case "gssapiServiceName": + service = v + case "connect": + if v == "direct" { + direct = true + break + } + if v == "replicaSet" { + break + } + fallthrough + default: + return nil, errors.New("unsupported connection URL option: " + k + "=" + v) + } + } + + info := &mgo.DialInfo{ + Addrs: uinfo.addrs, + Direct: direct, + Database: uinfo.db, + Username: uinfo.user, + Password: uinfo.pass, + Mechanism: mechanism, + Service: service, + Source: source, + } + + if len(username) > 0 { + info.Username = username + } + if len(pass) > 0 { + info.Password = pass + } + + return info, nil +} diff --git a/metricbeat/module/mongodb/parseurl_test.go b/metricbeat/module/mongodb/parseurl_test.go new file mode 100644 index 000000000000..85105f8f6702 --- /dev/null +++ b/metricbeat/module/mongodb/parseurl_test.go @@ -0,0 +1,78 @@ +package mongodb + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseURL(t *testing.T) { + tests := []struct { + Name string + URL string + Username string + Password string + ExpectedAddr string + ExpectedUsername string + ExpectedPassword string + }{ + { + Name: "basic test", + URL: "localhost:40001", + Username: "user", + Password: "secret", + + ExpectedAddr: "localhost:40001", + ExpectedUsername: "user", + ExpectedPassword: "secret", + }, + { + Name: "with schema", + URL: "mongodb://localhost:40001", + Username: "user", + Password: "secret", + + ExpectedAddr: "localhost:40001", + ExpectedUsername: "user", + ExpectedPassword: "secret", + }, + { + Name: "user password in url", + URL: "mongodb://user:secret@localhost:40001", + Username: "", + Password: "", + + ExpectedAddr: "localhost:40001", + ExpectedUsername: "user", + ExpectedPassword: "secret", + }, + { + Name: "user password overwride", + URL: "mongodb://user:secret@localhost:40001", + Username: "anotheruser", + Password: "anotherpass", + + ExpectedAddr: "localhost:40001", + ExpectedUsername: "anotheruser", + ExpectedPassword: "anotherpass", + }, + { + Name: "with options", + URL: "mongodb://localhost:40001?connect=direct&authSource=me", + Username: "anotheruser", + Password: "anotherpass", + + ExpectedAddr: "localhost:40001", + ExpectedUsername: "anotheruser", + ExpectedPassword: "anotherpass", + }, + } + + for _, test := range tests { + info, err := ParseURL(test.URL, test.Username, test.Password) + assert.NoError(t, err, test.Name) + assert.Equal(t, info.Addrs[0], test.ExpectedAddr, test.Name) + assert.Equal(t, info.Username, test.ExpectedUsername, test.Name) + assert.Equal(t, info.Password, test.ExpectedPassword, test.Name) + } +} diff --git a/metricbeat/module/mongodb/status/status.go b/metricbeat/module/mongodb/status/status.go index d5d6df4eed91..6be2f84dc859 100644 --- a/metricbeat/module/mongodb/status/status.go +++ b/metricbeat/module/mongodb/status/status.go @@ -3,6 +3,7 @@ package status import ( "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/metricbeat/mb" + "github.com/elastic/beats/metricbeat/module/mongodb" "github.com/pkg/errors" "gopkg.in/mgo.v2" @@ -11,7 +12,6 @@ import ( /* TODOs: - * add support for username/password * add metricset for "locks" data * add a metricset for "metrics" data */ @@ -24,26 +24,36 @@ func init() { type MetricSet struct { mb.BaseMetricSet + dialInfo *mgo.DialInfo } func New(base mb.BaseMetricSet) (mb.MetricSet, error) { config := struct { - Hosts []string `config:"hosts" validate:"nonzero,required"` + Hosts []string `config:"hosts" validate:"nonzero,required"` + Username string `config:"username"` + Password string `config:"username"` }{} if err := base.Module().UnpackConfig(&config); err != nil { return nil, err } + info, err := mongodb.ParseURL(base.Host(), config.Username, config.Password) + if err != nil { + return nil, err + } + info.Timeout = base.Module().Config().Timeout + return &MetricSet{ BaseMetricSet: base, + dialInfo: info, }, nil } func (m *MetricSet) Fetch() (common.MapStr, error) { - session, err := mgo.DialWithTimeout(m.Host(), m.Module().Config().Timeout) + session, err := mgo.DialWithInfo(m.dialInfo) if err != nil { return nil, err }