privilege: add restricted read only (#25340)
This commit is contained in:
56
docs/design/2021-06-23-restricted-read-only.md
Normal file
56
docs/design/2021-06-23-restricted-read-only.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Technical Design Document for Restrcted Read Only
|
||||
|
||||
> Author: [huzhifeng@pingcap.com](mailto:huzhifeng@pingcap.com)
|
||||
This document is open to the web, and it will appear as a design doc PR to the TiDB repository.
|
||||
|
||||
# Introduction
|
||||
|
||||
In this document, we introduced the `TIDB_RESTRICTED_READ_ONLY` global variable. Turning it on will make the cluster read-only eventually for all users, including users with SUPER or CONNECTION_ADMIN privilege.
|
||||
|
||||
# Motivation or Background
|
||||
|
||||
See the [original specification](https://docs.google.com/document/d/1LtmW4fIohTjPcupD9smQGQpN2B2tRSFNyH8PFbg-L2c/edit).
|
||||
|
||||
# **Detailed Design**
|
||||
|
||||
## read-only handling
|
||||
|
||||
Currently, TiDB supports `read_only` and `super_read_only` global variables with no actual operation. Since TIDB_RESTRICTED_READ_ONLY does not comply with the behavior of MySQL read-only and super-read-only (which will block if there is table locked or ongoing committing of a transaction), we will not build TIDB_RESTRICTED_READ_ONLY upon read-only and super-read-only. These two variables will be remained untouched.
|
||||
|
||||
We will create a new global variable TIDB_RESTRICTED_READ_ONLY, and turning it to on and off will only be allowed in SEM mode, and only users with RESTRICTED_VARIABLES_ADMIN will be allowed to modify this variable.
|
||||
|
||||
To allow replication service to write, we introduced a new dynamic privilege level called `RESTRICTED_REPLICA_WRITER_ADMIN`, user with this privilege will be ignored for read-only checks.
|
||||
|
||||
Upon the change of the variable on a TiDB sever, the change will be broadcasted to all other TiDB servers through PD. Normally, the other TiDB servers will receive the update immediately, however under certain circumstances (such as TiDB server lose connection to PD), the lag can be up to 30 seconds.
|
||||
|
||||
### How to restrict SQLs
|
||||
|
||||
If read-only is turned on, we will reject SQLs that might change data during planning. The rules are:
|
||||
|
||||
- we won't restrict internal SQLs
|
||||
- we rely on an allow list to determine whether SQL is allowed to execute
|
||||
- we would allow set variables (otherwise can't unset them)
|
||||
- we would allow `analyze table`
|
||||
- we allow `show`
|
||||
- we allow create and drop SQL bindings
|
||||
- we allow prepare SQLs
|
||||
- we allow `begin` and `rollback`
|
||||
- we only allow `commit` if there is no change in the transaction, otherwise the transaction will abort
|
||||
- finally, we resort to `planner/optimizer.go:IsReadOnly` for testing if the query is read-only for cases like explain, prepare and execute, etc.
|
||||
|
||||
## Privilege management
|
||||
|
||||
- add `tidb_restricted_read_only` to hidden variables, i.e., only users with `RESTRICTED_VARIABLES_ADMIN` privilege can modify it.
|
||||
- alternative: add `RESTRICTED_READ_ONLY_ADMIN` privilege, only user with this privilege can change the setting, and user with this privilege can write to the cluster without being affected by the read-only setting. However it might be redundant for introducing a new privilege level.
|
||||
- add `RESTRICTED_REPLICA_WRITER_ADMIN` to allow replication writer to surpass read-only checks. Note this privilege check is required even if SEM is not enabled, i.e., SUPER user still needs to be explicitly granted this privilege to be able to write.
|
||||
|
||||
## Difference with MySQL read-only
|
||||
|
||||
MySQL has support for read-only and super-read-only, which is currently not supported in TiDB. Restricted read only shares similar functionality as them, but differs in:
|
||||
|
||||
* When turning on read-only or super-read-only, MySQL might [fail or block when enabling read-only under some circumstances](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_read_only). However turning on TIDB_RESTRICTED_READ_ONLY will return success immediately. Some other TiDB servers might not yet received the update of this variable, and will remain in updatable status until eventually the change on the global variable is broadcasted to all TiDB servers in the cluster.
|
||||
|
||||
## Alternative
|
||||
|
||||
- we will implement read-only and super-read-only that complies with MySQL's, and build TIDB_RESTRICED_READ_ONLY upon them, but we only need eventual read-only for TIDB_RESTRICTED_READ_ONLY. It is more relaxed, and it should always return success.
|
||||
- we support read-only and super-read-only in a similar manner as TIDB_RESTRICED_READ_ONLY, this however will make its behavior incompatible with MySQL.
|
||||
@ -199,6 +199,8 @@ func checkEnableServerGlobalVar(name, sVal string) {
|
||||
break
|
||||
}
|
||||
variable.TopSQLVariable.ReportIntervalSeconds.Store(val)
|
||||
case variable.TiDBRestrictedReadOnly:
|
||||
variable.RestrictedReadOnly.Store(variable.TiDBOptOn(sVal))
|
||||
}
|
||||
if err != nil {
|
||||
logutil.BgLogger().Error(fmt.Sprintf("load global variable %s error", name), zap.Error(err))
|
||||
|
||||
@ -1021,6 +1021,11 @@ error = '''
|
||||
Internal : %s
|
||||
'''
|
||||
|
||||
["planner:1836"]
|
||||
error = '''
|
||||
Running in read-only mode
|
||||
'''
|
||||
|
||||
["planner:3029"]
|
||||
error = '''
|
||||
Expression #%d of ORDER BY contains aggregate function and applies to the result of a non-aggregated query
|
||||
|
||||
@ -358,6 +358,7 @@ func (s *testSuiteP1) TestShow(c *C) {
|
||||
"RESTRICTED_VARIABLES_ADMIN Server Admin ",
|
||||
"RESTRICTED_USER_ADMIN Server Admin ",
|
||||
"RESTRICTED_CONNECTION_ADMIN Server Admin ",
|
||||
"RESTRICTED_REPLICA_WRITER_ADMIN Server Admin ",
|
||||
))
|
||||
c.Assert(len(tk.MustQuery("show table status").Rows()), Equals, 1)
|
||||
}
|
||||
|
||||
@ -93,6 +93,7 @@ var (
|
||||
ErrCTERecursiveForbidsAggregation = dbterror.ClassOptimizer.NewStd(mysql.ErrCTERecursiveForbidsAggregation)
|
||||
ErrCTERecursiveForbiddenJoinOrder = dbterror.ClassOptimizer.NewStd(mysql.ErrCTERecursiveForbiddenJoinOrder)
|
||||
ErrInvalidRequiresSingleReference = dbterror.ClassOptimizer.NewStd(mysql.ErrInvalidRequiresSingleReference)
|
||||
ErrSQLInReadOnlyMode = dbterror.ClassOptimizer.NewStd(mysql.ErrReadOnlyMode)
|
||||
// Since we cannot know if user logged in with a password, use message of ErrAccessDeniedNoPassword instead
|
||||
ErrAccessDenied = dbterror.ClassOptimizer.NewStdErr(mysql.ErrAccessDenied, mysql.MySQLErrName[mysql.ErrAccessDeniedNoPassword])
|
||||
ErrBadNull = dbterror.ClassOptimizer.NewStd(mysql.ErrBadNull)
|
||||
|
||||
@ -221,6 +221,46 @@ func Optimize(ctx context.Context, sctx sessionctx.Context, node ast.Node, is in
|
||||
return bestPlan, names, nil
|
||||
}
|
||||
|
||||
func allowInReadOnlyMode(sctx sessionctx.Context, node ast.Node) (bool, error) {
|
||||
pm := privilege.GetPrivilegeManager(sctx)
|
||||
if pm == nil {
|
||||
return true, nil
|
||||
}
|
||||
roles := sctx.GetSessionVars().ActiveRoles
|
||||
// allow replication thread
|
||||
// NOTE: it is required, whether SEM is enabled or not, only user with explicit RESTRICTED_REPLICA_WRITER_ADMIN granted can ignore the restriction, so we need to surpass the case that if SEM is not enabled, SUPER will has all privileges
|
||||
if pm.HasExplicitlyGrantedDynamicPrivilege(roles, "RESTRICTED_REPLICA_WRITER_ADMIN", false) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
switch node.(type) {
|
||||
// allow change variables (otherwise can't unset read-only mode)
|
||||
case *ast.SetStmt,
|
||||
// allow analyze table
|
||||
*ast.AnalyzeTableStmt,
|
||||
*ast.UseStmt,
|
||||
*ast.ShowStmt,
|
||||
*ast.CreateBindingStmt,
|
||||
*ast.DropBindingStmt,
|
||||
*ast.PrepareStmt,
|
||||
*ast.BeginStmt,
|
||||
*ast.RollbackStmt:
|
||||
return true, nil
|
||||
case *ast.CommitStmt:
|
||||
txn, err := sctx.Txn(true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !txn.IsReadOnly() {
|
||||
return false, txn.Rollback()
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
vars := sctx.GetSessionVars()
|
||||
return IsReadOnly(node, vars), nil
|
||||
}
|
||||
|
||||
var planBuilderPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return plannercore.NewPlanBuilder()
|
||||
@ -275,6 +315,16 @@ func optimize(ctx context.Context, sctx sessionctx.Context, node ast.Node, is in
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
if !sctx.GetSessionVars().InRestrictedSQL && variable.RestrictedReadOnly.Load() {
|
||||
allowed, err := allowInReadOnlyMode(sctx, node)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
if !allowed {
|
||||
return nil, nil, 0, errors.Trace(core.ErrSQLInReadOnlyMode)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the execute statement.
|
||||
if execPlan, ok := p.(*plannercore.Execute); ok {
|
||||
err := execPlan.OptimizePreparedPlan(ctx, sctx, is)
|
||||
|
||||
@ -46,6 +46,10 @@ type Manager interface {
|
||||
// RequestVerificationWithUser verifies specific user privilege for the request.
|
||||
RequestVerificationWithUser(db, table, column string, priv mysql.PrivilegeType, user *auth.UserIdentity) bool
|
||||
|
||||
// HasExplicitlyGrantedDynamicPrivilege verifies is a user has a dynamic privilege granted
|
||||
// without using the SUPER privilege as a fallback.
|
||||
HasExplicitlyGrantedDynamicPrivilege(activeRoles []*auth.RoleIdentity, privName string, grantable bool) bool
|
||||
|
||||
// RequestDynamicVerification verifies user privilege for a DYNAMIC privilege.
|
||||
// Dynamic privileges are only assignable globally, and have their own grantable attribute.
|
||||
RequestDynamicVerification(activeRoles []*auth.RoleIdentity, privName string, grantable bool) bool
|
||||
|
||||
@ -963,8 +963,9 @@ func (p *MySQLPrivilege) matchColumns(user, host, db, table, column string) *col
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestDynamicVerification checks all roles for a specific DYNAMIC privilege.
|
||||
func (p *MySQLPrivilege) RequestDynamicVerification(activeRoles []*auth.RoleIdentity, user, host, privName string, withGrant bool) bool {
|
||||
// HasExplicitlyGrantedDynamicPrivilege checks if a user has a DYNAMIC privilege
|
||||
// without accepting SUPER privilege as a fallback.
|
||||
func (p *MySQLPrivilege) HasExplicitlyGrantedDynamicPrivilege(activeRoles []*auth.RoleIdentity, user, host, privName string, withGrant bool) bool {
|
||||
privName = strings.ToUpper(privName)
|
||||
roleList := p.FindAllRole(activeRoles)
|
||||
roleList = append(roleList, &auth.RoleIdentity{Username: user, Hostname: host})
|
||||
@ -984,6 +985,15 @@ func (p *MySQLPrivilege) RequestDynamicVerification(activeRoles []*auth.RoleIden
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RequestDynamicVerification checks all roles for a specific DYNAMIC privilege.
|
||||
func (p *MySQLPrivilege) RequestDynamicVerification(activeRoles []*auth.RoleIdentity, user, host, privName string, withGrant bool) bool {
|
||||
privName = strings.ToUpper(privName)
|
||||
if p.HasExplicitlyGrantedDynamicPrivilege(activeRoles, user, host, privName, withGrant) {
|
||||
return true
|
||||
}
|
||||
// If SEM is enabled, and the privilege is of type restricted, do not fall through
|
||||
// To using SUPER as a replacement privilege.
|
||||
if sem.IsEnabled() && sem.IsRestrictedPrivilege(privName) {
|
||||
|
||||
@ -45,11 +45,12 @@ var dynamicPrivs = []string{
|
||||
"SYSTEM_VARIABLES_ADMIN",
|
||||
"ROLE_ADMIN",
|
||||
"CONNECTION_ADMIN",
|
||||
"RESTRICTED_TABLES_ADMIN", // Can see system tables when SEM is enabled
|
||||
"RESTRICTED_STATUS_ADMIN", // Can see all status vars when SEM is enabled.
|
||||
"RESTRICTED_VARIABLES_ADMIN", // Can see all variables when SEM is enabled
|
||||
"RESTRICTED_USER_ADMIN", // User can not have their access revoked by SUPER users.
|
||||
"RESTRICTED_CONNECTION_ADMIN", // Can not be killed by PROCESS/CONNECTION_ADMIN privilege
|
||||
"RESTRICTED_TABLES_ADMIN", // Can see system tables when SEM is enabled
|
||||
"RESTRICTED_STATUS_ADMIN", // Can see all status vars when SEM is enabled.
|
||||
"RESTRICTED_VARIABLES_ADMIN", // Can see all variables when SEM is enabled
|
||||
"RESTRICTED_USER_ADMIN", // User can not have their access revoked by SUPER users.
|
||||
"RESTRICTED_CONNECTION_ADMIN", // Can not be killed by PROCESS/CONNECTION_ADMIN privilege
|
||||
"RESTRICTED_REPLICA_WRITER_ADMIN", // Can write to the sever even when tidb_restriced_read_only is turned on.
|
||||
}
|
||||
var dynamicPrivLock sync.Mutex
|
||||
|
||||
@ -76,6 +77,20 @@ func (p *UserPrivileges) RequestDynamicVerificationWithUser(privName string, gra
|
||||
return mysqlPriv.RequestDynamicVerification(roles, user.Username, user.Hostname, privName, grantable)
|
||||
}
|
||||
|
||||
// HasExplicitlyGrantedDynamicPrivilege checks if a user has a DYNAMIC privilege
|
||||
// without accepting SUPER privilege as a fallback.
|
||||
func (p *UserPrivileges) HasExplicitlyGrantedDynamicPrivilege(activeRoles []*auth.RoleIdentity, privName string, grantable bool) bool {
|
||||
if SkipWithGrant {
|
||||
return true
|
||||
}
|
||||
if p.user == "" && p.host == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
mysqlPriv := p.Handle.Get()
|
||||
return mysqlPriv.HasExplicitlyGrantedDynamicPrivilege(activeRoles, p.user, p.host, privName, grantable)
|
||||
}
|
||||
|
||||
// RequestDynamicVerification implements the Manager interface.
|
||||
func (p *UserPrivileges) RequestDynamicVerification(activeRoles []*auth.RoleIdentity, privName string, grantable bool) bool {
|
||||
if SkipWithGrant {
|
||||
|
||||
@ -1619,7 +1619,8 @@ var defaultSysVars = []*SysVar{
|
||||
errors.RedactLogEnabled.Store(s.EnableRedactLog)
|
||||
return nil
|
||||
}},
|
||||
{Scope: ScopeGlobal | ScopeSession, Name: TiDBShardAllocateStep, Value: strconv.Itoa(DefTiDBShardAllocateStep), Type: TypeInt, MinValue: 1, MaxValue: math.MaxInt64, AutoConvertOutOfRange: true, SetSession: func(s *SessionVars, val string) error {
|
||||
{Scope: ScopeGlobal, Name: TiDBRestrictedReadOnly, Value: BoolToOnOff(DefTiDBRestrictedReadOnly), Type: TypeBool},
|
||||
{Scope: ScopeGlobal | ScopeSession, Name: TiDBShardAllocateStep, Value: strconv.Itoa(DefTiDBShardAllocateStep), Type: TypeInt, MinValue: 1, MaxValue: uint64(math.MaxInt64), AutoConvertOutOfRange: true, SetSession: func(s *SessionVars, val string) error {
|
||||
s.ShardAllocateStep = tidbOptInt64(val, DefTiDBShardAllocateStep)
|
||||
return nil
|
||||
}},
|
||||
|
||||
@ -504,6 +504,9 @@ const (
|
||||
// TiDBRedactLog indicates that whether redact log.
|
||||
TiDBRedactLog = "tidb_redact_log"
|
||||
|
||||
// TiDBRestrictedReadOnly is meant for the cloud admin to toggle the cluster read only
|
||||
TiDBRestrictedReadOnly = "tidb_restricted_read_only"
|
||||
|
||||
// TiDBShardAllocateStep indicates the max size of continuous rowid shard in one transaction.
|
||||
TiDBShardAllocateStep = "tidb_shard_allocate_step"
|
||||
// TiDBEnableTelemetry indicates that whether usage data report to PingCAP is enabled.
|
||||
@ -698,6 +701,7 @@ const (
|
||||
DefTiDBAllowAutoRandExplicitInsert = false
|
||||
DefTiDBEnableClusteredIndex = ClusteredIndexDefModeIntOnly
|
||||
DefTiDBRedactLog = false
|
||||
DefTiDBRestrictedReadOnly = false
|
||||
DefTiDBShardAllocateStep = math.MaxInt64
|
||||
DefTiDBEnableTelemetry = true
|
||||
DefTiDBEnableParallelApply = false
|
||||
@ -754,7 +758,8 @@ var (
|
||||
MaxCollect: atomic.NewInt64(DefTiDBTopSQLMaxCollect),
|
||||
ReportIntervalSeconds: atomic.NewInt64(DefTiDBTopSQLReportIntervalSeconds),
|
||||
}
|
||||
EnableLocalTxn = atomic.NewBool(DefTiDBEnableLocalTxn)
|
||||
EnableLocalTxn = atomic.NewBool(DefTiDBEnableLocalTxn)
|
||||
RestrictedReadOnly = atomic.NewBool(DefTiDBRestrictedReadOnly)
|
||||
)
|
||||
|
||||
// TopSQL is the variable for control top sql feature.
|
||||
|
||||
15
tests/readonlytest/README.md
Normal file
15
tests/readonlytest/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# E2E test for TiDB restricted read only
|
||||
|
||||
The test is not yet automated, so you need to set up an environment manually to test it.
|
||||
|
||||
Create a TiDB cluster with 2 TiDB servers on localhost. One server listen on port 4001 and another on 4002, then execute `go test` in this folder to run the test.
|
||||
|
||||
You are expected to see 2 tests passed.
|
||||
|
||||
```
|
||||
$ go test
|
||||
OK: 2 passed
|
||||
PASS
|
||||
ok github.com/pingcap/tidb/tests/readonlytest 2.150s
|
||||
```
|
||||
|
||||
8
tests/readonlytest/go.mod
Normal file
8
tests/readonlytest/go.mod
Normal file
@ -0,0 +1,8 @@
|
||||
module github.com/pingcap/tidb/tests/readonlytest
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/pingcap/check v0.0.0-20200212061837-5e12011dc712
|
||||
)
|
||||
68
tests/readonlytest/go.sum
Normal file
68
tests/readonlytest/go.sum
Normal file
@ -0,0 +1,68 @@
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ=
|
||||
github.com/pingcap/check v0.0.0-20200212061837-5e12011dc712 h1:R8gStypOBmpnHEx1qi//SaqxJVI4inOqljg/Aj5/390=
|
||||
github.com/pingcap/check v0.0.0-20200212061837-5e12011dc712/go.mod h1:PYMCGwN0JHjoqGr3HrZoD+b8Tgx8bKnArhSq8YVzUMc=
|
||||
github.com/pingcap/errors v0.11.0 h1:DCJQB8jrHbQ1VVlMFIrbj2ApScNNotVmkSNplu2yUt4=
|
||||
github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9 h1:AJD9pZYm72vMgPcQDww9rkZ1DnWfl0pXV3BOWlkYIjA=
|
||||
github.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E=
|
||||
go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.12.0 h1:dySoUQPFBGj6xwjmBzageVL8jGi8uxc6bEmJQjA06bw=
|
||||
go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191107010934-f79515f33823 h1:akkRBeitX2EZP59KdtKw310CI4WGPCNPyrLbE7WZA8Y=
|
||||
golang.org/x/tools v0.0.0-20191107010934-f79515f33823/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
170
tests/readonlytest/readonly_test.go
Normal file
170
tests/readonlytest/readonly_test.go
Normal file
@ -0,0 +1,170 @@
|
||||
package readonlytest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
. "github.com/pingcap/check"
|
||||
)
|
||||
|
||||
var (
|
||||
logLevel = flag.String("L", "info", "test log level")
|
||||
serverLogLevel = flag.String("server_log_level", "info", "server log level")
|
||||
tmpPath = flag.String("tmp", "/tmp/tidb_globalkilltest", "temporary files path")
|
||||
|
||||
tidbBinaryPath = flag.String("s", "bin/globalkilltest_tidb-server", "tidb server binary path")
|
||||
pdBinaryPath = flag.String("p", "bin/pd-server", "pd server binary path")
|
||||
tikvBinaryPath = flag.String("k", "bin/tikv-server", "tikv server binary path")
|
||||
|
||||
tidbRootPassword = flag.String("passwd", "", "tidb root password")
|
||||
tidbStartPort = flag.Int("tidb_start_port", 4000, "first tidb server listening port")
|
||||
tidbStatusPort = flag.Int("tidb_status_port", 8000, "first tidb server status port")
|
||||
|
||||
ReadOnlyErrMsg = "Error 1836: Running in read-only mode"
|
||||
)
|
||||
|
||||
func TestReadOnly(t *testing.T) {
|
||||
TestingT(t)
|
||||
}
|
||||
|
||||
var _ = Suite(&TestReadOnlySuit{})
|
||||
|
||||
type TestReadOnlySuit struct {
|
||||
db *sql.DB
|
||||
udb *sql.DB
|
||||
rdb *sql.DB
|
||||
}
|
||||
|
||||
func checkVariable(c *C, db *sql.DB, on bool) {
|
||||
var name, status string
|
||||
rs, err := db.Query("show variables like 'tidb_restricted_read_only'")
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(rs.Next(), IsTrue)
|
||||
|
||||
err = rs.Scan(&name, &status)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(name, Equals, "tidb_restricted_read_only")
|
||||
if on {
|
||||
c.Assert(status, Equals, "ON")
|
||||
} else {
|
||||
c.Assert(status, Equals, "OFF")
|
||||
}
|
||||
}
|
||||
|
||||
func setReadOnly(c *C, db *sql.DB, status int) {
|
||||
_, err := db.Exec(fmt.Sprintf("set global tidb_restricted_read_only=%d", status))
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *TestReadOnlySuit) SetUpSuite(c *C) {
|
||||
var err error
|
||||
s.db, err = sql.Open("mysql", fmt.Sprintf("root:%s@(%s:%d)/test", *tidbRootPassword, "127.0.0.1", *tidbStartPort+1))
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
setReadOnly(c, s.db, 0)
|
||||
|
||||
_, err = s.db.Exec("drop user if exists 'u1'@'%'")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.db.Exec("create user 'u1'@'%' identified by 'password'")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.db.Exec("grant all privileges on test.* to 'u1'@'%'")
|
||||
c.Assert(err, IsNil)
|
||||
s.udb, err = sql.Open("mysql", fmt.Sprintf("u1:password@(%s:%d)/test", "127.0.0.1", *tidbStartPort+2))
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.db.Exec("drop user if exists 'r1'@'%'")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.db.Exec("create user 'r1'@'%' identified by 'password'")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.db.Exec("grant all privileges on test.* to 'r1'@'%'")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.db.Exec("grant RESTRICTED_REPLICA_WRITER_ADMIN on *.* to 'r1'@'%'")
|
||||
c.Assert(err, IsNil)
|
||||
s.rdb, err = sql.Open("mysql", fmt.Sprintf("r1:password@(%s:%d)/test", "127.0.0.1", *tidbStartPort+2))
|
||||
}
|
||||
|
||||
func (s *TestReadOnlySuit) TestRestriction(c *C) {
|
||||
_, err := s.db.Exec("set global tidb_restricted_read_only=1")
|
||||
c.Assert(err, IsNil)
|
||||
time.Sleep(1)
|
||||
checkVariable(c, s.udb, true)
|
||||
|
||||
_, err = s.udb.Exec("create table t(a int)")
|
||||
c.Assert(err, NotNil)
|
||||
c.Assert(err.Error(), Equals, ReadOnlyErrMsg)
|
||||
}
|
||||
|
||||
func (s *TestReadOnlySuit) TestRestrictionWithConnectionPool(c *C) {
|
||||
_, err := s.db.Exec("set global tidb_restricted_read_only=0")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.db.Exec("drop table if exists t")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.db.Exec("create table t (a int)")
|
||||
c.Assert(err, IsNil)
|
||||
time.Sleep(1)
|
||||
checkVariable(c, s.udb, false)
|
||||
|
||||
conn, err := s.udb.Conn(context.Background())
|
||||
c.Assert(err, IsNil)
|
||||
done := make(chan bool)
|
||||
go func(conn *sql.Conn, done chan bool) {
|
||||
ticker := time.NewTicker(50 * time.Millisecond)
|
||||
for {
|
||||
t := <-ticker.C
|
||||
_, err := conn.ExecContext(context.Background(), fmt.Sprintf("insert into t values (%d)", t.Nanosecond()))
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if err.Error() == ReadOnlyErrMsg {
|
||||
done <- true
|
||||
} else {
|
||||
done <- false
|
||||
}
|
||||
}
|
||||
}(conn, done)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
timer := time.NewTimer(10 * time.Second)
|
||||
_, err = s.db.Exec("set global tidb_restricted_read_only=1")
|
||||
c.Assert(err, IsNil)
|
||||
select {
|
||||
case <-timer.C:
|
||||
c.Fail()
|
||||
case success := <-done:
|
||||
c.Assert(success, IsTrue)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestReadOnlySuit) TestReplicationWriter(c *C) {
|
||||
_, err := s.db.Exec("set global tidb_restricted_read_only=0")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.db.Exec("drop table if exists t")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = s.db.Exec("create table t (a int)")
|
||||
c.Assert(err, IsNil)
|
||||
time.Sleep(1)
|
||||
checkVariable(c, s.udb, false)
|
||||
|
||||
conn, err := s.rdb.Conn(context.Background())
|
||||
c.Assert(err, IsNil)
|
||||
go func(conn *sql.Conn) {
|
||||
ticker := time.NewTicker(50 * time.Millisecond)
|
||||
for {
|
||||
t := <-ticker.C
|
||||
_, err := conn.ExecContext(context.Background(), fmt.Sprintf("insert into t values (%d)", t.Nanosecond()))
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
}(conn)
|
||||
time.Sleep(1 * time.Second)
|
||||
timer := time.NewTimer(3 * time.Second)
|
||||
_, err = s.db.Exec("set global tidb_restricted_read_only=1")
|
||||
c.Assert(err, IsNil)
|
||||
// SUPER user can't write
|
||||
_, err = s.db.Exec("insert into t values (1)")
|
||||
c.Assert(err.Error(), Equals, ReadOnlyErrMsg)
|
||||
<-timer.C
|
||||
}
|
||||
@ -153,6 +153,7 @@ func IsInvisibleSysVar(varNameInLower string) bool {
|
||||
variable.TiDBEnableCollectExecutionInfo,
|
||||
variable.TiDBMemoryUsageAlarmRatio,
|
||||
variable.TiDBRedactLog,
|
||||
variable.TiDBRestrictedReadOnly,
|
||||
variable.TiDBSlowLogMasking:
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user