Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ddl/ddl_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4206,6 +4206,10 @@ func (d *ddl) ExchangeTablePartition(ctx sessionctx.Context, ident ast.Ident, sp

ntMeta := nt.Meta()

if util.IsMemOrSysDB(ntSchema.Name.L) {
return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs(fmt.Sprintf("Exchange partition on system table '%s.%s'", ntSchema.Name.L, ntMeta.Name.L))
}

err = checkExchangePartition(ptMeta, ntMeta)
if err != nil {
return errors.Trace(err)
Expand Down
92 changes: 64 additions & 28 deletions planner/core/planbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4414,19 +4414,60 @@ func (b *PlanBuilder) buildDDL(ctx context.Context, node ast.DDLNode) (Plan, err
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.DropPriv, dbName,
v.Table.Name.L, "", authErr)

newDBName := spec.NewTable.Schema.L
if newDBName == "" {
newDBName = strings.ToLower(b.ctx.GetSessionVars().CurrentDB)
}
if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("CREATE", b.ctx.GetSessionVars().User.AuthUsername,
b.ctx.GetSessionVars().User.AuthHostname, spec.NewTable.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.CreatePriv, dbName,
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.CreatePriv, newDBName,
spec.NewTable.Name.L, "", authErr)

if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("INSERT", b.ctx.GetSessionVars().User.AuthUsername,
b.ctx.GetSessionVars().User.AuthHostname, spec.NewTable.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.InsertPriv, dbName,
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.InsertPriv, newDBName,
spec.NewTable.Name.L, "", authErr)

if spec.Tp == ast.AlterTableExchangePartition {
// EXCHANGE PARTITION swaps the data of a partition of the partitioned
// table with the whole non-partitioned table, mutating BOTH tables. To
// match MySQL, ALTER, INSERT, CREATE and DROP are required on both tables.
// The shared path above already covers ALTER+DROP on the partitioned
// table and CREATE+INSERT on the non-partitioned table; add the remaining
// INSERT+CREATE on the partitioned table and ALTER+DROP on the
// non-partitioned table. (RENAME TABLE must not require these extras.)
if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("INSERT", b.ctx.GetSessionVars().User.AuthUsername,
b.ctx.GetSessionVars().User.AuthHostname, v.Table.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.InsertPriv, dbName,
v.Table.Name.L, "", authErr)

if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("CREATE", b.ctx.GetSessionVars().User.AuthUsername,
b.ctx.GetSessionVars().User.AuthHostname, v.Table.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.CreatePriv, dbName,
v.Table.Name.L, "", authErr)

if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("ALTER", b.ctx.GetSessionVars().User.AuthUsername,
b.ctx.GetSessionVars().User.AuthHostname, spec.NewTable.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.AlterPriv, newDBName,
spec.NewTable.Name.L, "", authErr)

if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("DROP", b.ctx.GetSessionVars().User.AuthUsername,
b.ctx.GetSessionVars().User.AuthHostname, spec.NewTable.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.DropPriv, newDBName,
spec.NewTable.Name.L, "", authErr)
}
} else if spec.Tp == ast.AlterTableDropPartition {
if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("DROP", b.ctx.GetSessionVars().User.AuthUsername,
Expand Down Expand Up @@ -4608,33 +4649,28 @@ func (b *PlanBuilder) buildDDL(ctx context.Context, node ast.DDLNode) (Plan, err
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.DropPriv, v.Table.Schema.L,
v.Table.Name.L, "", authErr)
case *ast.RenameTableStmt:
if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("ALTER", b.ctx.GetSessionVars().User.AuthUsername,
b.ctx.GetSessionVars().User.AuthHostname, v.TableToTables[0].OldTable.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.AlterPriv, v.TableToTables[0].OldTable.Schema.L,
v.TableToTables[0].OldTable.Name.L, "", authErr)

if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("DROP", b.ctx.GetSessionVars().User.AuthUsername,
b.ctx.GetSessionVars().User.AuthHostname, v.TableToTables[0].OldTable.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.DropPriv, v.TableToTables[0].OldTable.Schema.L,
v.TableToTables[0].OldTable.Name.L, "", authErr)

if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("CREATE", b.ctx.GetSessionVars().User.AuthUsername,
b.ctx.GetSessionVars().User.AuthHostname, v.TableToTables[0].NewTable.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.CreatePriv, v.TableToTables[0].NewTable.Schema.L,
v.TableToTables[0].NewTable.Name.L, "", authErr)

if b.ctx.GetSessionVars().User != nil {
authErr = ErrTableaccessDenied.GenWithStackByArgs("INSERT", b.ctx.GetSessionVars().User.AuthUsername,
b.ctx.GetSessionVars().User.AuthHostname, v.TableToTables[0].NewTable.Name.L)
user := b.ctx.GetSessionVars().User
for _, tableToTable := range v.TableToTables {
var alterErr, dropErr, createErr, insertErr error
if user != nil {
alterErr = ErrTableaccessDenied.GenWithStackByArgs("ALTER", user.AuthUsername,
user.AuthHostname, tableToTable.OldTable.Name.L)
dropErr = ErrTableaccessDenied.GenWithStackByArgs("DROP", user.AuthUsername,
user.AuthHostname, tableToTable.OldTable.Name.L)
createErr = ErrTableaccessDenied.GenWithStackByArgs("CREATE", user.AuthUsername,
user.AuthHostname, tableToTable.NewTable.Name.L)
insertErr = ErrTableaccessDenied.GenWithStackByArgs("INSERT", user.AuthUsername,
user.AuthHostname, tableToTable.NewTable.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.AlterPriv, tableToTable.OldTable.Schema.L,
tableToTable.OldTable.Name.L, "", alterErr)
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.DropPriv, tableToTable.OldTable.Schema.L,
tableToTable.OldTable.Name.L, "", dropErr)
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.CreatePriv, tableToTable.NewTable.Schema.L,
tableToTable.NewTable.Name.L, "", createErr)
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.InsertPriv, tableToTable.NewTable.Schema.L,
tableToTable.NewTable.Name.L, "", insertErr)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.InsertPriv, v.TableToTables[0].NewTable.Schema.L,
v.TableToTables[0].NewTable.Name.L, "", authErr)
case *ast.RecoverTableStmt, *ast.FlashBackTableStmt, *ast.FlashBackDatabaseStmt:
// Recover table command can only be executed by administrator.
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.SuperPriv, "", "", "", nil)
Expand Down
11 changes: 8 additions & 3 deletions planner/core/preprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -1150,10 +1150,15 @@ func (p *preprocessor) checkGroupBy(stmt *ast.GroupByClause) {
}

func (p *preprocessor) checkRenameTableGrammar(stmt *ast.RenameTableStmt) {
oldTable := stmt.TableToTables[0].OldTable.Name.String()
newTable := stmt.TableToTables[0].NewTable.Name.String()
for _, tableToTable := range stmt.TableToTables {
oldTable := tableToTable.OldTable.Name.String()
newTable := tableToTable.NewTable.Name.String()

p.checkRenameTable(oldTable, newTable)
p.checkRenameTable(oldTable, newTable)
if p.err != nil {
return
}
}
}

func (p *preprocessor) checkRenameTable(oldTable, newTable string) {
Expand Down
118 changes: 118 additions & 0 deletions privilege/privileges/privileges_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,124 @@ func TestShowCreateTable(t *testing.T) {
tk.MustExec(`SHOW CREATE TABLE mysql.user`)
}

func TestExchangePartitionChecksTargetTablePrivileges(t *testing.T) {
store := createStoreAndPrepareDB(t)
rootTk := testkit.NewTestKit(t, store)
rootTk.MustExec(`CREATE DATABASE exchange_src`)
rootTk.MustExec(`CREATE DATABASE exchange_dst`)
rootTk.MustExec(`CREATE TABLE exchange_src.pt (a int primary key) PARTITION BY RANGE (a) (
PARTITION p0 VALUES LESS THAN (100),
PARTITION p1 VALUES LESS THAN (200)
)`)
rootTk.MustExec(`CREATE TABLE exchange_dst.nt (a int primary key)`)
rootTk.MustExec(`CREATE USER 'exchange_low'@'%'`)
rootTk.MustExec(`GRANT ALTER, DROP ON exchange_src.* TO 'exchange_low'@'%'`)

tk := testkit.NewTestKit(t, store)
authLow := func() {
require.NoError(t, tk.Session().Auth(&auth.UserIdentity{
Username: "exchange_low", Hostname: "%", AuthUsername: "exchange_low", AuthHostname: "%",
}, nil, nil))
}
exchange := func() error {
authLow()
return tk.ExecToErr(`ALTER TABLE exchange_src.pt EXCHANGE PARTITION p0 WITH TABLE exchange_dst.nt`)
}

// EXCHANGE PARTITION swaps the data of both tables, so to match MySQL it requires
// ALTER, INSERT, CREATE and DROP on BOTH the partitioned table (pt) and the
// non-partitioned table (nt). Grant them one missing privilege at a time and assert
// each is enforced; in particular DROP on nt guards against destroying/replacing nt's
// existing data without the DROP privilege.

// Missing CREATE on the target table nt.
err := exchange()
require.True(t, terror.ErrorEqual(err, core.ErrTableaccessDenied))
require.Contains(t, err.Error(), "CREATE command denied")
require.Contains(t, err.Error(), "'nt'")

// nt now has CREATE+INSERT, but the partitioned table pt still lacks INSERT.
rootTk.MustExec(`GRANT CREATE, INSERT ON exchange_dst.* TO 'exchange_low'@'%'`)
err = exchange()
require.True(t, terror.ErrorEqual(err, core.ErrTableaccessDenied))
require.Contains(t, err.Error(), "INSERT command denied")
require.Contains(t, err.Error(), "'pt'")

// pt now has INSERT+CREATE, but the target table nt still lacks ALTER.
rootTk.MustExec(`GRANT INSERT, CREATE ON exchange_src.* TO 'exchange_low'@'%'`)
err = exchange()
require.True(t, terror.ErrorEqual(err, core.ErrTableaccessDenied))
require.Contains(t, err.Error(), "ALTER command denied")
require.Contains(t, err.Error(), "'nt'")

// nt now has ALTER, but still lacks DROP: exchanging replaces nt's existing data, which
// MySQL gates behind DROP on nt. Without DROP the statement must still be denied.
rootTk.MustExec(`GRANT ALTER ON exchange_dst.* TO 'exchange_low'@'%'`)
err = exchange()
require.True(t, terror.ErrorEqual(err, core.ErrTableaccessDenied))
require.Contains(t, err.Error(), "DROP command denied")
require.Contains(t, err.Error(), "'nt'")

// Full MySQL-aligned privilege set on both tables -> allowed.
rootTk.MustExec(`GRANT DROP ON exchange_dst.* TO 'exchange_low'@'%'`)
authLow()
tk.MustExec(`ALTER TABLE exchange_src.pt EXCHANGE PARTITION p0 WITH TABLE exchange_dst.nt`)
}

func TestExchangePartitionRejectsReservedSystemTableTarget(t *testing.T) {
store := createStoreAndPrepareDB(t)
tk := testkit.NewTestKit(t, store)
tk.MustExec(`CREATE DATABASE exchange_reserved`)
tk.MustExec(`CREATE TABLE exchange_reserved.pt (a int primary key) PARTITION BY RANGE (a) (
PARTITION p0 VALUES LESS THAN (100),
PARTITION p1 VALUES LESS THAN (200)
)`)

err := tk.ExecToErr(`ALTER TABLE exchange_reserved.pt EXCHANGE PARTITION p0 WITH TABLE mysql.tidb_ddl_job`)
require.Error(t, err)
require.Contains(t, err.Error(), "Exchange partition on system table 'mysql.tidb_ddl_job'")
}

func TestRenameTablesChecksAllPairsPrivileges(t *testing.T) {
store := createStoreAndPrepareDB(t)

rootTk := testkit.NewTestKit(t, store)
rootTk.MustExec(`CREATE DATABASE rename_priv_atk`)
rootTk.MustExec(`CREATE DATABASE rename_priv_vic`)
rootTk.MustExec(`CREATE USER 'rename_low'@'%'`)
rootTk.MustExec(`GRANT ALL PRIVILEGES ON rename_priv_atk.* TO 'rename_low'@'%'`)
rootTk.MustExec(`CREATE TABLE rename_priv_atk.pair0a (id INT)`)
rootTk.MustExec(`CREATE TABLE rename_priv_atk.pair0b (id INT)`)
rootTk.MustExec(`CREATE TABLE rename_priv_vic.secret (id INT)`)

tk := testkit.NewTestKit(t, store)
require.NoError(t, tk.Session().Auth(&auth.UserIdentity{
Username: "rename_low",
Hostname: "localhost",
AuthUsername: "rename_low",
AuthHostname: "%",
}, nil, nil))

tk.MustGetErrCode(`RENAME TABLE rename_priv_atk.pair0a TO rename_priv_atk.pair0a_tmp,
rename_priv_vic.secret TO rename_priv_atk.secret_stolen`, errno.ErrTableaccessDenied)
rootTk.MustQuery(`SHOW TABLES FROM rename_priv_atk LIKE 'pair0a'`).Check(testkit.Rows("pair0a"))
rootTk.MustQuery(`SHOW TABLES FROM rename_priv_atk LIKE 'pair0a_tmp'`).Check(testkit.Rows())
rootTk.MustQuery(`SHOW TABLES FROM rename_priv_vic LIKE 'secret'`).Check(testkit.Rows("secret"))
rootTk.MustQuery(`SHOW TABLES FROM rename_priv_atk LIKE 'secret_stolen'`).Check(testkit.Rows())

tk.MustGetErrCode(`RENAME TABLE rename_priv_atk.pair0a TO rename_priv_atk.pair0a_tmp,
rename_priv_atk.pair0b TO rename_priv_vic.pair0b_tmp`, errno.ErrTableaccessDenied)
rootTk.MustQuery(`SHOW TABLES FROM rename_priv_atk LIKE 'pair0a'`).Check(testkit.Rows("pair0a"))
rootTk.MustQuery(`SHOW TABLES FROM rename_priv_atk LIKE 'pair0a_tmp'`).Check(testkit.Rows())
rootTk.MustQuery(`SHOW TABLES FROM rename_priv_atk LIKE 'pair0b'`).Check(testkit.Rows("pair0b"))
rootTk.MustQuery(`SHOW TABLES FROM rename_priv_vic LIKE 'pair0b_tmp'`).Check(testkit.Rows())

tk.MustExec(`RENAME TABLE rename_priv_atk.pair0a TO rename_priv_atk.pair0a_tmp,
rename_priv_atk.pair0b TO rename_priv_atk.pair0b_tmp`)
rootTk.MustQuery(`SHOW TABLES FROM rename_priv_atk LIKE 'pair0a_tmp'`).Check(testkit.Rows("pair0a_tmp"))
rootTk.MustQuery(`SHOW TABLES FROM rename_priv_atk LIKE 'pair0b_tmp'`).Check(testkit.Rows("pair0b_tmp"))
}

func TestReplaceAndInsertOnDuplicate(t *testing.T) {
store := createStoreAndPrepareDB(t)

Expand Down
Loading