Skip to content

Commit

Permalink
*: Fix ALTER USER privilege requirements (#25461) (#25512)
Browse files Browse the repository at this point in the history
  • Loading branch information
ti-srebot authored Jun 17, 2021
1 parent 573515e commit 709b68e
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 32 deletions.
1 change: 1 addition & 0 deletions executor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ func (s *testSuiteP1) TestShow(c *C) {
"Usage Server Admin No privileges - allow connect only",
"BACKUP_ADMIN Server Admin ",
"RESTORE_ADMIN Server Admin ",
"SYSTEM_USER Server Admin ",
"SYSTEM_VARIABLES_ADMIN Server Admin ",
"ROLE_ADMIN Server Admin ",
"CONNECTION_ADMIN Server Admin ",
Expand Down
84 changes: 64 additions & 20 deletions executor/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/pingcap/tidb/kv"
"github.com/pingcap/tidb/metrics"
"github.com/pingcap/tidb/planner/core"
plannercore "github.com/pingcap/tidb/planner/core"
"github.com/pingcap/tidb/plugin"
"github.com/pingcap/tidb/privilege"
"github.com/pingcap/tidb/sessionctx"
Expand All @@ -45,6 +46,7 @@ import (
"github.com/pingcap/tidb/util/collate"
"github.com/pingcap/tidb/util/hack"
"github.com/pingcap/tidb/util/logutil"
"github.com/pingcap/tidb/util/sem"
"github.com/pingcap/tidb/util/sqlexec"
"github.com/pingcap/tidb/util/timeutil"
"github.com/pingcap/tipb/go-tipb"
Expand Down Expand Up @@ -847,16 +849,47 @@ func (e *SimpleExec) executeAlterUser(s *ast.AlterUserStmt) error {
}

failedUsers := make([]string, 0, len(s.Specs))
checker := privilege.GetPrivilegeManager(e.ctx)
if checker == nil {
return errors.New("could not load privilege checker")
}
activeRoles := e.ctx.GetSessionVars().ActiveRoles
hasCreateUserPriv := checker.RequestVerification(activeRoles, "", "", "", mysql.CreateUserPriv)
hasSystemUserPriv := checker.RequestDynamicVerification(activeRoles, "SYSTEM_USER", false)
hasRestrictedUserPriv := checker.RequestDynamicVerification(activeRoles, "RESTRICTED_USER_ADMIN", false)
hasSystemSchemaPriv := checker.RequestVerification(activeRoles, mysql.SystemDB, mysql.UserTable, "", mysql.UpdatePriv)

for _, spec := range s.Specs {
user := e.ctx.GetSessionVars().User
if spec.User.CurrentUser || ((user != nil) && (user.Username == spec.User.Username) && (user.AuthHostname == spec.User.Hostname)) {
spec.User.Username = user.Username
spec.User.Hostname = user.AuthHostname
} else {
checker := privilege.GetPrivilegeManager(e.ctx)
activeRoles := e.ctx.GetSessionVars().ActiveRoles
if checker != nil && !checker.RequestVerification(activeRoles, "", "", "", mysql.SuperPriv) {
return ErrDBaccessDenied.GenWithStackByArgs(spec.User.Username, spec.User.Hostname, "mysql")

// The user executing the query (user) does not match the user specified (spec.User)
// The MySQL manual states:
// "In most cases, ALTER USER requires the global CREATE USER privilege, or the UPDATE privilege for the mysql system schema"
//
// This is true unless the user being modified has the SYSTEM_USER dynamic privilege.
// See: https://mysqlserverteam.com/the-system_user-dynamic-privilege/
//
// In the current implementation of DYNAMIC privileges, SUPER can be used as a substitute for any DYNAMIC privilege
// (unless SEM is enabled; in which case RESTRICTED_* privileges will not use SUPER as a substitute). This is intentional
// because visitInfo can not accept OR conditions for permissions and in many cases MySQL permits SUPER instead.

// Thus, any user with SUPER can effectively ALTER/DROP a SYSTEM_USER, and
// any user with only CREATE USER can not modify the properties of users with SUPER privilege.
// We extend this in TiDB with SEM, where SUPER users can not modify users with RESTRICTED_USER_ADMIN.
// For simplicity: RESTRICTED_USER_ADMIN also counts for SYSTEM_USER here.

if !(hasCreateUserPriv || hasSystemSchemaPriv) {
return plannercore.ErrSpecificAccessDenied.GenWithStackByArgs("CREATE USER")
}
if checker.RequestDynamicVerificationWithUser("SYSTEM_USER", false, spec.User) && !(hasSystemUserPriv || hasRestrictedUserPriv) {
return plannercore.ErrSpecificAccessDenied.GenWithStackByArgs("SYSTEM_USER or SUPER")
}
if sem.IsEnabled() && checker.RequestDynamicVerificationWithUser("RESTRICTED_USER_ADMIN", false, spec.User) && !hasRestrictedUserPriv {
return plannercore.ErrSpecificAccessDenied.GenWithStackByArgs("RESTRICTED_USER_ADMIN")
}
}

Expand Down Expand Up @@ -1100,25 +1133,24 @@ func renameUserHostInSystemTable(sqlExecutor sqlexec.SQLExecutor, tableName, use
func (e *SimpleExec) executeDropUser(s *ast.DropUserStmt) error {
// Check privileges.
// Check `CREATE USER` privilege.
if !config.GetGlobalConfig().Security.SkipGrantTable {
checker := privilege.GetPrivilegeManager(e.ctx)
if checker == nil {
return errors.New("miss privilege checker")
}
activeRoles := e.ctx.GetSessionVars().ActiveRoles
if !checker.RequestVerification(activeRoles, mysql.SystemDB, mysql.UserTable, "", mysql.DeletePriv) {
if s.IsDropRole {
if !checker.RequestVerification(activeRoles, "", "", "", mysql.DropRolePriv) &&
!checker.RequestVerification(activeRoles, "", "", "", mysql.CreateUserPriv) {
return core.ErrSpecificAccessDenied.GenWithStackByArgs("DROP ROLE or CREATE USER")
}
}
if !s.IsDropRole && !checker.RequestVerification(activeRoles, "", "", "", mysql.CreateUserPriv) {
return core.ErrSpecificAccessDenied.GenWithStackByArgs("CREATE USER")
checker := privilege.GetPrivilegeManager(e.ctx)
if checker == nil {
return errors.New("miss privilege checker")
}
activeRoles := e.ctx.GetSessionVars().ActiveRoles
if !checker.RequestVerification(activeRoles, mysql.SystemDB, mysql.UserTable, "", mysql.DeletePriv) {
if s.IsDropRole {
if !checker.RequestVerification(activeRoles, "", "", "", mysql.DropRolePriv) &&
!checker.RequestVerification(activeRoles, "", "", "", mysql.CreateUserPriv) {
return core.ErrSpecificAccessDenied.GenWithStackByArgs("DROP ROLE or CREATE USER")
}
}
if !s.IsDropRole && !checker.RequestVerification(activeRoles, "", "", "", mysql.CreateUserPriv) {
return core.ErrSpecificAccessDenied.GenWithStackByArgs("CREATE USER")
}
}

hasSystemUserPriv := checker.RequestDynamicVerification(activeRoles, "SYSTEM_USER", false)
hasRestrictedUserPriv := checker.RequestDynamicVerification(activeRoles, "RESTRICTED_USER_ADMIN", false)
failedUsers := make([]string, 0, len(s.UserList))
sysSession, err := e.getSysSession()
defer e.releaseSysSession(sysSession)
Expand Down Expand Up @@ -1146,6 +1178,18 @@ func (e *SimpleExec) executeDropUser(s *ast.DropUserStmt) error {
}
}

// Certain users require additional privileges in order to be modified.
// If this is the case, we need to rollback all changes and return a privilege error.
// Because in TiDB SUPER can be used as a substitute for any dynamic privilege, this effectively means that
// any user with SUPER requires a user with SUPER to be able to DROP the user.
// We also allow RESTRICTED_USER_ADMIN to count for simplicity.
if checker.RequestDynamicVerificationWithUser("SYSTEM_USER", false, user) && !(hasSystemUserPriv || hasRestrictedUserPriv) {
if _, err := sqlExecutor.ExecuteInternal(context.TODO(), "rollback"); err != nil {
return err
}
return plannercore.ErrSpecificAccessDenied.GenWithStackByArgs("SYSTEM_USER or SUPER")
}

// begin a transaction to delete a user.
sql.Reset()
sqlexec.MustFormatSQL(sql, `DELETE FROM %n.%n WHERE Host = %? and User = %?;`, mysql.SystemDB, mysql.UserTable, user.Hostname, user.Username)
Expand Down
2 changes: 1 addition & 1 deletion planner/core/planbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2347,7 +2347,7 @@ func (b *PlanBuilder) buildSimple(ctx context.Context, node ast.StmtNode) (Plan,
case *ast.AlterInstanceStmt:
err := ErrSpecificAccessDenied.GenWithStack("SUPER")
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.SuperPriv, "", "", "", err)
case *ast.AlterUserStmt, *ast.RenameUserStmt:
case *ast.RenameUserStmt:
err := ErrSpecificAccessDenied.GenWithStackByArgs("CREATE USER")
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.CreateUserPriv, "", "", "", err)
case *ast.GrantStmt:
Expand Down
1 change: 1 addition & 0 deletions privilege/privileges/privileges.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var _ privilege.Manager = (*UserPrivileges)(nil)
var dynamicPrivs = []string{
"BACKUP_ADMIN",
"RESTORE_ADMIN",
"SYSTEM_USER",
"SYSTEM_VARIABLES_ADMIN",
"ROLE_ADMIN",
"CONNECTION_ADMIN",
Expand Down
99 changes: 88 additions & 11 deletions privilege/privileges/privileges_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,22 +472,99 @@ func (s *testPrivilegeSuite) TestAlterUserStmt(c *C) {
se := newSession(c, s.store, s.dbName)

// high privileged user setting password for other user (passes)
mustExec(c, se, "CREATE USER 'superuser2'")
mustExec(c, se, "CREATE USER 'nobodyuser2'")
mustExec(c, se, "CREATE USER 'nobodyuser3'")
mustExec(c, se, "GRANT ALL ON *.* TO 'superuser2'")
mustExec(c, se, "GRANT CREATE USER ON *.* TO 'nobodyuser2'")

c.Assert(se.Auth(&auth.UserIdentity{Username: "superuser2", Hostname: "localhost", AuthUsername: "superuser2", AuthHostname: "%"}, nil, nil), IsTrue)
mustExec(c, se, "CREATE USER superuser2, nobodyuser2, nobodyuser3, nobodyuser4, nobodyuser5, semuser1, semuser2, semuser3, semuser4")
mustExec(c, se, "GRANT ALL ON *.* TO superuser2")
mustExec(c, se, "GRANT CREATE USER ON *.* TO nobodyuser2")
mustExec(c, se, "GRANT SYSTEM_USER ON *.* TO nobodyuser4")
mustExec(c, se, "GRANT UPDATE ON mysql.user TO nobodyuser5, semuser1")
mustExec(c, se, "GRANT RESTRICTED_TABLES_ADMIN ON *.* TO semuser1")
mustExec(c, se, "GRANT RESTRICTED_USER_ADMIN ON *.* TO semuser1, semuser2, semuser3")
mustExec(c, se, "GRANT SYSTEM_USER ON *.* to semuser3") // user is both restricted + has SYSTEM_USER (or super)

c.Assert(se.Auth(&auth.UserIdentity{Username: "superuser2", Hostname: "localhost"}, nil, nil), IsTrue)
mustExec(c, se, "ALTER USER 'nobodyuser2' IDENTIFIED BY 'newpassword'")
mustExec(c, se, "ALTER USER 'nobodyuser2' IDENTIFIED BY ''")

// low privileged user trying to set password for other user (fails)
c.Assert(se.Auth(&auth.UserIdentity{Username: "nobodyuser2", Hostname: "localhost", AuthUsername: "nobodyuser2", AuthHostname: "%"}, nil, nil), IsTrue)
// low privileged user trying to set password for others
// nobodyuser3 = SUCCESS (not a SYSTEM_USER)
// nobodyuser4 = FAIL (has SYSTEM_USER)
// superuser2 = FAIL (has SYSTEM_USER privilege implied by SUPER)

c.Assert(se.Auth(&auth.UserIdentity{Username: "nobodyuser2", Hostname: "localhost"}, nil, nil), IsTrue)
mustExec(c, se, "ALTER USER 'nobodyuser2' IDENTIFIED BY 'newpassword'")
mustExec(c, se, "ALTER USER 'nobodyuser2' IDENTIFIED BY ''")
_, err := se.ExecuteInternal(context.Background(), "ALTER USER 'superuser2' IDENTIFIED BY 'newpassword'")
c.Assert(err, NotNil)
mustExec(c, se, "ALTER USER 'nobodyuser3' IDENTIFIED BY ''")
_, err := se.ExecuteInternal(context.Background(), "ALTER USER 'nobodyuser4' IDENTIFIED BY 'newpassword'")
c.Assert(err.Error(), Equals, "[planner:1227]Access denied; you need (at least one of) the SYSTEM_USER or SUPER privilege(s) for this operation")
_, err = se.ExecuteInternal(context.Background(), "ALTER USER 'superuser2' IDENTIFIED BY 'newpassword'")
c.Assert(err.Error(), Equals, "[planner:1227]Access denied; you need (at least one of) the SYSTEM_USER or SUPER privilege(s) for this operation")

// Nobody3 has no privileges at all, but they can still alter their own password.
// Any other user fails.
c.Assert(se.Auth(&auth.UserIdentity{Username: "nobodyuser3", Hostname: "localhost"}, nil, nil), IsTrue)
mustExec(c, se, "ALTER USER 'nobodyuser3' IDENTIFIED BY ''")
_, err = se.ExecuteInternal(context.Background(), "ALTER USER 'nobodyuser4' IDENTIFIED BY 'newpassword'")
c.Assert(err.Error(), Equals, "[planner:1227]Access denied; you need (at least one of) the CREATE USER privilege(s) for this operation")
_, err = se.ExecuteInternal(context.Background(), "ALTER USER 'superuser2' IDENTIFIED BY 'newpassword'") // it checks create user before SYSTEM_USER
c.Assert(err.Error(), Equals, "[planner:1227]Access denied; you need (at least one of) the CREATE USER privilege(s) for this operation")

// Nobody5 doesn't explicitly have CREATE USER, but mysql also accepts UDPATE on mysql.user
// as a substitute so it can modify nobody2 and nobody3 but not nobody4

c.Assert(se.Auth(&auth.UserIdentity{Username: "nobodyuser5", Hostname: "localhost"}, nil, nil), IsTrue)
mustExec(c, se, "ALTER USER 'nobodyuser2' IDENTIFIED BY ''")
mustExec(c, se, "ALTER USER 'nobodyuser3' IDENTIFIED BY ''")
_, err = se.ExecuteInternal(context.Background(), "ALTER USER 'nobodyuser4' IDENTIFIED BY 'newpassword'")
c.Assert(err.Error(), Equals, "[planner:1227]Access denied; you need (at least one of) the SYSTEM_USER or SUPER privilege(s) for this operation")

c.Assert(se.Auth(&auth.UserIdentity{Username: "semuser1", Hostname: "localhost"}, nil, nil), IsTrue)
mustExec(c, se, "ALTER USER 'semuser1' IDENTIFIED BY ''")
mustExec(c, se, "ALTER USER 'semuser2' IDENTIFIED BY ''")
mustExec(c, se, "ALTER USER 'semuser3' IDENTIFIED BY ''")

sem.Enable()
defer sem.Disable()

// When SEM is enabled, even though we have UPDATE privilege on mysql.user, it explicitly
// denies writeable privileges to system schemas unless RESTRICTED_TABLES_ADMIN is granted.
// so the previous method of granting to the table instead of CREATE USER will fail now.
// This is intentional because SEM plugs directly into the privilege manager to DENY
// any request for UpdatePriv on mysql.user even if the privilege exists in the internal mysql.user table.

// UpdatePriv on mysql.user
c.Assert(se.Auth(&auth.UserIdentity{Username: "nobodyuser5", Hostname: "localhost"}, nil, nil), IsTrue)
_, err = se.ExecuteInternal(context.Background(), "ALTER USER 'nobodyuser2' IDENTIFIED BY 'newpassword'")
c.Assert(err.Error(), Equals, "[planner:1227]Access denied; you need (at least one of) the CREATE USER privilege(s) for this operation")

// actual CreateUserPriv
c.Assert(se.Auth(&auth.UserIdentity{Username: "nobodyuser2", Hostname: "localhost"}, nil, nil), IsTrue)
mustExec(c, se, "ALTER USER 'nobodyuser2' IDENTIFIED BY ''")
mustExec(c, se, "ALTER USER 'nobodyuser3' IDENTIFIED BY ''")

// UpdatePriv on mysql.user but also has RESTRICTED_TABLES_ADMIN
c.Assert(se.Auth(&auth.UserIdentity{Username: "semuser1", Hostname: "localhost"}, nil, nil), IsTrue)
mustExec(c, se, "ALTER USER 'nobodyuser2' IDENTIFIED BY ''")
mustExec(c, se, "ALTER USER 'nobodyuser3' IDENTIFIED BY ''")

// As it has (RESTRICTED_TABLES_ADMIN + UpdatePriv on mysql.user) + RESTRICTED_USER_ADMIN it can modify other restricted_user_admins like semuser2
// and it can modify semuser3 because RESTRICTED_USER_ADMIN does not also need SYSTEM_USER
mustExec(c, se, "ALTER USER 'semuser1' IDENTIFIED BY ''")
mustExec(c, se, "ALTER USER 'semuser2' IDENTIFIED BY ''")
mustExec(c, se, "ALTER USER 'semuser3' IDENTIFIED BY ''")

c.Assert(se.Auth(&auth.UserIdentity{Username: "superuser2", Hostname: "localhost"}, nil, nil), IsTrue)
_, err = se.ExecuteInternal(context.Background(), "ALTER USER 'semuser1' IDENTIFIED BY 'newpassword'")
c.Assert(err.Error(), Equals, "[planner:1227]Access denied; you need (at least one of) the RESTRICTED_USER_ADMIN privilege(s) for this operation")

c.Assert(se.Auth(&auth.UserIdentity{Username: "semuser4", Hostname: "localhost"}, nil, nil), IsTrue)
// has restricted_user_admin but not CREATE USER or (update on mysql.user + RESTRICTED_TABLES_ADMIN)
mustExec(c, se, "ALTER USER 'semuser4' IDENTIFIED BY ''") // can modify self
_, err = se.ExecuteInternal(context.Background(), "ALTER USER 'nobodyuser3' IDENTIFIED BY 'newpassword'")
c.Assert(err.Error(), Equals, "[planner:1227]Access denied; you need (at least one of) the CREATE USER privilege(s) for this operation")
_, err = se.ExecuteInternal(context.Background(), "ALTER USER 'semuser1' IDENTIFIED BY 'newpassword'")
c.Assert(err.Error(), Equals, "[planner:1227]Access denied; you need (at least one of) the CREATE USER privilege(s) for this operation")
_, err = se.ExecuteInternal(context.Background(), "ALTER USER 'semuser3' IDENTIFIED BY 'newpassword'")
c.Assert(err.Error(), Equals, "[planner:1227]Access denied; you need (at least one of) the CREATE USER privilege(s) for this operation")
}

func (s *testPrivilegeSuite) TestSelectViewSecurity(c *C) {
Expand Down

0 comments on commit 709b68e

Please sign in to comment.