Skip to content

Commit 87f6c7b

Browse files
authored
executor: introduce max_user_connections (pingcap#59197)
close pingcap#59203
1 parent 538bab6 commit 87f6c7b

23 files changed

+532
-21
lines changed

br/pkg/restore/snap_client/systable_restore_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ func TestCheckSysTableCompatibility(t *testing.T) {
114114
// - IF it is an system privilege table, please add the table name into `sysPrivilegeTableMap`.
115115
// - IF it is an statistics table, please add the table name into `statsTables`.
116116
//
117+
117118
// The above variables are in the file br/pkg/restore/systable_restore.go
118119
func TestMonitorTheSystemTableIncremental(t *testing.T) {
119-
require.Equal(t, int64(243), session.CurrentBootstrapVersion)
120+
require.Equal(t, int64(244), session.CurrentBootstrapVersion)
120121
}

errors.toml

+5
Original file line numberDiff line numberDiff line change
@@ -2976,6 +2976,11 @@ error = '''
29762976
Aborted connection %d to db: '%-.192s' user: '%-.48s' host: '%-.255s' (%-.64s)
29772977
'''
29782978

2979+
["server:1203"]
2980+
error = '''
2981+
User %-.64s has exceeded the 'max_user_connections' resource
2982+
'''
2983+
29792984
["server:1251"]
29802985
error = '''
29812986
Client does not support authentication protocol requested by server; consider upgrading MySQL client

pkg/errno/errname.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ var MySQLErrName = map[uint16]*mysql.ErrMessage{
217217
ErrCrashedOnRepair: mysql.Message("Table '%-.192s' is marked as crashed and last (automatic?) repair failed", nil),
218218
ErrWarningNotCompleteRollback: mysql.Message("Some non-transactional changed tables couldn't be rolled back", nil),
219219
ErrTransCacheFull: mysql.Message("Multi-statement transaction required more than 'maxBinlogCacheSize' bytes of storage; increase this mysqld variable and try again", nil),
220-
ErrTooManyUserConnections: mysql.Message("User %-.64s already has more than 'maxUserConnections' active connections", nil),
220+
ErrTooManyUserConnections: mysql.Message("User %-.64s has exceeded the 'max_user_connections' resource", nil),
221221
ErrSetConstantsOnly: mysql.Message("You may only use constant expressions with SET", nil),
222222
ErrLockWaitTimeout: mysql.Message("Lock wait timeout exceeded; try restarting transaction", nil),
223223
ErrLockTableFull: mysql.Message("The total number of locks exceeds the lock table size", nil),

pkg/executor/show.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -1755,7 +1755,8 @@ func (e *ShowExec) fetchShowCreateUser(ctx context.Context) error {
17551755
`SELECT plugin, Account_locked, user_attributes->>'$.metadata', Token_issuer,
17561756
Password_reuse_history, Password_reuse_time, Password_expired, Password_lifetime,
17571757
user_attributes->>'$.Password_locking.failed_login_attempts',
1758-
user_attributes->>'$.Password_locking.password_lock_time_days', authentication_string
1758+
user_attributes->>'$.Password_locking.password_lock_time_days', authentication_string,
1759+
Max_user_connections
17591760
FROM %n.%n WHERE User=%? AND Host=%?`,
17601761
mysql.SystemDB, mysql.UserTable, userName, strings.ToLower(hostName))
17611762
if err != nil {
@@ -1835,6 +1836,12 @@ func (e *ShowExec) fetchShowCreateUser(ctx context.Context) error {
18351836
}
18361837
authData := rows[0].GetString(10)
18371838

1839+
maxUserConnections := rows[0].GetInt64(11)
1840+
maxUserConnectionsStr := ""
1841+
if maxUserConnections > 0 {
1842+
maxUserConnectionsStr = fmt.Sprintf(" WITH MAX_USER_CONNECTIONS %d", maxUserConnections)
1843+
}
1844+
18381845
rows, _, err = exec.ExecRestrictedSQL(ctx, nil, `SELECT Priv FROM %n.%n WHERE User=%? AND Host=%?`, mysql.SystemDB, mysql.GlobalPrivTable, userName, hostName)
18391846
if err != nil {
18401847
return errors.Trace(err)
@@ -1857,8 +1864,8 @@ func (e *ShowExec) fetchShowCreateUser(ctx context.Context) error {
18571864
}
18581865

18591866
// FIXME: the returned string is not escaped safely
1860-
showStr := fmt.Sprintf("CREATE USER '%s'@'%s' IDENTIFIED WITH '%s'%s REQUIRE %s%s %s ACCOUNT %s PASSWORD HISTORY %s PASSWORD REUSE INTERVAL %s%s%s%s",
1861-
e.User.Username, e.User.Hostname, authPlugin, authStr, require, tokenIssuer, passwordExpiredStr, accountLocked, passwordHistory, passwordReuseInterval, failedLoginAttempts, passwordLockTimeDays, userAttributes)
1867+
showStr := fmt.Sprintf("CREATE USER '%s'@'%s' IDENTIFIED WITH '%s'%s REQUIRE %s%s%s %s ACCOUNT %s PASSWORD HISTORY %s PASSWORD REUSE INTERVAL %s%s%s%s",
1868+
e.User.Username, e.User.Hostname, authPlugin, authStr, require, tokenIssuer, maxUserConnectionsStr, passwordExpiredStr, accountLocked, passwordHistory, passwordReuseInterval, failedLoginAttempts, passwordLockTimeDays, userAttributes)
18621869
e.appendRow([]any{showStr})
18631870
return nil
18641871
}

pkg/executor/simple.go

+64-3
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ type SimpleExec struct {
9696
staleTxnStartTS uint64
9797
}
9898

99+
// resourceOptionsInfo represents the resource infomations to limit user.
100+
// It contains 'MAX_QUERIES_PER_HOUR', 'MAX_UPDATES_PER_HOUR', 'MAX_CONNECTIONS_PER_HOUR' and 'MAX_USER_CONNECTIONS'.
101+
// It only implements the option of 'MAX_USER_CONNECTIONS' now.
102+
// To do: implement the other three options.
103+
type resourceOptionsInfo struct {
104+
maxQueriesPerHour int64
105+
maxUpdatesPerHour int64
106+
maxConnectionsPerHour int64
107+
maxUserConnections int64
108+
}
109+
99110
type passwordOrLockOptionsInfo struct {
100111
lockAccount string
101112
passwordExpired string
@@ -817,6 +828,22 @@ func (e *SimpleExec) executeRollback(s *ast.RollbackStmt) error {
817828
return nil
818829
}
819830

831+
func (info *resourceOptionsInfo) loadResourceOptions(userResource []*ast.ResourceOption) error {
832+
for _, option := range userResource {
833+
switch option.Type {
834+
case ast.MaxQueriesPerHour:
835+
info.maxQueriesPerHour = min(option.Count, math.MaxInt16)
836+
case ast.MaxUpdatesPerHour:
837+
info.maxUpdatesPerHour = min(option.Count, math.MaxInt16)
838+
case ast.MaxConnectionsPerHour:
839+
info.maxConnectionsPerHour = min(option.Count, math.MaxInt16)
840+
case ast.MaxUserConnections:
841+
info.maxUserConnections = min(option.Count, math.MaxInt16)
842+
}
843+
}
844+
return nil
845+
}
846+
820847
func whetherSavePasswordHistory(plOptions *passwordOrLockOptionsInfo) bool {
821848
var passwdSaveNum, passwdSaveTime int64
822849
// If the user specifies a default, read the global variable.
@@ -1046,6 +1073,18 @@ func (e *SimpleExec) executeCreateUser(ctx context.Context, s *ast.CreateUserStm
10461073
return err
10471074
}
10481075

1076+
userResource := &resourceOptionsInfo{
1077+
maxQueriesPerHour: 0,
1078+
maxUpdatesPerHour: 0,
1079+
maxConnectionsPerHour: 0,
1080+
maxUserConnections: 0,
1081+
}
1082+
1083+
err = userResource.loadResourceOptions(s.ResourceOptions)
1084+
if err != nil {
1085+
return err
1086+
}
1087+
10491088
plOptions := &passwordOrLockOptionsInfo{
10501089
lockAccount: "N",
10511090
passwordExpired: "N",
@@ -1114,8 +1153,8 @@ func (e *SimpleExec) executeCreateUser(ctx context.Context, s *ast.CreateUserStm
11141153
passwordInit := true
11151154
// Get changed user password reuse info.
11161155
savePasswdHistory := whetherSavePasswordHistory(plOptions)
1117-
sqlTemplate := "INSERT INTO %n.%n (Host, User, authentication_string, plugin, user_attributes, Account_locked, Token_issuer, Password_expired, Password_lifetime, Password_reuse_time, Password_reuse_history) VALUES "
1118-
valueTemplate := "(%?, %?, %?, %?, %?, %?, %?, %?, %?"
1156+
sqlTemplate := "INSERT INTO %n.%n (Host, User, authentication_string, plugin, user_attributes, Account_locked, Token_issuer, Password_expired, Password_lifetime, Max_user_connections, Password_reuse_time, Password_reuse_history) VALUES "
1157+
valueTemplate := "(%?, %?, %?, %?, %?, %?, %?, %?, %?, %?"
11191158

11201159
sqlescape.MustFormatSQL(sql, sqlTemplate, mysql.SystemDB, mysql.UserTable)
11211160
if savePasswdHistory {
@@ -1200,7 +1239,7 @@ func (e *SimpleExec) executeCreateUser(ctx context.Context, s *ast.CreateUserStm
12001239
}
12011240

12021241
hostName := strings.ToLower(spec.User.Hostname)
1203-
sqlescape.MustFormatSQL(sql, valueTemplate, hostName, spec.User.Username, pwd, authPlugin, userAttributesStr, plOptions.lockAccount, recordTokenIssuer, plOptions.passwordExpired, plOptions.passwordLifetime)
1242+
sqlescape.MustFormatSQL(sql, valueTemplate, hostName, spec.User.Username, pwd, authPlugin, userAttributesStr, plOptions.lockAccount, recordTokenIssuer, plOptions.passwordExpired, plOptions.passwordLifetime, userResource.maxUserConnections)
12041243
// add Password_reuse_time value.
12051244
if plOptions.passwordReuseIntervalChange && (plOptions.passwordReuseInterval != notSpecified) {
12061245
sqlescape.MustFormatSQL(sql, `, %?`, plOptions.passwordReuseInterval)
@@ -1695,6 +1734,20 @@ func (e *SimpleExec) executeAlterUser(ctx context.Context, s *ast.AlterUserStmt)
16951734
s.Specs = []*ast.UserSpec{spec}
16961735
}
16971736

1737+
userResource := &resourceOptionsInfo{
1738+
maxQueriesPerHour: 0,
1739+
maxUpdatesPerHour: 0,
1740+
maxConnectionsPerHour: 0,
1741+
// can't set 0 to maxUserConnections as default, because user could set 0 to this field.
1742+
// so we use -1(invalid value) as default.
1743+
maxUserConnections: -1,
1744+
}
1745+
1746+
err = userResource.loadResourceOptions(s.ResourceOptions)
1747+
if err != nil {
1748+
return err
1749+
}
1750+
16981751
plOptions := passwordOrLockOptionsInfo{
16991752
lockAccount: "",
17001753
passwordExpired: "",
@@ -1924,6 +1977,14 @@ func (e *SimpleExec) executeAlterUser(ctx context.Context, s *ast.AlterUserStmt)
19241977
fields = append(fields, alterField{"password_lifetime=%?", plOptions.passwordLifetime})
19251978
}
19261979

1980+
if userResource.maxUserConnections >= 0 {
1981+
// need `CREATE USER` privilege for the operation of modifying max_user_connections.
1982+
if !hasCreateUserPriv {
1983+
return plannererrors.ErrSpecificAccessDenied.GenWithStackByArgs("CREATE USER")
1984+
}
1985+
fields = append(fields, alterField{"max_user_connections=%?", userResource.maxUserConnections})
1986+
}
1987+
19271988
var newAttributes []string
19281989
if s.CommentOrAttributeOption != nil {
19291990
if s.CommentOrAttributeOption.Type == ast.UserCommentType {

pkg/executor/test/simpletest/BUILD.bazel

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ go_test(
99
],
1010
flaky = True,
1111
race = "on",
12-
shard_count = 11,
12+
shard_count = 12,
1313
deps = [
1414
"//pkg/config",
1515
"//pkg/parser/ast",

pkg/executor/test/simpletest/simple_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,67 @@ func TestRole(t *testing.T) {
230230
tk.MustExec("SET ROLE NONE")
231231
}
232232

233+
func TestMaxUserConnections(t *testing.T) {
234+
store := testkit.CreateMockStore(t)
235+
tk := testkit.NewTestKit(t, store)
236+
237+
// test global variables max_user_connections.
238+
result := tk.MustQuery(`show variables like 'max_user_connections'`)
239+
result.Check(testkit.Rows("max_user_connections 0"))
240+
tk.MustExec(`set global max_user_connections = 3;`)
241+
tk.MustQuery(`show variables like 'max_user_connections'`).Check(testkit.Rows("max_user_connections 3"))
242+
// if the value < 0, set 0 to max_user_connections.
243+
tk.MustExec(`set global max_user_connections = -1;`)
244+
tk.MustQuery(`show variables like 'max_user_connections'`).Check(testkit.Rows("max_user_connections 0"))
245+
// if the value > 100000, set 100000 to max_user_connections.
246+
tk.MustExec(`set global max_user_connections = 100001;`)
247+
tk.MustQuery(`show variables like 'max_user_connections'`).Check(testkit.Rows("max_user_connections 100000"))
248+
tk.MustExec(`set global max_user_connections = 0;`)
249+
tk.MustQuery(`show variables like 'max_user_connections'`).Check(testkit.Rows("max_user_connections 0"))
250+
251+
// create user with the default max_user_connections 0
252+
createUserSQL := `CREATE USER 'test'@'localhost';`
253+
tk.MustExec(createUserSQL)
254+
result = tk.MustQuery(`select user, max_user_connections from mysql.user`)
255+
result.Check(testkit.Rows("root 0", "test 0"))
256+
257+
// create user with max_user_connections 3
258+
createUserSQL = `CREATE USER 'test1'@'localhost' WITH MAX_USER_CONNECTIONS 3;`
259+
tk.MustExec(createUserSQL)
260+
result = tk.MustQuery(`select user, max_user_connections from mysql.user WHERE User="test1"`)
261+
result.Check(testkit.Rows("test1 3"))
262+
263+
// test alter user with MAX_USER_CONNECTIONS
264+
alterUserSQL := `ALTER USER 'test1'@'localhost' WITH MAX_USER_CONNECTIONS 4;`
265+
tk.MustExec(alterUserSQL)
266+
result = tk.MustQuery(`select user, max_user_connections from mysql.user WHERE User="test1"`)
267+
result.Check(testkit.Rows("test1 4"))
268+
alterUserSQL = `ALTER USER 'test1'@'localhost' WITH MAX_USER_CONNECTIONS -2;`
269+
_, err := tk.Exec(alterUserSQL)
270+
require.Error(t, err)
271+
require.Equal(t, err.Error(), "[parser:1064]You have an error in your SQL syntax; check the manual that corresponds to your TiDB version for the right syntax to use line 1 column 58 near \"-2;\" ")
272+
alterUserSQL = `ALTER USER 'test1'@'localhost' WITH MAX_USER_CONNECTIONS 0;`
273+
tk.MustExec(alterUserSQL)
274+
result = tk.MustQuery(`select user, max_user_connections from mysql.user WHERE User="test1"`)
275+
result.Check(testkit.Rows("test1 0"))
276+
277+
// grant the privilege of 'create user' to 'test1'@'localhost'
278+
tkTest1 := testkit.NewTestKit(t, store)
279+
require.NoError(t, tkTest1.Session().Auth(&auth.UserIdentity{Username: "test1", Hostname: "localhost"}, nil, nil, nil))
280+
_, err = tkTest1.Exec(`ALTER USER 'test1'@'localhost' WITH MAX_USER_CONNECTIONS 2`)
281+
require.Error(t, err)
282+
require.EqualError(t, err, "[planner:1227]Access denied; you need (at least one of) the CREATE USER privilege(s) for this operation")
283+
tk.MustExec(`GRANT CREATE USER ON *.* TO 'test1'@'localhost'`)
284+
_, err = tkTest1.Exec(`ALTER USER 'test1'@'localhost' WITH MAX_USER_CONNECTIONS 2`)
285+
require.Nil(t, err)
286+
287+
// revert the privilege of 'create user' for 'test1'@'localhost'
288+
tk.MustExec(`REVOKE CREATE USER ON *.* FROM 'test1'@'localhost'`)
289+
_, err = tkTest1.Exec(`ALTER USER 'test1'@'localhost' WITH MAX_USER_CONNECTIONS 2`)
290+
require.Error(t, err)
291+
require.EqualError(t, err, "[planner:1227]Access denied; you need (at least one of) the CREATE USER privilege(s) for this operation")
292+
}
293+
233294
func TestUser(t *testing.T) {
234295
store := testkit.CreateMockStore(t)
235296
tk := testkit.NewTestKit(t, store)

pkg/parser/parser.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -23323,8 +23323,19 @@ yynewstate:
2332323323
case 2519:
2332423324
{
2332523325
parser.yyVAL.item = yyS[yypt-0].item
23326-
yylex.AppendError(yylex.Errorf("TiDB does not support WITH ConnectionOptions now, they would be parsed but ignored."))
23327-
parser.lastErrorAsWarn()
23326+
needWarning := false
23327+
for _, option := range yyS[yypt-0].item.([]*ast.ResourceOption) {
23328+
switch option.Type {
23329+
case ast.MaxUserConnections:
23330+
// do nothing.
23331+
default:
23332+
needWarning = true
23333+
}
23334+
}
23335+
if needWarning {
23336+
yylex.AppendError(yylex.Errorf("TiDB does not support WITH ConnectionOptions but MAX_USER_CONNECTIONS now, they would be parsed but ignored."))
23337+
parser.lastErrorAsWarn()
23338+
}
2332823339
}
2332923340
case 2520:
2333023341
{

pkg/parser/parser.y

+13-2
Original file line numberDiff line numberDiff line change
@@ -13797,8 +13797,19 @@ ConnectionOptions:
1379713797
| "WITH" ConnectionOptionList
1379813798
{
1379913799
$$ = $2
13800-
yylex.AppendError(yylex.Errorf("TiDB does not support WITH ConnectionOptions now, they would be parsed but ignored."))
13801-
parser.lastErrorAsWarn()
13800+
needWarning := false
13801+
for _, option := range $2.([]*ast.ResourceOption) {
13802+
switch option.Type {
13803+
case ast.MaxUserConnections:
13804+
// do nothing.
13805+
default:
13806+
needWarning = true
13807+
}
13808+
}
13809+
if needWarning {
13810+
yylex.AppendError(yylex.Errorf("TiDB does not support WITH ConnectionOptions but MAX_USER_CONNECTIONS now, they would be parsed but ignored."))
13811+
parser.lastErrorAsWarn()
13812+
}
1380213813
}
1380313814

1380413815
ConnectionOptionList:

pkg/privilege/privilege.go

+3
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ type Manager interface {
116116

117117
// GetAuthPluginForConnection gets the authentication plugin used in connection establishment.
118118
GetAuthPluginForConnection(ctx context.Context, user, host string) (string, error)
119+
120+
//GetUserResources gets the max user connections for the account identified by the user and host
121+
GetUserResources(user, host string) (int64, error)
119122
}
120123

121124
const key keyType = 0

pkg/privilege/privileges/cache.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const (
7171
References_priv,Alter_priv,Execute_priv,Index_priv,Create_view_priv,Show_view_priv,
7272
Create_role_priv,Drop_role_priv,Create_tmp_table_priv,Lock_tables_priv,Create_routine_priv,
7373
Alter_routine_priv,Event_priv,Shutdown_priv,Reload_priv,File_priv,Config_priv,Repl_client_priv,Repl_slave_priv,
74-
Account_locked,Plugin,Token_issuer,User_attributes,password_expired,password_last_changed,password_lifetime FROM mysql.user`
74+
Account_locked,Plugin,Token_issuer,User_attributes,password_expired,password_last_changed,password_lifetime,max_user_connections FROM mysql.user`
7575
sqlLoadGlobalGrantsTable = `SELECT HIGH_PRIORITY Host,User,Priv,With_Grant_Option FROM mysql.global_grants`
7676
)
7777

@@ -121,6 +121,7 @@ type UserRecord struct {
121121
PasswordExpired bool
122122
PasswordLastChanged time.Time
123123
PasswordLifeTime int64
124+
MaxUserConnections int64
124125
ResourceGroup string
125126
}
126127

@@ -1044,6 +1045,8 @@ func (p *MySQLPrivilege) decodeUserTableRow(row chunk.Row, fs []*resolve.ResultF
10441045
continue
10451046
}
10461047
value.PasswordLifeTime = row.GetInt64(i)
1048+
case f.ColumnAsName.L == "max_user_connections":
1049+
value.MaxUserConnections = row.GetInt64(i)
10471050
case f.Column.GetType() == mysql.TypeEnum:
10481051
if row.GetEnum(i).String() != "Y" {
10491052
continue

pkg/privilege/privileges/privileges.go

+16
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,22 @@ func (p *UserPrivileges) isValidHash(record *UserRecord) bool {
320320
return false
321321
}
322322

323+
// GetUserResources gets the maximum number of connections for the current user
324+
func (p *UserPrivileges) GetUserResources(user, host string) (int64, error) {
325+
terror.Log(p.Handle.ensureActiveUser(context.Background(), user))
326+
mysqlPriv := p.Handle.Get()
327+
record := mysqlPriv.connectionVerification(user, host)
328+
if record == nil {
329+
logutil.BgLogger().Error("get user privilege record fail",
330+
zap.String("user", user), zap.String("host", host))
331+
return 0, errors.New("Failed to get user record")
332+
}
333+
if p.isValidHash(record) {
334+
return record.MaxUserConnections, nil
335+
}
336+
return 0, errors.New("Failed to get max user connections")
337+
}
338+
323339
// GetAuthPluginForConnection gets the authentication plugin used in connection establishment.
324340
func (p *UserPrivileges) GetAuthPluginForConnection(ctx context.Context, user, host string) (string, error) {
325341
if SkipWithGrant {

pkg/server/BUILD.bazel

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ go_library(
1616
"rpc_server.go",
1717
"server.go",
1818
"stat.go",
19+
"user_connections.go",
1920
],
2021
importpath = "github.com/pingcap/tidb/pkg/server",
2122
visibility = ["//visibility:public"],
@@ -149,6 +150,7 @@ go_test(
149150
"stat_test.go",
150151
"tidb_library_test.go",
151152
"tidb_test.go",
153+
"user_connections_test.go",
152154
],
153155
data = glob(["testdata/**"]),
154156
embed = [":server"],

0 commit comments

Comments
 (0)