diff --git a/enginetest/enginetests.go b/enginetest/enginetests.go index 81a617ecdc..1857872043 100644 --- a/enginetest/enginetests.go +++ b/enginetest/enginetests.go @@ -16,6 +16,7 @@ package enginetest import ( "context" + "crypto/tls" dsql "database/sql" "fmt" "io" @@ -2010,11 +2011,19 @@ func TestUserAuthentication(t *testing.T, h Harness) { User: "root", Address: "localhost", }) + + tlsCert, err := tls.LoadX509KeyPair("./testdata/selfsigned_cert.pem", "./testdata/selfsigned_key.pem") + require.NoError(t, err) + tlsConfig := tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + } + serverConfig := server.Config{ - Protocol: "tcp", - Address: fmt.Sprintf("localhost:%d", port), - MaxConnections: 1000, - AllowClearTextWithoutTLS: true, + Protocol: "tcp", + Address: fmt.Sprintf("localhost:%d", port), + MaxConnections: 1000, + TLSConfig: &tlsConfig, + RequireSecureTransport: true, } e := mustNewEngine(t, clientHarness) @@ -2055,24 +2064,37 @@ func TestUserAuthentication(t *testing.T, h Harness) { }() for _, assertion := range script.Assertions { - conn, err := dbr.Open("mysql", fmt.Sprintf("%s:%s@tcp(localhost:%d)/?allowCleartextPasswords=true", - assertion.Username, assertion.Password, port), nil) - require.NoError(t, err) - r, err := conn.Query(assertion.Query) - if assertion.ExpectedErr || len(assertion.ExpectedErrStr) > 0 || assertion.ExpectedErrKind != nil { - if !assert.Error(t, err) { - require.NoError(t, r.Close()) - } else if len(assertion.ExpectedErrStr) > 0 { - assert.Equal(t, assertion.ExpectedErrStr, err.Error()) - } else if assertion.ExpectedErrKind != nil { - assert.True(t, assertion.ExpectedErrKind.Is(err)) - } - } else { - if assert.NoError(t, err) { - require.NoError(t, r.Close()) + t.Run(assertion.Query, func(t *testing.T) { + conn, err := dbr.Open("mysql", fmt.Sprintf("%s:%s@tcp(localhost:%d)/?allowCleartextPasswords=true&tls=skip-verify", + assertion.Username, assertion.Password, port), nil) + require.NoError(t, err) + r, err := conn.Query(assertion.Query) + if assertion.ExpectedErr || len(assertion.ExpectedErrStr) > 0 || assertion.ExpectedErrKind != nil { + if !assert.Error(t, err) { + require.NoError(t, r.Close()) + } else if len(assertion.ExpectedErrStr) > 0 { + assert.Equal(t, assertion.ExpectedErrStr, err.Error()) + } else if assertion.ExpectedErrKind != nil { + assert.True(t, assertion.ExpectedErrKind.Is(err)) + } + } else { + if assert.NoError(t, err) { + require.NoError(t, r.Close()) + } + if assertion.ExpectedAuthPlugin != "" { + // NOTE: This query works as long as there is only one account configured for the current user + r, err := conn.Query("SELECT plugin FROM mysql.user WHERE user=SUBSTRING_INDEX(USER(),'@',1);") + require.NoError(t, err) + require.True(t, r.Next()) + var authPlugin string + err = r.Scan(&authPlugin) + require.False(t, r.Next()) + require.NoError(t, err) + require.Equal(t, assertion.ExpectedAuthPlugin, authPlugin) + } } - } - require.NoError(t, conn.Close()) + require.NoError(t, conn.Close()) + }) } }) } diff --git a/enginetest/queries/priv_auth_queries.go b/enginetest/queries/priv_auth_queries.go index f5d6ffc626..bc3c44fb45 100644 --- a/enginetest/queries/priv_auth_queries.go +++ b/enginetest/queries/priv_auth_queries.go @@ -95,12 +95,13 @@ type ServerAuthenticationTest struct { // ServerAuthenticationTestAssertion is within a ServerAuthenticationTest to assert functionality. type ServerAuthenticationTestAssertion struct { - Username string - Password string - Query string - ExpectedErr bool - ExpectedErrKind *errors.Kind - ExpectedErrStr string + Username string + Password string + Query string + ExpectedErr bool + ExpectedErrKind *errors.Kind + ExpectedErrStr string + ExpectedAuthPlugin string } // UserPrivTests test the user and privilege systems. These tests always have the root account available, and the root @@ -740,7 +741,37 @@ var UserPrivTests = []UserPrivilegeTest{ }, }, }, - + { + Name: "Migrate a user from mysql_native_password to caching_sha2_password", + SetUpScript: []string{ + "CREATE USER testuser1@`127.0.0.1` identified with mysql_native_password by 'pass1';", + }, + Assertions: []UserPrivilegeTestAssertion{ + { + Query: "select user, host, plugin, authentication_string from mysql.user where user='testuser1';", + Expected: []sql.Row{{"testuser1", "127.0.0.1", "mysql_native_password", "*22A99BA288DB55E8E230679259740873101CD636"}}, + }, + { + Query: "ALTER USER testuser1@`127.0.0.1` IDENTIFIED WITH caching_sha2_password BY 'pass1';", + Expected: []sql.Row{{types.NewOkResult(0)}}, + }, + { + // caching_sha2_password auth uses a random salt to create the authentication + // string. Since it's not a consistent value during each test run, we just sanity + // check the first bytes of metadata (digest type, iterations) in the auth string. + Query: "select user, host, plugin, authentication_string like '$A$005$%' from mysql.user where user='testuser1';", + Expected: []sql.Row{{"testuser1", "127.0.0.1", "caching_sha2_password", true}}, + }, + { + Query: "ALTER USER testuser1@`127.0.0.1` IDENTIFIED WITH caching_sha2_password;", + Expected: []sql.Row{{types.NewOkResult(0)}}, + }, + { + Query: "select user, host, plugin, authentication_string from mysql.user where user='testuser1';", + Expected: []sql.Row{{"testuser1", "127.0.0.1", "caching_sha2_password", ""}}, + }, + }, + }, { Name: "Dynamic privilege support", SetUpScript: []string{ @@ -2604,10 +2635,10 @@ var ServerAuthTests = []ServerAuthenticationTest{ }, Assertions: []ServerAuthenticationTestAssertion{ { - Username: "rand_user", - Password: "rand_pass", - Query: "SELECT * FROM mysql.user;", - ExpectedErr: false, + Username: "rand_user", + Password: "rand_pass", + Query: "SELECT * FROM mysql.user;", + ExpectedAuthPlugin: "mysql_native_password", }, { Username: "rand_user", @@ -2630,17 +2661,17 @@ var ServerAuthTests = []ServerAuthenticationTest{ }, }, { - Name: "Create User with plugin specification", + Name: "Create User explicitly with mysql_native_password plugin", SetUpScript: []string{ "CREATE USER ranuse@localhost IDENTIFIED WITH mysql_native_password BY 'ranpas';", "GRANT ALL ON *.* TO ranuse@localhost WITH GRANT OPTION;", }, Assertions: []ServerAuthenticationTestAssertion{ { - Username: "ranuse", - Password: "ranpas", - Query: "SELECT * FROM mysql.user;", - ExpectedErr: false, + Username: "ranuse", + Password: "ranpas", + Query: "SELECT * FROM mysql.user;", + ExpectedAuthPlugin: "mysql_native_password", }, { Username: "ranuse", @@ -2656,6 +2687,103 @@ var ServerAuthTests = []ServerAuthenticationTest{ }, }, }, + { + Name: "Create User explicitly with caching_sha2_password plugin", + SetUpScript: []string{ + // testuser1 is created with a password + "CREATE USER testuser1@localhost IDENTIFIED WITH caching_sha2_password BY 'mypassword3';", + "GRANT ALL ON *.* TO testuser1@localhost WITH GRANT OPTION;", + // testuser2 is created without a password + "CREATE USER testuser2@localhost IDENTIFIED WITH caching_sha2_password;", + "GRANT ALL ON *.* TO testuser2@localhost WITH GRANT OPTION;", + }, + Assertions: []ServerAuthenticationTestAssertion{ + { + Username: "testuser1", + Password: "mypassword3", + Query: "SELECT * FROM mysql.user;", + ExpectedAuthPlugin: "caching_sha2_password", + }, + { + Username: "testuser1", + Password: "what", + Query: "SELECT * FROM mysql.user;", + ExpectedErr: true, + ExpectedErrStr: "Error 1045 (28000): Access denied for user 'testuser1'", + }, + { + Username: "testuser1", + Password: "", + Query: "SELECT * FROM mysql.user;", + ExpectedErr: true, + ExpectedErrStr: "Error 1045 (28000): Access denied for user 'testuser1'", + }, + { + Username: "testuser2", + Password: "wrong", + Query: "SELECT * FROM mysql.user;", + ExpectedErr: true, + ExpectedErrStr: "Error 1045 (28000): Access denied for user 'testuser2'", + }, + { + Username: "testuser2", + Password: "", + Query: "SELECT * FROM mysql.user;", + ExpectedErr: false, + ExpectedAuthPlugin: "caching_sha2_password", + }, + }, + }, + { + Name: "Migrate user from mysql_native_password to caching_sha2_password", + SetUpScript: []string{ + // testuser1 is created with a password + "CREATE USER testuser1@localhost IDENTIFIED WITH mysql_native_password BY 'mypassword3';", + "GRANT ALL ON *.* TO testuser1@localhost WITH GRANT OPTION;", + }, + Assertions: []ServerAuthenticationTestAssertion{ + { + Username: "testuser1", + Password: "mypassword3", + Query: "SELECT * FROM mysql.user;", + ExpectedAuthPlugin: "mysql_native_password", + }, + { + Username: "root", + Query: "ALTER USER testuser1@localhost IDENTIFIED WITH caching_sha2_password BY 'pass1';", + }, + { + Username: "testuser1", + Password: "pass1", + Query: "SELECT * FROM mysql.user;", + ExpectedAuthPlugin: "caching_sha2_password", + }, + { + Username: "testuser1", + Password: "wrong", + Query: "SELECT * FROM mysql.user;", + ExpectedErr: true, + ExpectedErrStr: "Error 1045 (28000): Access denied for user 'testuser1'", + }, + { + Username: "root", + Query: "ALTER USER testuser1@localhost IDENTIFIED WITH caching_sha2_password;", + }, + { + Username: "testuser1", + Password: "", + Query: "SELECT * FROM mysql.user;", + ExpectedAuthPlugin: "caching_sha2_password", + }, + { + Username: "testuser1", + Password: "wrong", + Query: "SELECT * FROM mysql.user;", + ExpectedErr: true, + ExpectedErrStr: "Error 1045 (28000): Access denied for user 'testuser1'", + }, + }, + }, { Name: "Create User with jwt plugin specification", SetUpScript: []string{ @@ -2668,22 +2796,25 @@ var ServerAuthTests = []ServerAuthenticationTest{ }, Assertions: []ServerAuthenticationTestAssertion{ { - Username: "test-user", - Password: "what", - Query: "SELECT * FROM mysql.user;", - ExpectedErr: true, + Username: "test-user", + Password: "what", + Query: "SELECT * FROM mysql.user;", + ExpectedErr: true, + ExpectedErrStr: "Error 1045 (28000): Access denied for user 'test-user'", }, { - Username: "test-user", - Password: "", - Query: "SELECT * FROM mysql.user;", - ExpectedErr: true, + Username: "test-user", + Password: "", + Query: "SELECT * FROM mysql.user;", + ExpectedErr: true, + ExpectedErrStr: "Error 1045 (28000): Access denied for user 'test-user'", }, { - Username: "test-user", - Password: "right-password", - Query: "SELECT * FROM mysql.user;", - ExpectedErr: false, + + Username: "test-user", + Password: "right-password", + Query: "SELECT * FROM mysql.user;", + ExpectedAuthPlugin: "authentication_dolt_jwt", }, }, }, diff --git a/enginetest/testdata/selfsigned_cert.pem b/enginetest/testdata/selfsigned_cert.pem new file mode 100644 index 0000000000..7438a95980 --- /dev/null +++ b/enginetest/testdata/selfsigned_cert.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIErDCCApQCCQCP9IKGyBYVUDANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQKDA1E +b2x0SHViLCBJbmMuMB4XDTIyMDcyMTE5NTUyN1oXDTI2MDcyMDE5NTUyN1owGDEW +MBQGA1UECgwNRG9sdEh1YiwgSW5jLjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMRNMquL4n2bAMQmoedUFcT/zI42ThW5o9qxh9U8MhsDqKBet7JL7ruh +3FRj/yaU+3ax2lGgapcTnSJHWHPUX6MzUpqkwrLzKxqMrKQYnU2F96n3GOflllsB +8ISKBy0TIipLWKupndc893qk+j6HUQ9OrtFHwroZ6/Tg32h4LcJvhkwILb8EHZpI +fSSzi637msnXWqln1WYmX+v4ARUVVSwl5kkPGZ4PxMDQRX3ioqy2d27GbHYSo01K +tSScQvnFJeddu09l7hNTSFixRz+XU+F+jYna8xUtqHMawEpfGw8Gsu8sMV9PUfUx +6WrMb1tcyWEDvHwH6RUlhNa085eCRFznHLDZNxrgBwrslII9d9xW/JaRq43d4M0q +e7F+BazB3rImMo2QHFvtvq55N7mvp+LprMjoEneBboiz1I6M0rq7Sn58FxazgQvz +mCZmNYIrznx+S1mlJjxtC7xgDPZn5Z/68TsmT26j5UNLou7i/yb7XFdMVY6kAnoZ +BAf4+rAUEDV85OLz0kWZSJj+DdlPOl/gIhTgrgJev2Cc7THUTnuk+nGETG49DITS +ySWYLsMYFHweSTm3rG6sMyN7mdbRrUSM1bqbARm46mt89+0HGsFX1QLe3v4yda67 +ic93J5mkfNPZ96K9Hth3SBbZt43DvB56ZqsFQlzBZeQ3hkk7ooulAgMBAAEwDQYJ +KoZIhvcNAQELBQADggIBAJgopHEYmETWhH06T72EpgLr3xqckCP9QZ6/UBN8eDt0 +rRqMCfM5H33qpe2wojjKwFDkR8XpwF/80VflfFBt9hc1c1fuKmyQSES9gUw10uSL +Z54MCPOa+c8+hslkmJR8Na0QyWN5unnozVHf4XIChsgL1/FblXcOgQLPZygzMNM2 +IjdT9XpEHiZTDZDp1NmM7rkRkcpgF2J8G2dcRjo/OGpnhH7wHgxm7hS6yWLW8xPP +8M2/8LPYA7H6HMGYeyrYuDPeVzfaHrECTft+4cjHLu/jYnVLukMMSuI9v5FjtxtX +PYtxnLv8hnbParjSnzK8cOlGdfJDRPMUw9/tvZ4bTeyTtJQYgl/jtdaU4mbdWlge +XMzkZGH3kKpsV2rPZXKJuqRq3vzfr51cQhEcnbJ5H8BsDUQADS3ind37guqoKhuZ +6vFUBTKLeYK5VZ4J18ztXhEynAf7kdROKP6XbE53qtH8qQujmOMWliSFQFdidYsj +eItzGQ4M/ZqI84UnPRL3WPdfPkWqa0x1k6PHFRcFJPp8nhl3O5V4ZdyVC3pKhzUJ +Y8sMit5RH1K8ZTYUVtEKwMX9wRMEkbfE4u/P+yItKw7QgYRdKerlDfCGZP8JY9+k +wqYmF56EVGQFaJdJ1ublVEHQkAVHOBowzccwWOV/OPi1sL+cf4RxAaY7gJpuNEIk +-----END CERTIFICATE----- diff --git a/enginetest/testdata/selfsigned_key.pem b/enginetest/testdata/selfsigned_key.pem new file mode 100644 index 0000000000..31fc334acd --- /dev/null +++ b/enginetest/testdata/selfsigned_key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 ++MjyTS/rmN7YbJRkCrgg/4kGlldmYg== +-----END PRIVATE KEY----- diff --git a/go.mod b/go.mod index f3259eb5b4..352237f6e8 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/dolthub/go-icu-regex v0.0.0-20240916130659-0118adc6b662 github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81 - github.com/dolthub/vitess v0.0.0-20241209181212-588631aba4be + github.com/dolthub/vitess v0.0.0-20241210212237-371986101b56 github.com/go-kit/kit v0.10.0 github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d github.com/gocraft/dbr/v2 v2.7.2 diff --git a/go.sum b/go.sum index 9af3cc0d4c..a3f7c3a8db 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/dolthub/vitess v0.0.0-20241206185536-814752c66695 h1:jqTM+/S/LKYdsdre github.com/dolthub/vitess v0.0.0-20241206185536-814752c66695/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= github.com/dolthub/vitess v0.0.0-20241209181212-588631aba4be h1:YF+vUXEAMqai036iZPJS2JRKd3pXcCQ1TYcLWBR4boo= github.com/dolthub/vitess v0.0.0-20241209181212-588631aba4be/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= +github.com/dolthub/vitess v0.0.0-20241210212237-371986101b56 h1:Uya+vBAJnul10I0xUsJigBQvvptXpLo1oSTYviLpf5Q= +github.com/dolthub/vitess v0.0.0-20241210212237-371986101b56/go.mod h1:1gQZs/byeHLMSul3Lvl3MzioMtOW1je79QYGyi2fd70= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= diff --git a/sql/mysql_db/auth.go b/sql/mysql_db/auth.go index f9c261bb6c..11bd6abdef 100644 --- a/sql/mysql_db/auth.go +++ b/sql/mysql_db/auth.go @@ -54,7 +54,11 @@ func newAuthServer(db *MySQLDb) *authServer { &nativePasswordHashStorage{db: db}, newUserValidator(db, mysql.MysqlNativePassword)) - // TODO: Add CachingSha2Password AuthMethod + // caching_sha2_password auth support + cachingSha2PasswordAuthMethod := mysql.NewSha2CachingAuthMethod( + &noopCachingStorage{db: db}, + &sha2PlainTextStorage{db: db}, + newUserValidator(db, mysql.CachingSha2Password)) // The extended auth method allows for integrators to register their own PlaintextAuthPlugin implementations, // and uses the MySQL clear auth method to send the auth information from the client to the server. @@ -63,7 +67,11 @@ func newAuthServer(db *MySQLDb) *authServer { &extendedAuthUserValidator{db: db}) return &authServer{ - authMethods: []mysql.AuthMethod{nativePasswordAuthMethod, extendedAuthMethod}, + authMethods: []mysql.AuthMethod{ + nativePasswordAuthMethod, + cachingSha2PasswordAuthMethod, + extendedAuthMethod, + }, } } @@ -77,6 +85,135 @@ func (db *authServer) DefaultAuthMethodDescription() mysql.AuthMethodDescription return DefaultAuthMethod } +// noopCachingStorage is a simple implementation of mysql.CachingStorage that doesn't actually +// use a cache yet. Eventually this will be replaced with a real authentication info cache, but +// for the initial implementation there is no caching supported. This means that repeated +// authentication for the same user will take longer to complete, since the full auth process +// needs to be performed each time. Implementing auth caching will optimize repeated logins for users. +type noopCachingStorage struct { + db *MySQLDb +} + +var _ mysql.CachingStorage = (*noopCachingStorage)(nil) + +// UserEntryWithCacheHash implements the mysql.CachingStorage interface. This method gets called by +// Vitess' cachingSha2Password auth method implementation during the initial/fast auth portion of +// caching_sha2_password authentication. The client sends a scrambled password over the wire and +// this method looks for a matching cached scrambled password for the specified user. If a matching +// scrambled password is found, then the server responds back to the client with an auth success +// message. If no match is found, the server responds with a more auth data needed message and the +// client then ensures a TLS connection is present and sends the password in plaintext for the +// server to validate using the more secure, but slower, full auth process. See the implementation +// in sha2PlainTextStorage for more details. +// +// This implementation also handles authentication when a client doesn't send an auth response and +// the associated user account does not have a password set. +func (n noopCachingStorage) UserEntryWithCacheHash(_ []*x509.Certificate, _ []byte, user string, authResponse []byte, remoteAddr net.Addr) (mysql.Getter, mysql.CacheState, error) { + db := n.db + + // If there is no mysql database of user info, then don't approve or reject, since we can't look at + // user data to validate; just ask for more data + if !db.Enabled() { + return nil, mysql.AuthNeedMoreData, nil + } + + // If we have a user database and the client didn't send an auth response, then check to see if this user + // account has no password configured. If there is no password, then we can go ahead and accept the request + // without having to do the full auth process. + emptyClientAuthResponse := len(authResponse) == 0 || (len(authResponse) == 1 && authResponse[0] == 0) + if emptyClientAuthResponse { + host, err := extractHostAddress(remoteAddr) + if err != nil { + return nil, mysql.AuthRejected, err + } + + rd := db.Reader() + defer rd.Close() + + userEntry := db.GetUser(rd, user, host, false) + if userEntry == nil || userEntry.Locked { + return nil, mysql.AuthRejected, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user) + } + + if userEntry.AuthString == "" { + return sql.MysqlConnectionUser{User: user, Host: host}, mysql.AuthAccepted, nil + } else { + return nil, mysql.AuthRejected, nil + } + } + + // TODO: Add auth caching as an optimization. The full auth process is fairly expensive since + // it involves an additional network roundtrip and many rounds of hashing. Instead, we + // can store a less secure (hashed only twice) version of the password in memory once a + // user has successfully logged in once with the full auth process, and then compare the + // password with that hash on future logins. + // + // The only trick to this optimization is that we must ensure a user who logs and gets + // their password cached, cannot change their password and then still login with the old + // password. This means invalidating the cache entry when a user changes their password. + + return nil, mysql.AuthNeedMoreData, nil +} + +// sha2PlainTextStorage implements the mysql.PlainTextStorage interface for the caching_sha2_password +// authentication protocol. This type is responsible for looking up a user entry in the mysql database +// (|db|) and hashing a plaintext password to compare against the user entry's authentication string +// to determine if a user login is valid. +type sha2PlainTextStorage struct { + db *MySQLDb +} + +var _ mysql.PlainTextStorage = (*sha2PlainTextStorage)(nil) + +// UserEntryWithPassword implements the mysql.PlainTextStorage interface. +// The auth framework in Vitess also passes in user certificates, but we don't support that feature yet. +func (s sha2PlainTextStorage) UserEntryWithPassword(_ []*x509.Certificate, user string, password string, remoteAddr net.Addr) (mysql.Getter, error) { + db := s.db + + host, err := extractHostAddress(remoteAddr) + if err != nil { + return nil, err + } + + if !db.Enabled() { + return sql.MysqlConnectionUser{User: user, Host: host}, nil + } + + rd := db.Reader() + defer rd.Close() + + userEntry := db.GetUser(rd, user, host, false) + if userEntry == nil || userEntry.Locked { + return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user) + } + + if len(userEntry.AuthString) > 0 { + digestType, iterations, salt, _, err := mysql.DeserializeCachingSha2PasswordAuthString([]byte(userEntry.AuthString)) + if err != nil { + return nil, err + } + if digestType != "SHA256" { + return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, + "Access denied for user '%v': unsupported digest type: %s", user, digestType) + } + + authString, err := mysql.SerializeCachingSha2PasswordAuthString(password, salt, iterations) + if err != nil { + return nil, err + } + + if userEntry.AuthString != string(authString) { + return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user) + } + } else if len(password) > 0 { + // password is nil or empty, therefore no password is set + // a password was given and the account has no password set, therefore access is denied + return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user) + } + + return sql.MysqlConnectionUser{User: userEntry.User, Host: userEntry.Host}, nil +} + // extendedAuthPlainTextStorage implements the mysql.PlainTextStorage interface and plugs into // the MySQL clear password auth method in order to allow extension auth mechanisms to be used. // Integrators can register their own PlaintextAuthPlugin through the MySQLDb::SetPlugins method,