diff --git a/core/auth/plain.go b/core/auth/plain.go index 734c9fe4a4..9e8337a290 100644 --- a/core/auth/plain.go +++ b/core/auth/plain.go @@ -16,10 +16,6 @@ import ( const PLAIN = "PLAIN" func newPlainAuthenticator(cred *Cred) (Authenticator, error) { - if cred.Source != "" && cred.Source != "$external" { - return nil, newAuthError("PLAIN source must be empty or $external", nil) - } - return &PlainAuthenticator{ Username: cred.Username, Password: cred.Password, diff --git a/core/connstring/connstring.go b/core/connstring/connstring.go index 3f6ef5c304..8c436b8eb2 100644 --- a/core/connstring/connstring.go +++ b/core/connstring/connstring.go @@ -247,6 +247,16 @@ func (p *parser) parse(original string) error { } } + err = p.setDefaultAuthParams(extractedDatabase.db) + if err != nil { + return err + } + + err = p.validateAuth() + if err != nil { + return err + } + // Check for invalid write concern (i.e. w=0 and j=true) if p.WNumberSet && p.WNumber == 0 && p.JSet && p.J { return writeconcern.ErrInconsistent @@ -260,6 +270,116 @@ func (p *parser) parse(original string) error { return nil } +func (p *parser) setDefaultAuthParams(dbName string) error { + switch strings.ToLower(p.AuthMechanism) { + case "plain": + if p.AuthSource == "" { + p.AuthSource = dbName + if p.AuthSource == "" { + p.AuthSource = "$external" + } + } + case "gssapi": + if p.AuthMechanismProperties == nil { + p.AuthMechanismProperties = map[string]string{ + "SERVICE_NAME": "mongodb", + } + } else if v, ok := p.AuthMechanismProperties["SERVICE_NAME"]; !ok || v == "" { + p.AuthMechanismProperties["SERVICE_NAME"] = "mongodb" + } + fallthrough + case "mongodb-x509": + if p.AuthSource == "" { + p.AuthSource = "$external" + } else if p.AuthSource != "$external" { + return fmt.Errorf("auth source must be $external") + } + case "mongodb-cr": + fallthrough + case "scram-sha-1": + fallthrough + case "scram-sha-256": + if p.AuthSource == "" { + p.AuthSource = dbName + if p.AuthSource == "" { + p.AuthSource = "admin" + } + } + case "": + if p.AuthSource == "" { + p.AuthSource = "admin" + } + default: + return fmt.Errorf("invalid auth mechanism") + } + return nil +} + +func (p *parser) validateAuth() error { + switch strings.ToLower(p.AuthMechanism) { + case "mongodb-cr": + if p.Username == "" { + return fmt.Errorf("username required for MONGO-CR") + } + if p.Password == "" { + return fmt.Errorf("password required for MONGO-CR") + } + if p.AuthMechanismProperties != nil { + return fmt.Errorf("MONGO-CR cannot have mechanism properties") + } + case "mongodb-x509": + if p.Password != "" { + return fmt.Errorf("password cannot be specified for MONGO-X509") + } + if p.AuthMechanismProperties != nil { + return fmt.Errorf("MONGO-X509 cannot have mechanism properties") + } + case "gssapi": + if p.Username == "" { + return fmt.Errorf("username required for GSSAPI") + } + for k := range p.AuthMechanismProperties { + if k != "SERVICE_NAME" && k != "CANONICALIZE_HOST_NAME" && k != "SERVICE_REALM" { + return fmt.Errorf("invalid auth property for GSSAPI") + } + } + case "plain": + if p.Username == "" { + return fmt.Errorf("username required for PLAIN") + } + if p.Password == "" { + return fmt.Errorf("password required for PLAIN") + } + if p.AuthMechanismProperties != nil { + return fmt.Errorf("PLAIN cannot have mechanism properties") + } + case "scram-sha-1": + if p.Username == "" { + return fmt.Errorf("username required for SCRAM-SHA-1") + } + if p.Password == "" { + return fmt.Errorf("password required for SCRAM-SHA-1") + } + if p.AuthMechanismProperties != nil { + return fmt.Errorf("SCRAM-SHA-1 cannot have mechanism properties") + } + case "scram-sha-256": + if p.Username == "" { + return fmt.Errorf("username required for SCRAM-SHA-256") + } + if p.Password == "" { + return fmt.Errorf("password required for SCRAM-SHA-256") + } + if p.AuthMechanismProperties != nil { + return fmt.Errorf("SCRAM-SHA-256 cannot have mechanism properties") + } + case "": + default: + return fmt.Errorf("invalid auth mechanism") + } + return nil +} + func fetchSeedlistFromSRV(host string) ([]string, error) { var err error diff --git a/core/connstring/connstring_spec_test.go b/core/connstring/connstring_spec_test.go index 11a43dc377..9fcd713ccd 100644 --- a/core/connstring/connstring_spec_test.go +++ b/core/connstring/connstring_spec_test.go @@ -42,7 +42,10 @@ type testContainer struct { Tests []testCase } -const testsDir string = "../../data/connection-string/" +const connstringTestsDir = "../../data/connection-string/" + +// Note a test supporting the deprecated gssapiServiceName property was removed from data/auth/auth_tests.json +const authTestsDir = "../../data/auth/" func (h *host) toString() string { switch h.Type { @@ -77,8 +80,8 @@ func hostsToStrings(hosts []host) []string { return out } -func runTestsInFile(t *testing.T, filename string) { - filepath := path.Join(testsDir, filename) +func runTestsInFile(t *testing.T, dirname string, filename string) { + filepath := path.Join(dirname, filename) content, err := ioutil.ReadFile(filepath) require.NoError(t, err) @@ -106,7 +109,10 @@ func runTest(t *testing.T, filename string, test *testCase) { } require.Equal(t, test.URI, cs.Original) - require.Equal(t, hostsToStrings(test.Hosts), cs.Hosts) + + if test.Hosts != nil { + require.Equal(t, hostsToStrings(test.Hosts), cs.Hosts) + } if test.Auth != nil { require.Equal(t, test.Auth.Username, cs.Username) @@ -118,7 +124,11 @@ func runTest(t *testing.T, filename string, test *testCase) { require.Equal(t, *test.Auth.Password, cs.Password) } - require.Equal(t, test.Auth.DB, cs.Database) + if test.Auth.DB != cs.Database { + require.Equal(t, test.Auth.DB, cs.AuthSource) + } else { + require.Equal(t, test.Auth.DB, cs.Database) + } } // Check that all options are present. @@ -140,14 +150,11 @@ func runTest(t *testing.T, filename string, test *testCase) { // Test case for all connection string spec tests. func TestConnStringSpec(t *testing.T) { - entries, err := ioutil.ReadDir(testsDir) - require.NoError(t, err) - - for _, entry := range entries { - if entry.IsDir() || path.Ext(entry.Name()) != ".json" { - continue - } + for _, file := range testhelpers.FindJSONFilesInDir(t, connstringTestsDir) { + runTestsInFile(t, connstringTestsDir, file) + } - runTestsInFile(t, entry.Name()) + for _, file := range testhelpers.FindJSONFilesInDir(t, authTestsDir) { + runTestsInFile(t, authTestsDir, file) } } diff --git a/core/connstring/connstring_test.go b/core/connstring/connstring_test.go index cb3361f61d..2d9656ddf8 100644 --- a/core/connstring/connstring_test.go +++ b/core/connstring/connstring_test.go @@ -49,11 +49,11 @@ func TestAuthMechanism(t *testing.T) { }{ {s: "authMechanism=scram-sha-1", expected: "scram-sha-1"}, {s: "authMechanism=mongodb-CR", expected: "mongodb-CR"}, - {s: "authMechanism=LDAP", expected: "LDAP"}, + {s: "authMechanism=plain", expected: "plain"}, } for _, test := range tests { - s := fmt.Sprintf("mongodb://localhost/?%s", test.s) + s := fmt.Sprintf("mongodb://user:pass@localhost/?%s", test.s) t.Run(s, func(t *testing.T) { cs, err := connstring.Parse(s) if test.err { diff --git a/data/auth/auth_tests.json b/data/auth/auth_tests.json new file mode 100644 index 0000000000..e5d3469547 --- /dev/null +++ b/data/auth/auth_tests.json @@ -0,0 +1,434 @@ +{ + "tests": [ + { + "description": "should use the default source and mechanism", + "uri": "mongodb://user:password@localhost", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "admin" + }, + "options": null + }, + { + "description": "should use the database when no authSource is specified", + "uri": "mongodb://user:password@localhost/foo", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "foo" + }, + "options": null + }, + { + "description": "should use the authSource when specified", + "uri": "mongodb://user:password@localhost/foo?authSource=bar", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "bar" + }, + "options": null + }, + { + "description": "should recognise the mechanism (GSSAPI)", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user@DOMAIN.COM", + "password": null, + "db": "$external" + }, + "options": { + "authmechanism": "GSSAPI" + } + }, + { + "description": "should ignore the database (GSSAPI)", + "uri": "mongodb://user%40DOMAIN.COM@localhost/foo?authMechanism=GSSAPI", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user@DOMAIN.COM", + "password": null, + "db": "$external" + }, + "options": { + "authmechanism": "GSSAPI" + } + }, + { + "description": "should accept valid authSource (GSSAPI)", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=$external", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user@DOMAIN.COM", + "password": null, + "db": "$external" + }, + "options": { + "authmechanism": "GSSAPI" + } + }, + { + "description": "should accept generic mechanism property (GSSAPI)", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user@DOMAIN.COM", + "password": null, + "db": "$external" + }, + "options": { + "authmechanism": "GSSAPI", + "authmechanismproperties": { + "SERVICE_NAME": "other", + "CANONICALIZE_HOST_NAME": true + } + } + }, + { + "description": "should accept the password (GSSAPI)", + "uri": "mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user@DOMAIN.COM", + "password": "password", + "db": "$external" + }, + "options": { + "authmechanism": "GSSAPI" + } + }, + { + "description": "should throw an exception if authSource is invalid (GSSAPI)", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo", + "hosts": null, + "valid": false, + "warning": false, + "auth": null, + "options": null + }, + { + "description": "should throw an exception if no username (GSSAPI)", + "uri": "mongodb://localhost/?authMechanism=GSSAPI", + "hosts": null, + "valid": false, + "warning": false, + "auth": null, + "options": null + }, + { + "description": "should recognize the mechanism (MONGODB-CR)", + "uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-CR", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "admin" + }, + "options": { + "authmechanism": "MONGODB-CR" + } + }, + { + "description": "should use the database when no authSource is specified (MONGODB-CR)", + "uri": "mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "foo" + }, + "options": { + "authmechanism": "MONGODB-CR" + } + }, + { + "description": "should use the authSource when specified (MONGODB-CR)", + "uri": "mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR&authSource=bar", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "bar" + }, + "options": { + "authmechanism": "MONGODB-CR" + } + }, + { + "description": "should throw an exception if no username is supplied (MONGODB-CR)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-CR", + "hosts": null, + "valid": false, + "warning": false, + "auth": null, + "options": null + }, + { + "description": "should recognize the mechanism (MONGODB-X509)", + "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry", + "password": null, + "db": "$external" + }, + "options": { + "authmechanism": "MONGODB-X509" + } + }, + { + "description": "should ignore the database (MONGODB-X509)", + "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry", + "password": null, + "db": "$external" + }, + "options": { + "authmechanism": "MONGODB-X509" + } + }, + { + "description": "should accept valid authSource (MONGODB-X509)", + "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509&authSource=$external", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry", + "password": null, + "db": "$external" + }, + "options": { + "authmechanism": "MONGODB-X509" + } + }, + { + "description": "should recognize the mechanism with no username (MONGODB-X509)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-X509", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": null, + "password": null, + "db": "$external" + }, + "options": { + "authmechanism": "MONGODB-X509" + } + }, + { + "description": "should throw an exception if supplied a password (MONGODB-X509)", + "uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-X509", + "hosts": null, + "valid": false, + "warning": false, + "auth": null, + "options": null + }, + { + "description": "should throw an exception if authSource is invalid (MONGODB-X509)", + "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509&authSource=bar", + "hosts": null, + "valid": false, + "warning": false, + "auth": null, + "options": null + }, + { + "description": "should recognize the mechanism (PLAIN)", + "uri": "mongodb://user:password@localhost/?authMechanism=PLAIN", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "$external" + }, + "options": { + "authmechanism": "PLAIN" + } + }, + { + "description": "should use the database when no authSource is specified (PLAIN)", + "uri": "mongodb://user:password@localhost/foo?authMechanism=PLAIN", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "foo" + }, + "options": { + "authmechanism": "PLAIN" + } + }, + { + "description": "should use the authSource when specified (PLAIN)", + "uri": "mongodb://user:password@localhost/foo?authMechanism=PLAIN&authSource=bar", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "bar" + }, + "options": { + "authmechanism": "PLAIN" + } + }, + { + "description": "should throw an exception if no username (PLAIN)", + "uri": "mongodb://localhost/?authMechanism=PLAIN", + "hosts": null, + "valid": false, + "warning": false, + "auth": null, + "options": null + }, + { + "description": "should recognize the mechanism (SCRAM-SHA-1)", + "uri": "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-1", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "admin" + }, + "options": { + "authmechanism": "SCRAM-SHA-1" + } + }, + { + "description": "should use the database when no authSource is specified (SCRAM-SHA-1)", + "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "foo" + }, + "options": { + "authmechanism": "SCRAM-SHA-1" + } + }, + { + "description": "should accept valid authSource (SCRAM-SHA-1)", + "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1&authSource=bar", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "bar" + }, + "options": { + "authmechanism": "SCRAM-SHA-1" + } + }, + { + "description": "should throw an exception if no username (SCRAM-SHA-1)", + "uri": "mongodb://localhost/?authMechanism=SCRAM-SHA-1", + "hosts": null, + "valid": false, + "warning": false, + "auth": null, + "options": null + }, + { + "description": "should recognize the mechanism (SCRAM-SHA-256)", + "uri": "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-256", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "admin" + }, + "options": { + "authmechanism": "SCRAM-SHA-256" + } + }, + { + "description": "should use the database when no authSource is specified (SCRAM-SHA-256)", + "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "foo" + }, + "options": { + "authmechanism": "SCRAM-SHA-256" + } + }, + { + "description": "should accept valid authSource (SCRAM-SHA-256)", + "uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256&authSource=bar", + "hosts": null, + "valid": true, + "warning": false, + "auth": { + "username": "user", + "password": "password", + "db": "bar" + }, + "options": { + "authmechanism": "SCRAM-SHA-256" + } + }, + { + "description": "should throw an exception if no username (SCRAM-SHA-256)", + "uri": "mongodb://localhost/?authMechanism=SCRAM-SHA-256", + "hosts": null, + "valid": false, + "warning": false, + "auth": null, + "options": null + } + ] +}