Skip to content

Commit 097dd7e

Browse files
committed
Updated GMS AuthServer implementation for refactored changes in Vitess auth framework
1 parent 0aac419 commit 097dd7e

File tree

2 files changed

+311
-166
lines changed

2 files changed

+311
-166
lines changed

sql/mysql_db/auth.go

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
// Copyright 2024 Dolthub, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package mysql_db
16+
17+
import (
18+
"bytes"
19+
"crypto/sha1"
20+
"crypto/x509"
21+
"encoding/hex"
22+
"net"
23+
24+
"github.com/dolthub/vitess/go/mysql"
25+
"github.com/sirupsen/logrus"
26+
27+
"github.com/dolthub/go-mysql-server/sql"
28+
)
29+
30+
// authServer implements the mysql.AuthServer interface. It exposes configured AuthMethod implementations
31+
// that the auth framework in Vitess uses to negotiate authentication with a client. By default, authServer
32+
// configures support for the mysql_native_password auth plugin, as well as an extensible auth method, built
33+
// on the mysql_clear_password plugin, that integrators can use to provide extended authentication options,
34+
// through the use of registering PlaintextAuthPlugins with MySQLDb.
35+
type authServer struct {
36+
authMethods []mysql.AuthMethod
37+
}
38+
39+
var _ mysql.AuthServer = (*authServer)(nil)
40+
41+
// newAuthServer creates a new instance of an authServer, configured with auth method implementations supporting
42+
// mysql_native_password support, as well as an extensible auth method, built on the mysql_clear_password auth
43+
// method, that allows integrators to extend authentication to allow additional schemes.
44+
func newAuthServer(db *MySQLDb) *authServer {
45+
// The native password auth method allows auth over the mysql_native_password protocol
46+
nativePasswordAuthMethod := mysql.NewMysqlNativeAuthMethod(
47+
&nativePasswordHashStorage{db: db},
48+
&nativePasswordUserValidator{db: db})
49+
50+
// TODO: Add CachingSha2Password AuthMethod
51+
52+
// The extended auth method allows for integrators to register their own PlaintextAuthPlugin implementations,
53+
// and uses the MySQL clear auth method to send the auth information from the client to the server.
54+
extendedAuthMethod := mysql.NewMysqlClearAuthMethod(
55+
&extendedAuthPlainTextStorage{db: db},
56+
&extendedAuthUserValidator{db: db})
57+
58+
return &authServer{
59+
authMethods: []mysql.AuthMethod{nativePasswordAuthMethod, extendedAuthMethod},
60+
}
61+
}
62+
63+
// AuthMethods implements the mysql.AuthServer interface.
64+
func (as *authServer) AuthMethods() []mysql.AuthMethod {
65+
return as.authMethods
66+
}
67+
68+
// DefaultAuthMethodDescription implements the mysql.AuthServer interface.
69+
func (db *authServer) DefaultAuthMethodDescription() mysql.AuthMethodDescription {
70+
return mysql.MysqlNativePassword
71+
}
72+
73+
// extendedAuthPlainTextStorage implements the mysql.PlainTextStorage interface and plugs into
74+
// the MySQL clear password auth method in order to allow extension auth mechanisms to be used.
75+
// Integrators can register their own PlaintextAuthPlugin through the MySQLDb::SetPlugins method,
76+
// then if a user account's plugin is set to the registerd plugin, this PlainTextStorage, the
77+
// registered PlaintextAuthPlugin will be used to authenticate the user. This class serves as
78+
// a bridge between the MySQL clear password auth method implementation in Vitess, the user
79+
// account data stored in the MySQLDb, and custom PlaintextAuthPlugin implementations.
80+
type extendedAuthPlainTextStorage struct {
81+
db *MySQLDb
82+
}
83+
84+
var _ mysql.PlainTextStorage = (*extendedAuthPlainTextStorage)(nil)
85+
86+
// UserEntryWithPassword implements the mysql.PlainTextStorage interface. This method is called by the
87+
// MySQL clear password auth method to authenticate a user with a custom PlaintextAuthPlugin that was
88+
// previously registered with the MySQLDb instance.
89+
func (f extendedAuthPlainTextStorage) UserEntryWithPassword(userCerts []*x509.Certificate, user string, password string, remoteAddr net.Addr) (mysql.Getter, error) {
90+
db := f.db
91+
92+
host, err := extractHostAddress(remoteAddr)
93+
if err != nil {
94+
return nil, err
95+
}
96+
97+
connUser := sql.MysqlConnectionUser{User: user, Host: host}
98+
if !db.Enabled() {
99+
return connUser, nil
100+
}
101+
102+
rd := db.Reader()
103+
defer rd.Close()
104+
105+
userEntry := db.GetUser(rd, user, host, false)
106+
if userEntry == nil {
107+
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError,
108+
"Access denied for user '%v': no known user", user)
109+
}
110+
111+
authPluginName := userEntry.Plugin
112+
if authPluginName == "" {
113+
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError,
114+
"Access denied for user '%v': no auth plugin specified", user)
115+
}
116+
117+
authplugin, ok := db.plugins[authPluginName]
118+
if !ok {
119+
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError,
120+
"Access denied for user '%v'; auth plugin %s not registered with server", user, authPluginName)
121+
}
122+
123+
authed, err := authplugin.Authenticate(db, user, userEntry, password)
124+
if err != nil {
125+
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError,
126+
"Access denied for user '%v': %v", user, err)
127+
}
128+
if !authed {
129+
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError,
130+
"Access denied for user '%v'", user)
131+
}
132+
return connUser, nil
133+
}
134+
135+
// extendedAuthUserValidator implements the mysql.UserValidator interface and plugs into the MySQL clear password
136+
// auth method.
137+
type extendedAuthUserValidator struct {
138+
db *MySQLDb
139+
}
140+
141+
var _ mysql.UserValidator = (*extendedAuthUserValidator)(nil)
142+
143+
// HandleUser implements the mysql.UserValidator interface.
144+
func (uv extendedAuthUserValidator) HandleUser(user string, remoteAddr net.Addr) bool {
145+
// If the mysql database is not enabled, then we don't have user information, so
146+
// go ahead and return true without trying to look up the user in the db.
147+
if !uv.db.Enabled() {
148+
return true
149+
}
150+
151+
host, err := extractHostAddress(remoteAddr)
152+
if err != nil {
153+
logrus.Warnf("error extracting host address: %v", err)
154+
return false
155+
}
156+
157+
db := uv.db
158+
rd := db.Reader()
159+
defer rd.Close()
160+
161+
if !db.Enabled() {
162+
return true
163+
}
164+
userEntry := db.GetUser(rd, user, host, false)
165+
166+
for pluginName, _ := range db.plugins {
167+
if userEntry.Plugin == pluginName {
168+
return true
169+
}
170+
}
171+
172+
return false
173+
}
174+
175+
// nativePasswordHashStorage implements the mysql.HashStorage interface and plugs into the mysql_native_password
176+
// auth protocol. It is responsible for looking up a user in the MySQL database and validating a password hash
177+
// against the user's stored password hash.
178+
type nativePasswordHashStorage struct {
179+
db *MySQLDb
180+
}
181+
182+
var _ mysql.HashStorage = (*nativePasswordHashStorage)(nil)
183+
184+
// UserEntryWithHash implements the mysql.HashStorage interface. This implementation is called by the MySQL
185+
// native password auth method to validate a password hash with the user's stored password hash.
186+
func (nphs *nativePasswordHashStorage) UserEntryWithHash(_ []*x509.Certificate, salt []byte, user string, authResponse []byte, remoteAddr net.Addr) (mysql.Getter, error) {
187+
db := nphs.db
188+
189+
host, err := extractHostAddress(remoteAddr)
190+
if err != nil {
191+
return nil, err
192+
}
193+
194+
rd := db.Reader()
195+
defer rd.Close()
196+
197+
if !db.Enabled() {
198+
return sql.MysqlConnectionUser{User: user, Host: host}, nil
199+
}
200+
201+
userEntry := db.GetUser(rd, user, host, false)
202+
if userEntry == nil || userEntry.Locked {
203+
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
204+
}
205+
if len(userEntry.Password) > 0 {
206+
if !validateMysqlNativePassword(authResponse, salt, userEntry.Password) {
207+
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
208+
}
209+
} else if len(authResponse) > 0 {
210+
// password is nil or empty, therefore no password is set
211+
// a password was given and the account has no password set, therefore access is denied
212+
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
213+
}
214+
215+
return sql.MysqlConnectionUser{User: userEntry.User, Host: userEntry.Host}, nil
216+
}
217+
218+
// nativePasswordUserValidator implements the mysql.UserValidator interface and plugs into the mysql_native_password
219+
// auth method in Vitess. This implementation is called by the native password auth method to determine if a specific
220+
// user and remote address can connect to this server via the mysql_native_password auth protocol.
221+
type nativePasswordUserValidator struct {
222+
db *MySQLDb
223+
}
224+
225+
var _ mysql.UserValidator = (*nativePasswordUserValidator)(nil)
226+
227+
// HandleUser implements the mysql.UserValidator interface and verifies if the mysql_native_password auth method
228+
// can be used for the specified |user| at the specified |remoteAddr|.
229+
func (uv *nativePasswordUserValidator) HandleUser(user string, remoteAddr net.Addr) bool {
230+
// If the mysql database is not enabled, then we don't have user information, so
231+
// go ahead and return true without trying to look up the user in the db.
232+
if !uv.db.Enabled() {
233+
return true
234+
}
235+
236+
host, err := extractHostAddress(remoteAddr)
237+
if err != nil {
238+
logrus.Warnf("error extracting host address: %v", err)
239+
return false
240+
}
241+
242+
db := uv.db
243+
rd := db.Reader()
244+
defer rd.Close()
245+
246+
if !db.Enabled() {
247+
return true
248+
}
249+
userEntry := db.GetUser(rd, user, host, false)
250+
251+
return userEntry != nil && (userEntry.Plugin == "" || userEntry.Plugin == string(mysql.MysqlNativePassword))
252+
}
253+
254+
// extractHostAddress extracts the host address from |addr|, checking to see if it is a unix socket, and if
255+
// so, returning "localhost" as the host.
256+
func extractHostAddress(addr net.Addr) (host string, err error) {
257+
if addr.Network() == "unix" {
258+
host = "localhost"
259+
} else {
260+
host, _, err = net.SplitHostPort(addr.String())
261+
if err != nil {
262+
if err.(*net.AddrError).Err == "missing port in address" {
263+
host = addr.String()
264+
} else {
265+
return "", err
266+
}
267+
}
268+
}
269+
return host, nil
270+
}
271+
272+
// validateMysqlNativePassword was taken from vitess and validates the password hash for the mysql_native_password
273+
// auth protocol. Note that this implementation has diverged slightly from the original code in Vitess.
274+
func validateMysqlNativePassword(authResponse, salt []byte, mysqlNativePassword string) bool {
275+
// SERVER: recv(authResponse)
276+
// hash_stage1=xor(authResponse, sha1(salt,hash))
277+
// candidate_hash2=sha1(hash_stage1)
278+
// check(candidate_hash2==hash)
279+
if len(authResponse) == 0 || len(mysqlNativePassword) == 0 {
280+
return false
281+
}
282+
if mysqlNativePassword[0] == '*' {
283+
mysqlNativePassword = mysqlNativePassword[1:]
284+
}
285+
286+
hash, err := hex.DecodeString(mysqlNativePassword)
287+
if err != nil {
288+
return false
289+
}
290+
291+
// scramble = SHA1(salt+hash)
292+
crypt := sha1.New()
293+
crypt.Write(salt)
294+
crypt.Write(hash)
295+
scramble := crypt.Sum(nil)
296+
297+
// token = scramble XOR stage1Hash
298+
for i := range scramble {
299+
scramble[i] ^= authResponse[i]
300+
}
301+
stage1Hash := scramble
302+
crypt.Reset()
303+
crypt.Write(stage1Hash)
304+
candidateHash2 := crypt.Sum(nil)
305+
306+
return bytes.Equal(candidateHash2, hash)
307+
}

0 commit comments

Comments
 (0)