Files
tidb/pkg/extension/auth_test.go

689 lines
32 KiB
Go

// Copyright 2024 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extension_test
import (
"crypto/sha1"
"testing"
"github.com/pingcap/errors"
"github.com/pingcap/tidb/pkg/extension"
"github.com/pingcap/tidb/pkg/parser/auth"
"github.com/pingcap/tidb/pkg/parser/mysql"
"github.com/pingcap/tidb/pkg/sessionctx/vardef"
"github.com/pingcap/tidb/pkg/sessionctx/variable"
"github.com/pingcap/tidb/pkg/testkit"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type MockAuthPlugin struct {
mock.Mock
}
func (p *MockAuthPlugin) Name() string {
return p.Called().String(0)
}
func (p *MockAuthPlugin) AuthenticateUser(ctx extension.AuthenticateRequest) error {
return p.Called(ctx).Error(0)
}
func (p *MockAuthPlugin) ValidateAuthString(hash string) bool {
return p.Called(hash).Bool(0)
}
func (p *MockAuthPlugin) GenerateAuthString(password string) (string, bool) {
args := p.Called(password)
return args.String(0), args.Bool(1)
}
func (p *MockAuthPlugin) VerifyDynamicPrivilege(ctx extension.VerifyDynamicPrivRequest) bool {
return p.Called(ctx).Bool(0)
}
func (p *MockAuthPlugin) VerifyPrivilege(ctx extension.VerifyStaticPrivRequest) bool {
return p.Called(ctx).Bool(0)
}
type AuthenticateContextMatcher struct {
expected extension.AuthenticateRequest
}
func (m AuthenticateContextMatcher) Matches(x any) bool {
ctx, ok := x.(extension.AuthenticateRequest)
if !ok {
return false
}
return ctx.User == m.expected.User &&
ctx.StoredAuthString == m.expected.StoredAuthString &&
string(ctx.InputAuthString) == string(m.expected.InputAuthString)
}
type DynamicMatcher struct {
expected extension.VerifyDynamicPrivRequest
}
func (m DynamicMatcher) Matches(x any) bool {
ctx, ok := x.(extension.VerifyDynamicPrivRequest)
if !ok {
return false
}
return ctx.User == m.expected.User &&
ctx.DynamicPriv == m.expected.DynamicPriv &&
ctx.WithGrant == m.expected.WithGrant
}
type StaticMatcher struct {
expected extension.VerifyStaticPrivRequest
}
func (m StaticMatcher) Matches(x any) bool {
ctx, ok := x.(extension.VerifyStaticPrivRequest)
if !ok {
return false
}
return ctx.User == m.expected.User &&
ctx.StaticPriv == m.expected.StaticPriv &&
ctx.DB == m.expected.DB
}
func sha1Password(s string) []byte {
crypt := sha1.New()
crypt.Write([]byte(s))
hashStage1 := crypt.Sum(nil)
crypt.Reset()
crypt.Write(hashStage1)
hashStage2 := crypt.Sum(nil)
crypt.Reset()
crypt.Write(hashStage2)
hashStage3 := crypt.Sum(nil)
for i := range hashStage3 {
hashStage3[i] ^= hashStage1[i]
}
return hashStage3
}
func TestAuthPlugin(t *testing.T) {
defer extension.Reset()
extension.Reset()
p := new(MockAuthPlugin)
p.On("Name").Return("authentication_test_plugin")
authnMatcher1 := mock.MatchedBy(func(ctx extension.AuthenticateRequest) bool {
return AuthenticateContextMatcher{expected: extension.AuthenticateRequest{User: "u2", StoredAuthString: "encodedpassword", InputAuthString: []byte("1")}}.Matches(ctx)
})
p.On("AuthenticateUser", authnMatcher1).Return(nil)
authnMatcher2 := mock.MatchedBy(func(ctx extension.AuthenticateRequest) bool {
return AuthenticateContextMatcher{expected: extension.AuthenticateRequest{User: "u2", StoredAuthString: "encodedpassword", InputAuthString: []byte("2")}}.Matches(ctx)
})
p.On("AuthenticateUser", authnMatcher2).Return(errors.New("authentication failed"))
authnMatcher3 := mock.MatchedBy(func(ctx extension.AuthenticateRequest) bool {
return AuthenticateContextMatcher{expected: extension.AuthenticateRequest{User: "u2", StoredAuthString: "anotherencodedpassword", InputAuthString: []byte("1")}}.Matches(ctx)
})
p.On("AuthenticateUser", authnMatcher3).Return(nil)
authnMatcher4 := mock.MatchedBy(func(ctx extension.AuthenticateRequest) bool {
return AuthenticateContextMatcher{expected: extension.AuthenticateRequest{User: "u2", StoredAuthString: "yetanotherencodedpassword", InputAuthString: []byte("1")}}.Matches(ctx)
})
p.On("AuthenticateUser", authnMatcher4).Return(nil)
authnMatcher5 := mock.MatchedBy(func(ctx extension.AuthenticateRequest) bool {
return AuthenticateContextMatcher{expected: extension.AuthenticateRequest{User: "u2", StoredAuthString: "yetanotherencodedpassword2", InputAuthString: []byte("1")}}.Matches(ctx)
})
p.On("AuthenticateUser", authnMatcher5).Return(nil)
p.On("ValidateAuthString", mock.Anything).Return(true)
p.On("GenerateAuthString", "rawpassword").Return("encodedpassword", true)
p.On("GenerateAuthString", "anotherrawpassword").Return("anotherencodedpassword", true)
p.On("GenerateAuthString", "yetanotherrawpassword").Return("yetanotherencodedpassword", true)
p.On("GenerateAuthString", "yetanotherrawpassword2").Return("yetanotherencodedpassword2", true)
authzMatcher1 := mock.MatchedBy(func(ctx extension.VerifyStaticPrivRequest) bool {
return StaticMatcher{expected: extension.VerifyStaticPrivRequest{User: "u2", Host: "localhost", DB: "test", Table: "t1", StaticPriv: mysql.AllPrivMask}}.Matches(ctx)
})
p.On("VerifyPrivilege", authzMatcher1).Return(true)
authzMatcher2 := mock.MatchedBy(func(ctx extension.VerifyStaticPrivRequest) bool {
return StaticMatcher{expected: extension.VerifyStaticPrivRequest{User: "u2", Host: "localhost", DB: "test", Table: "t1", StaticPriv: mysql.SelectPriv}}.Matches(ctx)
})
p.On("VerifyPrivilege", authzMatcher2).Return(true)
authzMatcher3 := mock.MatchedBy(func(ctx extension.VerifyStaticPrivRequest) bool {
return StaticMatcher{expected: extension.VerifyStaticPrivRequest{User: "u2", Host: "localhost", DB: "test", Table: "t1", StaticPriv: mysql.InsertPriv}}.Matches(ctx)
})
p.On("VerifyPrivilege", authzMatcher3).Return(false)
authzMatcher4 := mock.MatchedBy(func(ctx extension.VerifyDynamicPrivRequest) bool {
return DynamicMatcher{expected: extension.VerifyDynamicPrivRequest{User: "u2"}}.Matches(ctx)
})
p.On("VerifyDynamicPrivilege", authzMatcher4).Return(false)
deleteMatcher := mock.MatchedBy(func(ctx extension.VerifyStaticPrivRequest) bool {
return StaticMatcher{expected: extension.VerifyStaticPrivRequest{User: "u2", Host: "localhost", DB: "test", Table: "t1", StaticPriv: mysql.DeletePriv}}.Matches(ctx)
})
p.On("VerifyPrivilege", deleteMatcher).Return(false)
sysVarAdminMatcher := mock.MatchedBy(func(ctx extension.VerifyDynamicPrivRequest) bool {
return DynamicMatcher{expected: extension.VerifyDynamicPrivRequest{User: "u2", DynamicPriv: "SYSTEM_VARIABLES_ADMIN", WithGrant: false}}.Matches(ctx)
})
p.On("VerifyDynamicPrivilege", sysVarAdminMatcher).Return(false)
authChecks := []*extension.AuthPlugin{{
Name: p.Name(),
AuthenticateUser: p.AuthenticateUser,
ValidateAuthString: p.ValidateAuthString,
GenerateAuthString: p.GenerateAuthString,
VerifyPrivilege: p.VerifyPrivilege,
VerifyDynamicPrivilege: p.VerifyDynamicPrivilege,
RequiredClientSidePlugin: mysql.AuthNativePassword,
}}
require.NoError(t, extension.Register(
"extension_authentication_plugin",
extension.WithCustomAuthPlugins(authChecks),
extension.WithCustomSysVariables([]*variable.SysVar{
{
Scope: vardef.ScopeGlobal,
Name: "extension_authentication_plugin",
Value: mysql.AuthNativePassword,
Type: vardef.TypeEnum,
PossibleValues: []string{p.Name()},
},
}),
extension.WithBootstrap(func(_ extension.BootstrapContext) error {
return nil
}),
))
require.NoError(t, extension.Setup())
ext, err := extension.GetExtensions()
require.NoError(t, err)
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
tk.Session().SetExtensions(ext.NewSessionExtensions())
tk.MustExec("use test")
// Create user with an invalid plugin should not work.
tk.MustContainErrMsg("create user 'u2'@'localhost' identified with 'bad_plugin' by 'rawpassword'", "[executor:1524]Plugin 'bad_plugin' is not loaded")
// Create user with a valid plugin should work.
tk.MustExec("create user 'u2'@'localhost' identified with 'authentication_test_plugin' as 'encodedpassword'")
tk.MustQuery(`SELECT user, plugin FROM mysql.user WHERE user='u2' and host='localhost'`).Check(testkit.Rows("u2 authentication_test_plugin"))
p.AssertCalled(t, "ValidateAuthString", "encodedpassword")
p.AssertNumberOfCalls(t, "ValidateAuthString", 1)
p.AssertNotCalled(t, "GenerateAuthString")
// Alter user with an invalid plugin should not work.
tk.MustContainErrMsg("alter user 'u2'@'localhost' identified with 'bad_plugin' by 'rawpassword'", "[executor:1524]Plugin 'bad_plugin' is not loaded")
tk1 := testkit.NewTestKit(t, store)
require.NoError(t, tk1.Session().Auth(&auth.UserIdentity{Username: "root", Hostname: "localhost"}, nil, nil, nil))
tk1.MustExec("use test")
// Authentication tests.
tk2 := testkit.NewTestKit(t, store)
tk.Session().SetExtensions(ext.NewSessionExtensions())
// Login using wrong password should fail
require.EqualError(t, tk2.Session().Auth(&auth.UserIdentity{Username: "u2", Hostname: "localhost"}, []byte("2"), nil, nil), "[privilege:1045]Access denied for user 'u2'@'localhost' (using password: YES)")
// Correct password should pass
require.NoError(t, tk2.Session().Auth(&auth.UserIdentity{Username: "u2", Hostname: "localhost"}, []byte("1"), nil, nil))
// Should authenticate using plugin impl.
p.AssertNumberOfCalls(t, "AuthenticateUser", 2)
p.AssertCalled(t, "ValidateAuthString", "encodedpassword")
p.AssertNumberOfCalls(t, "ValidateAuthString", 3)
// Change password should work using ALTER USER statement.
tk.MustExec("alter user 'u2'@'localhost' identified with 'authentication_test_plugin' by 'anotherrawpassword'")
p.AssertCalled(t, "GenerateAuthString", "anotherrawpassword")
// Correct password should pass
require.NoError(t, tk2.Session().Auth(&auth.UserIdentity{Username: "u2", Hostname: "localhost"}, []byte("1"), nil, nil))
// Should authenticate using plugin impl.
p.AssertNumberOfCalls(t, "AuthenticateUser", 3)
p.AssertCalled(t, "ValidateAuthString", "anotherencodedpassword")
// Change password using SET PASSWORD statement should work using root user.
tk.MustExec("set password for 'u2'@'localhost'='yetanotherrawpassword'")
p.AssertCalled(t, "GenerateAuthString", "yetanotherrawpassword")
// Corret password should pass
require.NoError(t, tk2.Session().Auth(&auth.UserIdentity{Username: "u2", Hostname: "localhost"}, []byte("1"), nil, nil))
// Should authenticate using plugin impl.
p.AssertNumberOfCalls(t, "AuthenticateUser", 4)
p.AssertCalled(t, "ValidateAuthString", "yetanotherencodedpassword")
tk2.Session().SetExtensions(ext.NewSessionExtensions())
// Change password using SET PASSWORD statement should work using the user itself.
tk2.MustExec("set password='yetanotherrawpassword2'")
p.AssertCalled(t, "GenerateAuthString", "yetanotherrawpassword2")
// Correct password should pass
require.NoError(t, tk2.Session().Auth(&auth.UserIdentity{Username: "u2", Hostname: "localhost"}, []byte("1"), nil, nil))
// Should authenticate using plugin impl.
p.AssertNumberOfCalls(t, "AuthenticateUser", 5)
p.AssertCalled(t, "ValidateAuthString", "yetanotherencodedpassword2")
tk2.Session().SetExtensions(ext.NewSessionExtensions())
// Authorization tests.
tk1.MustExec("create table t1(id int primary key, v int)")
tk.MustExec("grant select, insert on test.t1 TO u2@localhost")
tk2.MustExec("use test")
tk1.MustExec("insert into t1 values (1, 10), (2, 20)")
tk1.MustQuery("select * from t1 where id=1").Check(testkit.Rows("1 10"))
tk1.MustQuery("select * from t1").Check(testkit.Rows("1 10", "2 20"))
// First user is not a plugin user, so it should not verify privilege using plugin impl.
p.AssertNotCalled(t, "VerifyPrivilege")
tk2.MustQuery("select * from t1 where id=1").Check(testkit.Rows("1 10"))
tk2.MustQuery("select * from t1").Check(testkit.Rows("1 10", "2 20"))
require.EqualError(t, tk2.ExecToErr("insert into t1 values (3, 30)"), "[planner:1142]INSERT command denied to user 'u2'@'localhost' for table 't1'")
// Should verify privilege using plugin impl.
p.AssertNumberOfCalls(t, "VerifyPrivilege", 3)
p.AssertCalled(t, "VerifyPrivilege", authzMatcher2)
p.AssertCalled(t, "VerifyPrivilege", authzMatcher3)
require.EqualError(t, tk2.ExecToErr("delete from t1 where id=1"), "[planner:1142]DELETE command denied to user 'u2'@'localhost' for table 't1'")
// Should not verify delete privilege using plugin impl since privilege is not granted in mysql.
p.AssertNotCalled(t, "VerifyPrivilege", deleteMatcher)
require.EqualError(t, tk2.ExecToErr("SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER'"), "[planner:1227]Access denied; you need (at least one of) the SUPER or SYSTEM_VARIABLES_ADMIN privilege(s) for this operation")
p.AssertNotCalled(t, "VerifyDynamicPrivilege", sysVarAdminMatcher)
tk.MustExec("GRANT SYSTEM_VARIABLES_ADMIN on *.* TO u2@localhost")
require.EqualError(t, tk2.ExecToErr("SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER'"), "[planner:1227]Access denied; you need (at least one of) the SUPER or SYSTEM_VARIABLES_ADMIN privilege(s) for this operation")
// Should verify dynamic privilege using plugin impl.
p.AssertCalled(t, "VerifyDynamicPrivilege", sysVarAdminMatcher)
}
func TestAuthPluginSwitchPlugins(t *testing.T) {
defer extension.Reset()
extension.Reset()
p := new(MockAuthPlugin)
p.On("Name").Return("authentication_test_plugin")
authnMatcher1 := mock.MatchedBy(func(ctx extension.AuthenticateRequest) bool {
return AuthenticateContextMatcher{expected: extension.AuthenticateRequest{User: "u2", StoredAuthString: "rawpassword", InputAuthString: []byte("1")}}.Matches(ctx)
})
p.On("AuthenticateUser", authnMatcher1).Return(nil)
authnMatcher2 := mock.MatchedBy(func(ctx extension.AuthenticateRequest) bool {
return AuthenticateContextMatcher{expected: extension.AuthenticateRequest{User: "u2", StoredAuthString: "encodedpassword", InputAuthString: []byte("1")}}.Matches(ctx)
})
p.On("AuthenticateUser", authnMatcher2).Return(nil)
p.On("ValidateAuthString", mock.Anything).Return(true)
p.On("GenerateAuthString", "rawpassword").Return("encodedpassword", true)
allPrivMatcher := mock.MatchedBy(func(ctx extension.VerifyStaticPrivRequest) bool {
return StaticMatcher{expected: extension.VerifyStaticPrivRequest{User: "u2", Host: "localhost", DB: "test", Table: "t1", StaticPriv: mysql.AllPrivMask}}.Matches(ctx)
})
p.On("VerifyPrivilege", allPrivMatcher).Return(true)
selectMatcher := mock.MatchedBy(func(ctx extension.VerifyStaticPrivRequest) bool {
return StaticMatcher{expected: extension.VerifyStaticPrivRequest{User: "u2", Host: "localhost", DB: "test", Table: "t1", StaticPriv: mysql.SelectPriv}}.Matches(ctx)
})
p.On("VerifyPrivilege", selectMatcher).Return(true)
insertMatcher := mock.MatchedBy(func(ctx extension.VerifyStaticPrivRequest) bool {
return StaticMatcher{expected: extension.VerifyStaticPrivRequest{User: "u2", Host: "localhost", DB: "test", Table: "t1", StaticPriv: mysql.InsertPriv}}.Matches(ctx)
})
p.On("VerifyPrivilege", insertMatcher).Return(false)
authChecks := []*extension.AuthPlugin{{
Name: p.Name(),
AuthenticateUser: p.AuthenticateUser,
ValidateAuthString: p.ValidateAuthString,
GenerateAuthString: p.GenerateAuthString,
VerifyPrivilege: p.VerifyPrivilege,
VerifyDynamicPrivilege: p.VerifyDynamicPrivilege,
RequiredClientSidePlugin: mysql.AuthNativePassword,
}}
require.NoError(t, extension.Register(
"extension_authentication_plugin",
extension.WithCustomAuthPlugins(authChecks),
extension.WithCustomSysVariables([]*variable.SysVar{
{
Scope: vardef.ScopeGlobal,
Name: "extension_authentication_plugin",
Value: mysql.AuthNativePassword,
Type: vardef.TypeEnum,
PossibleValues: []string{p.Name()},
},
}),
extension.WithBootstrap(func(_ extension.BootstrapContext) error {
return nil
}),
))
ext, err := extension.GetExtensions()
require.NoError(t, err)
require.NoError(t, extension.Setup())
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
tk.Session().SetExtensions(ext.NewSessionExtensions())
tk.MustExec("use test")
// Create user with a valid plugin should work.
tk.MustExec("create user 'u2'@'localhost' identified with 'authentication_test_plugin' as 'rawpassword'")
tk.MustQuery(`SELECT user, plugin FROM mysql.user WHERE user='u2' and host='localhost'`).Check(testkit.Rows("u2 authentication_test_plugin"))
p.AssertCalled(t, "ValidateAuthString", "rawpassword")
tk1 := testkit.NewTestKit(t, store)
tk1.Session().SetExtensions(ext.NewSessionExtensions())
require.NoError(t, tk1.Session().Auth(&auth.UserIdentity{Username: "root", Hostname: "localhost"}, nil, nil, nil))
tk1.MustExec("use test")
tk1.Session().SetExtensions(ext.NewSessionExtensions())
// Authentication tests.
tk2 := testkit.NewTestKit(t, store)
tk2.Session().SetExtensions(ext.NewSessionExtensions())
// Correct password should pass
require.NoError(t, tk2.Session().Auth(&auth.UserIdentity{Username: "u2", Hostname: "localhost"}, []byte("1"), nil, nil))
// Should authenticate using plugin impl.
p.AssertNumberOfCalls(t, "AuthenticateUser", 1)
p.AssertCalled(t, "ValidateAuthString", "rawpassword")
// Authorization tests.
tk1.MustExec("create table t1(id int primary key, v int)")
tk.MustExec("grant select, insert on test.t1 TO u2@localhost")
tk2.MustExec("use test")
tk1.MustExec("insert into t1 values (1, 10), (2, 20)")
tk2.MustQuery("select * from t1 where id=1").Check(testkit.Rows("1 10"))
tk2.MustQuery("select * from t1").Check(testkit.Rows("1 10", "2 20"))
require.EqualError(t, tk2.ExecToErr("insert into t1 values (3, 30)"), "[planner:1142]INSERT command denied to user 'u2'@'localhost' for table 't1'")
// Should verify privilege using plugin impl.
p.AssertNumberOfCalls(t, "VerifyPrivilege", 3)
p.AssertCalled(t, "VerifyPrivilege", selectMatcher)
p.AssertCalled(t, "VerifyPrivilege", insertMatcher)
tk.MustExec("alter user 'u2'@'localhost' identified with 'mysql_native_password' by '123'")
tk.MustQuery(`SELECT user, plugin FROM mysql.user WHERE user='u2' and host='localhost'`).Check(testkit.Rows("u2 mysql_native_password"))
// Existing session should still use plugin.
tk2.MustQuery("select * from t1 where id=1").Check(testkit.Rows("1 10"))
tk2.MustQuery("select * from t1").Check(testkit.Rows("1 10", "2 20"))
p.AssertNumberOfCalls(t, "VerifyPrivilege", 5)
// New session should not use plugin.
tk2.RefreshSession()
tk2.Session().SetExtensions(ext.NewSessionExtensions())
require.NoError(t, tk2.Session().Auth(&auth.UserIdentity{Username: "u2", Hostname: "localhost"}, sha1Password("123"), nil, nil))
tk2.MustExec("use test")
tk2.MustQuery("select * from t1 where id=1").Check(testkit.Rows("1 10"))
tk2.MustExec("insert into t1 values (3, 30), (4, 40)")
tk2.MustQuery("select * from t1 where id=3").Check(testkit.Rows("3 30"))
// Should not verify privilege using plugin impl now that the user is not using the plugin.
p.AssertNumberOfCalls(t, "VerifyPrivilege", 5)
// Switch back to plugin should work.
tk.MustExec("alter user 'u2'@'localhost' identified with 'authentication_test_plugin' by 'rawpassword'")
tk.MustQuery(`SELECT user, plugin FROM mysql.user WHERE user='u2' and host='localhost'`).Check(testkit.Rows("u2 authentication_test_plugin"))
p.AssertCalled(t, "GenerateAuthString", "rawpassword")
tk2.RefreshSession()
require.NoError(t, tk2.Session().Auth(&auth.UserIdentity{Username: "u2", Hostname: "localhost"}, []byte("1"), nil, nil))
tk2.Session().SetExtensions(ext.NewSessionExtensions())
tk2.MustExec("use test")
tk2.MustQuery("select * from t1 where id=1").Check(testkit.Rows("1 10"))
require.EqualError(t, tk2.ExecToErr("insert into t1 values (5, 50)"), "[planner:1142]INSERT command denied to user 'u2'@'localhost' for table 't1'")
// Now it should verify privilege using plugin impl.
p.AssertNumberOfCalls(t, "VerifyPrivilege", 7)
}
func TestCreateUserWhenGrant(t *testing.T) {
defer extension.Reset()
extension.Reset()
p := new(MockAuthPlugin)
p.On("Name").Return("authentication_test_plugin")
p.On("ValidateAuthString", mock.Anything).Return(true)
p.On("GenerateAuthString", "xxx").Return("encodedpassword", true)
authChecks := []*extension.AuthPlugin{{
Name: p.Name(),
AuthenticateUser: p.AuthenticateUser,
ValidateAuthString: p.ValidateAuthString,
GenerateAuthString: p.GenerateAuthString,
RequiredClientSidePlugin: mysql.AuthNativePassword,
}}
require.NoError(t, extension.Register(
"extension_authentication_plugin",
extension.WithCustomAuthPlugins(authChecks),
extension.WithCustomSysVariables([]*variable.SysVar{
{
Scope: vardef.ScopeGlobal,
Name: "extension_authentication_plugin",
Value: mysql.AuthNativePassword,
Type: vardef.TypeEnum,
PossibleValues: []string{p.Name()},
},
}),
extension.WithBootstrap(func(_ extension.BootstrapContext) error {
return nil
}),
))
require.NoError(t, extension.Setup())
ext, err := extension.GetExtensions()
require.NoError(t, err)
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
tk.Session().SetExtensions(ext.NewSessionExtensions())
tk.MustExec(`DROP USER IF EXISTS 'test'@'%'`)
// This only applies to sql_mode:NO_AUTO_CREATE_USER off
tk.MustExec(`SET SQL_MODE=''`)
tk.MustExec(`GRANT ALL PRIVILEGES ON *.* to 'test'@'%' IDENTIFIED WITH 'authentication_test_plugin' AS 'xxx'`)
// Make sure user is created automatically when grant to a non-exists one.
tk.MustQuery(`SELECT user, plugin FROM mysql.user WHERE user='test' and host='%'`).Check(
testkit.Rows("test authentication_test_plugin"),
)
tk.MustExec(`DROP USER IF EXISTS 'test'@'%'`)
// Grant without a password.
tk.MustExec(`GRANT ALL PRIVILEGES ON *.* to 'test'@'%'`)
// Make sure user is created automatically when grant to a non-exists one.
tk.MustQuery(`SELECT user, plugin FROM mysql.user WHERE user='test' and host='%'`).Check(
testkit.Rows("test mysql_native_password"),
)
tk.MustExec(`DROP USER IF EXISTS 'test'@'%'`)
}
func TestCreateViewWithPluginUser(t *testing.T) {
defer extension.Reset()
extension.Reset()
p := new(MockAuthPlugin)
p.On("Name").Return("authentication_test_plugin")
authnMatcher1 := mock.MatchedBy(func(ctx extension.AuthenticateRequest) bool {
return AuthenticateContextMatcher{expected: extension.AuthenticateRequest{User: "u1", StoredAuthString: "rawpassword", InputAuthString: []byte("1")}}.Matches(ctx)
})
p.On("AuthenticateUser", authnMatcher1).Return(nil).Return(nil)
p.On("ValidateAuthString", mock.Anything).Return(true)
p.On("GenerateAuthString", "rawpassword").Return("encodedpassword", true)
allPrivMatcherHost1 := mock.MatchedBy(func(ctx extension.VerifyStaticPrivRequest) bool {
return StaticMatcher{expected: extension.VerifyStaticPrivRequest{User: "u1", Host: "localhost", DB: "test", Table: "t1", StaticPriv: mysql.AllPrivMask}}.Matches(ctx)
})
p.On("VerifyPrivilege", allPrivMatcherHost1).Return(true)
createViewMatcher := mock.MatchedBy(func(ctx extension.VerifyStaticPrivRequest) bool {
return StaticMatcher{expected: extension.VerifyStaticPrivRequest{User: "u1", Host: "localhost", DB: "test", Table: "t1", StaticPriv: mysql.CreateViewPriv}}.Matches(ctx)
})
p.On("VerifyPrivilege", createViewMatcher).Return(true)
authChecks := []*extension.AuthPlugin{{
Name: p.Name(),
AuthenticateUser: p.AuthenticateUser,
ValidateAuthString: p.ValidateAuthString,
GenerateAuthString: p.GenerateAuthString,
VerifyPrivilege: p.VerifyPrivilege,
VerifyDynamicPrivilege: p.VerifyDynamicPrivilege,
RequiredClientSidePlugin: mysql.AuthNativePassword,
}}
require.NoError(t, extension.Register(
"extension_authentication_plugin",
extension.WithCustomAuthPlugins(authChecks),
extension.WithCustomSysVariables([]*variable.SysVar{
{
Scope: vardef.ScopeGlobal,
Name: "extension_authentication_plugin",
Value: mysql.AuthNativePassword,
Type: vardef.TypeEnum,
PossibleValues: []string{p.Name()},
},
}),
))
ext, err := extension.GetExtensions()
require.NoError(t, err)
require.NoError(t, extension.Setup())
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
tk.Session().SetExtensions(ext.NewSessionExtensions())
// Set up the table using root user.
tk.MustExec("use test")
tk.MustExec("create table t1(id int primary key, v int)")
tk.MustExec("insert into t1 values (1, 10), (2, 20)")
// Create user u1 with plugin.
tk.MustExec("create user 'u1' identified with 'authentication_test_plugin' as 'rawpassword'")
tk.MustExec("grant select, insert, create view on test.* TO u1")
// Create another user u2 without plugin.
tk.MustExec("create user 'u2'")
tk.MustExec("grant select on test.* TO u2")
tk1 := testkit.NewTestKit(t, store)
tk1.Session().SetExtensions(ext.NewSessionExtensions())
// Create one session for u1. This session does not have SELECT privilege.
require.NoError(t, tk1.Session().Auth(&auth.UserIdentity{Username: "u1", Hostname: "localhost"}, []byte("1"), nil, nil))
tk1.MustExec("use test")
selectMatcher := mock.MatchedBy(func(ctx extension.VerifyStaticPrivRequest) bool {
return StaticMatcher{expected: extension.VerifyStaticPrivRequest{User: "u1", Host: "localhost", DB: "test", Table: "t1", StaticPriv: mysql.SelectPriv}}.Matches(ctx)
})
p.On("VerifyPrivilege", selectMatcher).Return(false).Once()
// Create view should not work.
require.ErrorContains(t, tk1.ExecToErr("create view v1 as select * from t1"), "[planner:1142]SELECT command denied to user 'u1'@'%' for table 't1'")
p.AssertNumberOfCalls(t, "VerifyPrivilege", 1)
tk2 := testkit.NewTestKit(t, store)
tk2.Session().SetExtensions(ext.NewSessionExtensions())
// Create another session for u1. This session has SELECT privilege.
require.NoError(t, tk2.Session().Auth(&auth.UserIdentity{Username: "u1", Hostname: "localhost"}, []byte("1"), nil, nil))
tk2.MustExec("use test")
p.On("VerifyPrivilege", selectMatcher).Return(true)
// Create view should work.
tk2.MustExec("create view v1 as select * from t1")
tk2.MustQuery("select * from v1").Check(testkit.Rows("1 10", "2 20"))
// Session 1 should also be able to access the view now.
tk1.MustQuery("select * from v1").Check(testkit.Rows("1 10", "2 20"))
p.AssertNumberOfCalls(t, "VerifyPrivilege", 5)
tk3 := testkit.NewTestKit(t, store)
tk3.Session().SetExtensions(ext.NewSessionExtensions())
// Create another session for u1, logging in with a different host. This session has SELECT privilege.
require.NoError(t, tk3.Session().Auth(&auth.UserIdentity{Username: "u2", Hostname: "localhost"}, nil, nil, nil))
tk3.MustExec("use test")
// Now reject the SELECT privilege check for u1, but u2 should be able to access the view.
p.On("VerifyPrivilege", selectMatcher).Return(false)
tk3.MustQuery("select * from v1").Check(testkit.Rows("1 10", "2 20"))
// Should not verify privilege using the plugin since the definer is not in-session.
p.AssertNumberOfCalls(t, "VerifyPrivilege", 5)
}
func TestPluginUserModification(t *testing.T) {
defer extension.Reset()
extension.Reset()
p := new(MockAuthPlugin)
p.On("Name").Return("authentication_test_plugin")
authnMatcher1 := mock.MatchedBy(func(ctx extension.AuthenticateRequest) bool {
return AuthenticateContextMatcher{expected: extension.AuthenticateRequest{User: "u4", StoredAuthString: "rawpassword", InputAuthString: []byte("1")}}.Matches(ctx)
})
p.On("AuthenticateUser", authnMatcher1).Return(nil)
p.On("ValidateAuthString", mock.Anything).Return(true)
p.On("GenerateAuthString", mock.Anything).Return("encodedrandompassword", true)
p.On("VerifyPrivilege", mock.Anything).Return(true)
p.On("VerifyDynamicPrivilege", mock.Anything).Return(true)
authChecks := []*extension.AuthPlugin{{
Name: p.Name(),
AuthenticateUser: p.AuthenticateUser,
ValidateAuthString: p.ValidateAuthString,
GenerateAuthString: p.GenerateAuthString,
VerifyPrivilege: p.VerifyPrivilege,
VerifyDynamicPrivilege: p.VerifyDynamicPrivilege,
RequiredClientSidePlugin: mysql.AuthNativePassword,
}}
require.NoError(t, extension.Register(
"extension_authentication_plugin",
extension.WithCustomAuthPlugins(authChecks),
extension.WithCustomSysVariables([]*variable.SysVar{
{
Scope: vardef.ScopeGlobal,
Name: "extension_authentication_plugin",
Value: mysql.AuthNativePassword,
Type: vardef.TypeEnum,
PossibleValues: []string{p.Name()},
},
}),
))
ext, err := extension.GetExtensions()
require.NoError(t, err)
require.NoError(t, extension.Setup())
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
tk.Session().SetExtensions(ext.NewSessionExtensions())
// Create user u1 with plugin with super privilege
tk.MustExec("create user 'u1' identified with 'authentication_test_plugin' as 'rawpassword'")
tk.MustExec("grant super on *.* to u1")
// Create a non-plugin user u2 with create user privilege.
tk.MustExec("create user 'u2'")
tk.MustExec("grant create user on *.* to u2")
// Create a non-plugin user u3 with create user and system_user privilege. It also needs the super privilege so we can run set password on u1.
tk.MustExec("create user 'u3'")
tk.MustExec("grant create user, system_user, super on *.* to u3")
// Create another super plugin user u4 to check if it can run admin operations on u3.
tk.MustExec("create user 'u4' identified with 'authentication_test_plugin' as 'rawpassword'")
tk.MustExec("grant create user, super on *.* to u4")
tk2 := testkit.NewTestKit(t, store)
tk2.Session().SetExtensions(ext.NewSessionExtensions())
require.NoError(t, tk2.Session().Auth(&auth.UserIdentity{Username: "u2", Hostname: "localhost"}, nil, nil, nil))
// User u2 should not be able to alter user u1 or drop it.
tk2.MustContainErrMsg("drop user u1", "[planner:1227]Access denied; you need (at least one of) the SYSTEM_USER or SUPER privilege(s) for this operation")
tk2.MustContainErrMsg("alter user u1 identified with 'authentication_test_plugin' as 'randompassword'", "[planner:1227]Access denied; you need (at least one of) the SYSTEM_USER or SUPER privilege(s) for this operation")
// Should not even call the plugin to verify privilege of u1 at all.
p.AssertNumberOfCalls(t, "VerifyPrivilege", 0)
p.AssertNumberOfCalls(t, "VerifyDynamicPrivilege", 0)
tk3 := testkit.NewTestKit(t, store)
tk3.Session().SetExtensions(ext.NewSessionExtensions())
require.NoError(t, tk3.Session().Auth(&auth.UserIdentity{Username: "u3", Hostname: "localhost"}, nil, nil, nil))
// Should be able to run these admin operations on u1.
tk3.MustExec("alter user u1 identified with 'authentication_test_plugin' as 'randompassword'")
tk3.MustExec("set password for 'u1'='random2password'")
tk3.MustExec("drop user u1")
// Should not even call the plugin to verify privilege of u1 at all.
p.AssertNumberOfCalls(t, "VerifyPrivilege", 0)
p.AssertNumberOfCalls(t, "VerifyDynamicPrivilege", 0)
tk4 := testkit.NewTestKit(t, store)
tk4.Session().SetExtensions(ext.NewSessionExtensions())
require.NoError(t, tk4.Session().Auth(&auth.UserIdentity{Username: "u4", Hostname: "localhost"}, []byte("1"), nil, nil))
// Should be able to run these admin operations on u3.
tk4.MustExec("alter user u3 identified by 'blah'")
tk4.MustExec("set password for 'u3'='blahblah'")
tk4.MustExec("drop user u3")
// Should have called the plugin to verify privilege of u4 since it's in-session for CREATE_USER/SUPER privileges.
p.AssertNumberOfCalls(t, "VerifyPrivilege", 3)
// Dynamic privilege should be checked for u4. for SYSTEM_USER and RESTRICTED_USER_ADMIN.
p.AssertNumberOfCalls(t, "VerifyDynamicPrivilege", 4)
}