Skip to content

Commit 0520a6c

Browse files
committed
Support for creating users with the caching_sha2_password auth plugin
1 parent 6ffe4b8 commit 0520a6c

File tree

12 files changed

+177
-50
lines changed

12 files changed

+177
-50
lines changed

enginetest/queries/priv_auth_queries.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -692,15 +692,55 @@ var UserPrivTests = []UserPrivilegeTest{
692692
},
693693
},
694694
{
695-
Query: "SELECT Host, User FROM mysql.user;",
695+
Query: "SELECT Host, User, Plugin, length(authentication_string) > 0 FROM mysql.user order by User;",
696696
Expected: []sql.Row{
697-
{"localhost", "root"},
698-
{"localhost", "testuser2"},
699-
{"127.0.0.1", "testuser"},
697+
{"localhost", "root", "mysql_native_password", false},
698+
{"127.0.0.1", "testuser", "mysql_native_password", false},
699+
// testuser2 was inserted directly into the table, so it uses the column default
700+
// from the plugin field – caching_sha2_password
701+
{"localhost", "testuser2", "caching_sha2_password", false},
700702
},
701703
},
702704
},
703705
},
706+
{
707+
Name: "User creation with auth plugin specified: mysql_native_password",
708+
SetUpScript: []string{
709+
"CREATE USER testuser1@`127.0.0.1` identified with mysql_native_password by 'pass1';",
710+
"CREATE USER testuser2@`127.0.0.1` identified with 'mysql_native_password';",
711+
},
712+
Assertions: []UserPrivilegeTestAssertion{
713+
{
714+
Query: "select user, host, plugin, authentication_string from mysql.user where user='testuser1';",
715+
Expected: []sql.Row{{"testuser1", "127.0.0.1", "mysql_native_password", "*22A99BA288DB55E8E230679259740873101CD636"}},
716+
},
717+
{
718+
Query: "select user, host, plugin, authentication_string from mysql.user where user='testuser2';",
719+
Expected: []sql.Row{{"testuser2", "127.0.0.1", "mysql_native_password", ""}},
720+
},
721+
},
722+
},
723+
{
724+
Name: "User creation with auth plugin specified: caching_sha2_password",
725+
SetUpScript: []string{
726+
"CREATE USER testuser1@`127.0.0.1` identified with caching_sha2_password by 'pass1';",
727+
"CREATE USER testuser2@`127.0.0.1` identified with 'caching_sha2_password';",
728+
},
729+
Assertions: []UserPrivilegeTestAssertion{
730+
{
731+
// caching_sha2_password auth uses a random salt to create the authentication
732+
// string. Since it's not a consistent value during each test run, we just sanity
733+
// check the first bytes of metadata (digest type, iterations) in the auth string.
734+
Query: "select user, host, plugin, authentication_string like '$A$005$%' from mysql.user where user='testuser1';",
735+
Expected: []sql.Row{{"testuser1", "127.0.0.1", "caching_sha2_password", true}},
736+
},
737+
{
738+
Query: "select user, host, plugin, authentication_string like '$A$005$%' from mysql.user where user='testuser2';",
739+
Expected: []sql.Row{{"testuser2", "127.0.0.1", "caching_sha2_password", true}},
740+
},
741+
},
742+
},
743+
704744
{
705745
Name: "Dynamic privilege support",
706746
SetUpScript: []string{

sql/mysql_db/auth.go

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ import (
2727
"github.com/dolthub/go-mysql-server/sql"
2828
)
2929

30+
// DefaultAuthMethod specifies the MySQL auth protocol (e.g. mysql_native_password,
31+
// caching_sha2_password) that is used by default. When the auth server advertises
32+
// what auth protocol it prefers, as part of the auth handshake, it is controlled
33+
// by this constant. When a new user is created, if no auth plugin is specified, this
34+
// auth method will be used.
35+
const DefaultAuthMethod = mysql.MysqlNativePassword
36+
3037
// authServer implements the mysql.AuthServer interface. It exposes configured AuthMethod implementations
3138
// that the auth framework in Vitess uses to negotiate authentication with a client. By default, authServer
3239
// configures support for the mysql_native_password auth plugin, as well as an extensible auth method, built
@@ -42,10 +49,10 @@ var _ mysql.AuthServer = (*authServer)(nil)
4249
// mysql_native_password support, as well as an extensible auth method, built on the mysql_clear_password auth
4350
// method, that allows integrators to extend authentication to allow additional schemes.
4451
func newAuthServer(db *MySQLDb) *authServer {
45-
// The native password auth method allows auth over the mysql_native_password protocol
52+
// mysql_native_password auth support
4653
nativePasswordAuthMethod := mysql.NewMysqlNativeAuthMethod(
4754
&nativePasswordHashStorage{db: db},
48-
&nativePasswordUserValidator{db: db})
55+
newUserValidator(db, mysql.MysqlNativePassword))
4956

5057
// TODO: Add CachingSha2Password AuthMethod
5158

@@ -67,7 +74,7 @@ func (as *authServer) AuthMethods() []mysql.AuthMethod {
6774

6875
// DefaultAuthMethodDescription implements the mysql.AuthServer interface.
6976
func (db *authServer) DefaultAuthMethodDescription() mysql.AuthMethodDescription {
70-
return mysql.MysqlNativePassword
77+
return DefaultAuthMethod
7178
}
7279

7380
// extendedAuthPlainTextStorage implements the mysql.PlainTextStorage interface and plugs into
@@ -205,8 +212,8 @@ func (nphs *nativePasswordHashStorage) UserEntryWithHash(_ []*x509.Certificate,
205212
if userEntry == nil || userEntry.Locked {
206213
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
207214
}
208-
if len(userEntry.Password) > 0 {
209-
if !validateMysqlNativePassword(authResponse, salt, userEntry.Password) {
215+
if len(userEntry.AuthString) > 0 {
216+
if !validateMysqlNativePassword(authResponse, salt, userEntry.AuthString) {
210217
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
211218
}
212219
} else if len(authResponse) > 0 {
@@ -218,18 +225,32 @@ func (nphs *nativePasswordHashStorage) UserEntryWithHash(_ []*x509.Certificate,
218225
return sql.MysqlConnectionUser{User: userEntry.User, Host: userEntry.Host}, nil
219226
}
220227

221-
// nativePasswordUserValidator implements the mysql.UserValidator interface and plugs into the mysql_native_password
222-
// auth method in Vitess. This implementation is called by the native password auth method to determine if a specific
223-
// user and remote address can connect to this server via the mysql_native_password auth protocol.
224-
type nativePasswordUserValidator struct {
228+
// userValidator implements the mysql.UserValidator interface. It looks up a user and host from the
229+
// associated mysql database (|db|) and validates that a user entry exists and that it is configured
230+
// for the specified authentication plugin (|authMethod|).
231+
type userValidator struct {
232+
// db is the mysql database that contains user information
225233
db *MySQLDb
234+
235+
// authMethod is the name of the auth plugin for which this validator will
236+
// validate users.
237+
authMethod mysql.AuthMethodDescription
226238
}
227239

228-
var _ mysql.UserValidator = (*nativePasswordUserValidator)(nil)
240+
var _ mysql.UserValidator = (*userValidator)(nil)
241+
242+
// newUserValidator creates a new userValidator instance, configured to use |db| to look up user
243+
// entries and validate that they have the specified auth plugin (|authMethod|) configured.
244+
func newUserValidator(db *MySQLDb, authMethod mysql.AuthMethodDescription) *userValidator {
245+
return &userValidator{
246+
db: db,
247+
authMethod: authMethod,
248+
}
249+
}
229250

230251
// HandleUser implements the mysql.UserValidator interface and verifies if the mysql_native_password auth method
231252
// can be used for the specified |user| at the specified |remoteAddr|.
232-
func (uv *nativePasswordUserValidator) HandleUser(user string, remoteAddr net.Addr) bool {
253+
func (uv *userValidator) HandleUser(user string, remoteAddr net.Addr) bool {
233254
// If the mysql database is not enabled, then we don't have user information, so
234255
// go ahead and return true without trying to look up the user in the db.
235256
if !uv.db.Enabled() {
@@ -251,7 +272,7 @@ func (uv *nativePasswordUserValidator) HandleUser(user string, remoteAddr net.Ad
251272
}
252273
userEntry := db.GetUser(rd, user, host, false)
253274

254-
return userEntry != nil && (userEntry.Plugin == "" || userEntry.Plugin == string(mysql.MysqlNativePassword))
275+
return userEntry != nil && userEntry.Plugin == string(uv.authMethod)
255276
}
256277

257278
// extractHostAddress extracts the host address from |addr|, checking to see if it is a unix socket, and if

sql/mysql_db/mysql_db.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -780,8 +780,8 @@ func (db *MySQLDb) ValidateHash(salt []byte, user string, authResponse []byte, a
780780
if userEntry == nil || userEntry.Locked {
781781
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
782782
}
783-
if len(userEntry.Password) > 0 {
784-
if !validateMysqlNativePassword(authResponse, salt, userEntry.Password) {
783+
if len(userEntry.AuthString) > 0 {
784+
if !validateMysqlNativePassword(authResponse, salt, userEntry.AuthString) {
785785
return nil, mysql.NewSQLError(mysql.ERAccessDeniedError, mysql.SSAccessDeniedError, "Access denied for user '%v'", user)
786786
}
787787
} else if len(authResponse) > 0 { // password is nil or empty, therefore no password is set

sql/mysql_db/mysql_db_load.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func LoadUser(serialUser *serial.User) *User {
132132
Host: string(serialUser.Host()),
133133
PrivilegeSet: *privilegeSet,
134134
Plugin: string(serialUser.Plugin()),
135-
Password: string(serialUser.Password()),
135+
AuthString: string(serialUser.Password()),
136136
PasswordLastChanged: time.Unix(serialUser.PasswordLastChanged(), 0),
137137
Locked: serialUser.Locked(),
138138
Attributes: attributes,

sql/mysql_db/mysql_db_serialize.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ func serializeUser(b *flatbuffers.Builder, users []*User) flatbuffers.UOffsetT {
169169
host := b.CreateString(user.Host)
170170
privilegeSet := serializePrivilegeSet(b, &user.PrivilegeSet)
171171
plugin := b.CreateString(user.Plugin)
172-
password := b.CreateString(user.Password)
172+
authString := b.CreateString(user.AuthString)
173173
attributes := serializeAttributes(b, user.Attributes)
174174
identity := b.CreateString(user.Identity)
175175

@@ -178,7 +178,7 @@ func serializeUser(b *flatbuffers.Builder, users []*User) flatbuffers.UOffsetT {
178178
serial.UserAddHost(b, host)
179179
serial.UserAddPrivilegeSet(b, privilegeSet)
180180
serial.UserAddPlugin(b, plugin)
181-
serial.UserAddPassword(b, password)
181+
serial.UserAddPassword(b, authString)
182182
serial.UserAddPasswordLastChanged(b, user.PasswordLastChanged.Unix())
183183
serial.UserAddLocked(b, user.Locked)
184184
serial.UserAddAttributes(b, attributes)

sql/mysql_db/user.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type User struct {
3030
Host string
3131
PrivilegeSet PrivilegeSet
3232
Plugin string
33-
Password string
33+
AuthString string
3434
PasswordLastChanged time.Time
3535
Locked bool
3636
Attributes *string
@@ -56,7 +56,7 @@ func UserToRow(ctx *sql.Context, u *User) (sql.Row, error) {
5656
row[userTblColIndex_User] = u.User
5757
row[userTblColIndex_Host] = u.Host
5858
row[userTblColIndex_plugin] = u.Plugin
59-
row[userTblColIndex_authentication_string] = u.Password
59+
row[userTblColIndex_authentication_string] = u.AuthString
6060
row[userTblColIndex_password_last_changed] = u.PasswordLastChanged
6161
row[userTblColIndex_identity] = u.Identity
6262
if u.Locked {
@@ -87,7 +87,7 @@ func UserFromRow(ctx *sql.Context, row sql.Row) (*User, error) {
8787
Host: row[userTblColIndex_Host].(string),
8888
PrivilegeSet: UserRowToPrivSet(ctx, row),
8989
Plugin: row[userTblColIndex_plugin].(string),
90-
Password: row[userTblColIndex_authentication_string].(string),
90+
AuthString: row[userTblColIndex_authentication_string].(string),
9191
PasswordLastChanged: passwordLastChanged,
9292
Locked: row[userTblColIndex_account_locked].(uint16) == 2,
9393
Attributes: attributes,
@@ -117,7 +117,7 @@ func UserEquals(left, right *User) bool {
117117
if left.User != right.User ||
118118
left.Host != right.Host ||
119119
left.Plugin != right.Plugin ||
120-
left.Password != right.Password ||
120+
left.AuthString != right.AuthString ||
121121
left.Identity != right.Identity ||
122122
!left.PasswordLastChanged.Equal(right.PasswordLastChanged) ||
123123
left.Locked != right.Locked ||

sql/mysql_db/user_table.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,13 @@ func init() {
215215
}
216216
}
217217

218-
func addSuperUser(ed *Editor, username string, host string, password string) {
218+
func addSuperUser(ed *Editor, username string, host string, authString string) {
219219
ed.PutUser(&User{
220220
User: username,
221221
Host: host,
222222
PrivilegeSet: NewPrivilegeSetWithAllPrivileges(),
223223
Plugin: "mysql_native_password",
224-
Password: password,
224+
AuthString: authString,
225225
PasswordLastChanged: time.Unix(1, 0).UTC(),
226226
Locked: false,
227227
Attributes: nil,

sql/mysql_db/user_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestUserJson(t *testing.T) {
3434
Host: "localhost",
3535
PrivilegeSet: NewPrivilegeSet(),
3636
Plugin: "mysql_native_password",
37-
Password: "*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19",
37+
AuthString: "*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19",
3838
PasswordLastChanged: time.Unix(184301, 0),
3939
Locked: false,
4040
Attributes: nil,

sql/plan/create_user_data.go

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"encoding/hex"
2020
"fmt"
2121
"strings"
22+
23+
"github.com/dolthub/vitess/go/mysql"
2224
)
2325

2426
// UserName represents either a user or role name.
@@ -47,8 +49,9 @@ func (un *UserName) String(quote string) string {
4749
type Authentication interface {
4850
// Plugin returns the name of the plugin that this authentication represents.
4951
Plugin() string
50-
// Password returns the value to insert into the database as the password.
51-
Password() string
52+
// AuthString returns the authentication string that encodes the password
53+
// in an obscured or hashed form specific to an auth plugin's protocol.
54+
AuthString() (string, error)
5255
}
5356

5457
// AuthenticatedUser represents a user with the relevant methods of authentication.
@@ -89,20 +92,64 @@ type PasswordOptions struct {
8992
LockTime *int64
9093
}
9194

95+
// CachingSha2PasswordAuthentication implements the Authentication interface for the
96+
// caching_sha2_password auth plugin.
97+
type CachingSha2PasswordAuthentication struct {
98+
password string
99+
authorizationStringBytes []byte
100+
}
101+
102+
var _ Authentication = (*CachingSha2PasswordAuthentication)(nil)
103+
104+
// NewCachingSha2PasswordAuthentication creates a new CachingSha2PasswordAuthentication
105+
// instance that will obscure the specified |password| when the AuthString() method is called.
106+
func NewCachingSha2PasswordAuthentication(password string) Authentication {
107+
return CachingSha2PasswordAuthentication{password: password}
108+
}
109+
110+
// Plugin implements the Authentication interface.
111+
func (a CachingSha2PasswordAuthentication) Plugin() string {
112+
return string(mysql.CachingSha2Password)
113+
}
114+
115+
// AuthString implements the Authentication interface.
116+
func (a CachingSha2PasswordAuthentication) AuthString() (string, error) {
117+
// We cache the computed authorization data, since it's expensive to compute
118+
// and we must return the same authorization data on multiple calls, not
119+
// generate new auth data with a new salt.
120+
if a.authorizationStringBytes != nil {
121+
return string(a.authorizationStringBytes), nil
122+
}
123+
124+
salt, err := mysql.NewSalt()
125+
if err != nil {
126+
return "", err
127+
}
128+
129+
authorizationStringBytes, err := mysql.SerializeCachingSha2PasswordAuthString(
130+
a.password, salt, mysql.DefaultCachingSha2PasswordHashIterations)
131+
if err != nil {
132+
return "", err
133+
}
134+
a.authorizationStringBytes = authorizationStringBytes
135+
136+
return string(a.authorizationStringBytes), nil
137+
}
138+
92139
// AuthenticationMysqlNativePassword is an authentication type that represents "mysql_native_password".
93140
type AuthenticationMysqlNativePassword string
94141

95142
var _ Authentication = AuthenticationMysqlNativePassword("")
96143

97144
// Plugin implements the interface Authentication.
98145
func (a AuthenticationMysqlNativePassword) Plugin() string {
99-
return "mysql_native_password"
146+
return string(mysql.MysqlNativePassword)
100147
}
101148

102-
// Password implements the interface Authentication.
103-
func (a AuthenticationMysqlNativePassword) Password() string {
149+
// AuthString implements the interface Authentication.
150+
func (a AuthenticationMysqlNativePassword) AuthString() (string, error) {
104151
if len(a) == 0 {
105-
return ""
152+
return "", nil
106153
}
107154
// native = sha1(sha1(password))
108155
hash := sha1.New()
@@ -111,7 +158,7 @@ func (a AuthenticationMysqlNativePassword) Password() string {
111158
hash.Reset()
112159
hash.Write(s1)
113160
s2 := hash.Sum(nil)
114-
return "*" + strings.ToUpper(hex.EncodeToString(s2))
161+
return "*" + strings.ToUpper(hex.EncodeToString(s2)), nil
115162
}
116163

117164
// NewDefaultAuthentication returns the given password with the default
@@ -137,9 +184,10 @@ func (a AuthenticationOther) Plugin() string {
137184
return a.plugin
138185
}
139186

140-
func (a AuthenticationOther) Password() string {
187+
// AuthString implements the interface Authentication.
188+
func (a AuthenticationOther) AuthString() (string, error) {
141189
if a.password == "" {
142-
return a.identity
190+
return a.identity, nil
143191
}
144-
return string(a.password)
192+
return a.password, nil
145193
}

sql/planbuilder/priv.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/dolthub/go-mysql-server/sql"
2525
"github.com/dolthub/go-mysql-server/sql/plan"
26+
"github.com/dolthub/vitess/go/mysql"
2627
)
2728

2829
func convertAccountName(names ...ast.AccountName) []plan.UserName {
@@ -153,8 +154,14 @@ func (b *Builder) buildAuthenticatedUser(user ast.AccountWithAuth) plan.Authenti
153154
}
154155
if user.Auth1 != nil {
155156
authUser.Identity = user.Auth1.Identity
156-
if user.Auth1.Plugin == "mysql_native_password" && len(user.Auth1.Password) > 0 {
157+
if user.Auth1.Password == "" && user.Auth1.Identity != "" {
158+
// If an identity has been specified, instead of a password, then use the auth details
159+
// directly, without an Authentication implementation that would obscure the password.
160+
authUser.Auth1 = plan.NewOtherAuthentication(user.Auth1.Password, user.Auth1.Plugin, user.Auth1.Identity)
161+
} else if user.Auth1.Plugin == string(mysql.MysqlNativePassword) {
157162
authUser.Auth1 = plan.AuthenticationMysqlNativePassword(user.Auth1.Password)
163+
} else if user.Auth1.Plugin == string(mysql.CachingSha2Password) {
164+
authUser.Auth1 = plan.NewCachingSha2PasswordAuthentication(user.Auth1.Password)
158165
} else if len(user.Auth1.Plugin) > 0 {
159166
authUser.Auth1 = plan.NewOtherAuthentication(user.Auth1.Password, user.Auth1.Plugin, user.Auth1.Identity)
160167
} else {

0 commit comments

Comments
 (0)