暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

TiDB 赋权问题

2021-06-26
307

作者:jiyf

原文来源:https://tidb.net/blog/9268fb04

【是否原创】是\ 【首发渠道】TiDB 社区\ 【目录】\ 【正文】

复现问题

  1. 创建了一个用户 abc
  2. use information_schema
  3. 给新建的 abc 添加 insert 权限失败,提示权限验证失败

``` mysql> select @@version; +---------------------+ | @@version | +---------------------+ | 5.7.25-TiDB-v4.0.11 | +---------------------+ 1 row in set (0.00 sec)

mysql> create user 'abc'@'%'; Query OK, 0 rows affected (0.01 sec)

mysql> use information_schema Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A

Database changed mysql> grant insert on . to 'abc'@'%'; ERROR 8121 (HY000): privilege check fail ```

正常情况下这里不应该报错的,以下的操作就没有问题,这也是期望的结果:

``` Type 'help;' or '\"h' for help. Type '\"c' to clear the current input statement.

mysql> grant insert on . to 'abc'@'%'; Query OK, 0 rows affected (0.01 sec)

mysql> use test Database changed mysql> grant insert on . to 'abc'@'%'; Query OK, 0 rows affected (0.01 sec) ```

只要在 information_schema,就会报错:

mysql> use information_schema Database changed mysql> grant insert on *.* to 'abc'@'%'; ERROR 8121 (HY000): privilege check fail

排查问题

怀疑是下面的原因导致的问题:

  1. use information_schema 使得当前会话的当前 DB 为 information_schema
  2. information_schema 非用户表,所以不允许 update 权限
  3. 在权限检查时候,指定的赋权范围是 *.*,但是权限检查却错误的检查了当前 DB information_schema

顺着这个思路查看 TiDB 源码,翻到下面一段:

`` func (p *UserPrivileges) RequestVerification(activeRoles []*auth.RoleIdentity, db, table, column string, priv mysql.PrivilegeType) bool { ... // Skip check for system databases. // See https://dev.mysql.com/doc/refman/5.7/en/information-schema.html dbLowerName := strings.ToLower(db) switch dbLowerName { // 如果 db 是 InformationSchema,且权限检查是 mysql.UpdatePri,那这里就返回验证权限失败 case util.InformationSchemaName.L: switch priv { case mysql.CreatePriv, mysql.AlterPriv, mysql.DropPriv, mysql.IndexPriv, mysql.CreateViewPriv, mysql.InsertPriv, mysql.UpdatePriv, mysql.DeletePriv: return false } return true // We should be very careful of limiting privileges, so ignoremysql` for now. case util.PerformanceSchemaName.L, util.MetricSchemaName.L: if (dbLowerName == util.PerformanceSchemaName.L && perfschema.IsPredefinedTable(table)) || (dbLowerName == util.MetricSchemaName.L && infoschema.IsMetricTable(table)) { switch priv { case mysql.CreatePriv, mysql.AlterPriv, mysql.DropPriv, mysql.IndexPriv, mysql.InsertPriv, mysql.UpdatePriv, mysql.DeletePriv: return false case mysql.SelectPriv: return true } } }

...

} ```

这里对系统表 InformationSchema、PerformanceSchema 进行了权限限制。从代码上看,如果验证用户对 information_schema 执行 update 权限,那返回不通过。

mysql> grant insert on information_schema.* to 'abc'@'%'; ERROR 8121 (HY000): privilege check fail

这里尝试给用户在 information_schema 库上添加 insert 权限,返回权限不通过,符合预期。

所以接下来排查,在 use information_schema 情况下,哪里错把 information_schema 作为了 *.* 的验证参数。

打开 planbuilder.go,看下 grant 操作需要对哪些权限进行验证:

``` func (b PlanBuilder) buildSimple(node ast.StmtNode) (Plan, error) { p := &Simple{Statement: node} switch raw := node.(type) { ... case ast.GrantStmt: if b.ctx.GetSessionVars().CurrentDB == "" && raw.Level.DBName == "" { if raw.Level.Level == ast.GrantLevelTable { return nil, ErrNoDB } } b.visitInfo = collectVisitInfoFromGrantStmt(b.ctx, b.visitInfo, raw) ... }

func collectVisitInfoFromGrantStmt(sctx sessionctx.Context, vi []visitInfo, stmt *ast.GrantStmt) []visitInfo { // To use GRANT, you must have the GRANT OPTION privilege, // and you must have the privileges that you are granting. dbName := stmt.Level.DBName tableName := stmt.Level.TableName // 这里就是出现问题的原因 if dbName == "" { dbName = sctx.GetSessionVars().CurrentDB } vi = appendVisitInfo(vi, mysql.GrantPriv, dbName, tableName, "", nil) ... } ```

从代码上看,如果 stmt.Level.DBName 是空,那么就赋值 CurrentDB。

所以如果对 *.* 处理后,认为 stmt.Level.DBName 是空,那么就赋值 CurrentDB(information_schema ),后面自然就出现验证不通过的情景。

在这里添加一条日志:

case *ast.GrantStmt: if b.ctx.GetSessionVars().CurrentDB == "" && raw.Level.DBName == "" { if raw.Level.Level == ast.GrantLevelTable { return nil, ErrNoDB } } logutil.BgLogger().Error("hello", zap.String("CurrentDB", b.ctx.GetSessionVars().CurrentDB), zap.String("rawDB", raw.Level.DBName), zap.String("rawTable", raw.Level.TableName)) b.visitInfo = collectVisitInfoFromGrantStmt(b.ctx, b.visitInfo, raw)

启动后执行以下 mysql 操作:

mysql> use information_schema Database changed mysql> grant insert on *.* to 'abc'@'%'; ERROR 8121 (HY000): privilege check fail

查看日志看到下面的记录:

[2021/06/24 14:09:27.698 +08:00] [ERROR] [planbuilder.go:2019] [hello] [CurrentDB=information_schema] [rawDB=] [rawTable=]

这里果然对于 *.* 把 raw.Level.DBName、raw.Level.TableName,都作为空置对待,然后出现 dbName = sctx.GetSessionVars().CurrentDB,导致验证失败。

再添加一条日志:

dbLowerName := strings.ToLower(db) logutil.BgLogger().Error("hello world", zap.String("db", dbLowerName), zap.String("table", table), zap.String("user", p.user), zap.String("host", p.host), zap.Uint64("priv", uint64(priv))) switch dbLowerName { case util.InformationSchemaName.L: switch priv { case mysql.CreatePriv, mysql.AlterPriv, mysql.DropPriv, mysql.IndexPriv, mysql.CreateViewPriv, mysql.InsertPriv, mysql.UpdatePriv, mysql.DeletePriv: return false }

看到以下日志:

[2021/06/24 14:05:03.419 +08:00] [ERROR] [privileges.go:60] [hello world] [db=information_schema] [table=] [user=root] [host=%] [priv=4096] [2021/06/24 14:05:03.419 +08:00] [ERROR] [privileges.go:60] [hello world] [db=information_schema] [table=] [user=root] [host=%] [priv=8]

其中 priv=4096 是 GrantPriv,priv=8 就是 InsertPriv,到这里就验证不通过,添加权限失败。

安全权限问题

给用户赋权需要以下条件:

  1. 进行赋权的用户带有 grant 权限
  2. 只能对自己拥有的权限赋给其他用户,比如自己只有 select 权限,那自然没有权利去给其他用户添加 insert 权限。

进行 grant 操作时候,权限检查实际就是检查赋权人是否满足以上条件。

结合以上几个方面,可以实现一个带有 grant 权限的非超级用户达到扩展自己权限的目的。

扩大权限范围

扩大权限方法如下:

  1. grant priv on *.* to ‘user’@‘host’,参考日志,*.* 会使得 [rawDB=] [rawTable=]
  2. 如果当前 CurrentDB 不为空,那么验证权限的 db 就被设置为 CurrentDB
  3. 然后会对当前用户对 CurrentDB 进行 priv 权限验证
  4. 而当前用户拥有对 CurrentDB 的 priv 权限
  5. 那么权限验证成功
  6. 给 ‘user’@‘host’ 添加权限成功,而这些 priv 权限是对所有库表,超过 CurrentDB。

创建用户 ‘abc’@’%’,给 Select,Insert,Update,Delete,GRANT 权限:

``` mysql> create user 'abc'@'%' identified by '123'; Query OK, 0 rows affected (0.02 sec)

mysql> grant insert,update,delete,select on test.* to 'abc'@'%' with grant option; Query OK, 0 rows affected (0.01 sec)

mysql> show grants for 'abc'@'%'; +----------------------------------------------------------------------------+ | Grants for abc@% | +----------------------------------------------------------------------------+ | GRANT USAGE ON . TO 'abc'@'%' | | GRANT Select,Insert,Update,Delete ON test.* TO 'abc'@'%' WITH GRANT OPTION | +----------------------------------------------------------------------------+ 2 rows in set (0.00 sec) ```

使用 ‘abc’@’%’ 登录数据库,可以看到它拥有的权限:

``` mysql> show grants; +----------------------------------------------------------------------------+ | Grants for User | +----------------------------------------------------------------------------+ | GRANT USAGE ON . TO 'abc'@'%' | | GRANT Select,Insert,Update,Delete ON test.* TO 'abc'@'%' WITH GRANT OPTION | +----------------------------------------------------------------------------+ 2 rows in set (0.00 sec)

mysql> show databases; +--------------------+ | Database | +--------------------+ | INFORMATION_SCHEMA | | test | +--------------------+ 2 rows in set (0.00 sec) ```

对它的权限进行扩充是被禁止的,这符合预期:

mysql> GRANT Select,Insert,Update,Delete ON *.* TO 'abc'@'%'; ERROR 8121 (HY000): privilege check fail mysql> show grants; +----------------------------------------------------------------------------+ | Grants for User | +----------------------------------------------------------------------------+ | GRANT USAGE ON *.* TO 'abc'@'%' | | GRANT Select,Insert,Update,Delete ON test.* TO 'abc'@'%' WITH GRANT OPTION | +----------------------------------------------------------------------------+ 2 rows in set (0.00 sec)

执行以下方法就可以顺利扩充它的权限:

``` mysql> use test Database changed mysql> GRANT Select,Insert,Update,Delete ON . TO 'abc'@'%'; Query OK, 0 rows affected (0.02 sec)

mysql> show grants; +----------------------------------------------------------------------------+ | Grants for User | +----------------------------------------------------------------------------+ | GRANT Select,Insert,Update,Delete ON . TO 'abc'@'%' | | GRANT Select,Insert,Update,Delete ON test.* TO 'abc'@'%' WITH GRANT OPTION | +----------------------------------------------------------------------------+ 2 rows in set (0.00 sec)

mysql> show databases; +--------------------+ | Database | +--------------------+ | INFORMATION_SCHEMA | | METRICS_SCHEMA | | PERFORMANCE_SCHEMA | | mysql | | sbtest
| test | | tpcc | +--------------------+ 10 rows in set (0.00 sec) ```

可以看到 ’abc’@’%’ 拥有了全部库表 Select,Insert,Update,Delete 权限,实现权限扩充。

新增权限种类

再回头看下这段代码:

dbLowerName := strings.ToLower(db) switch dbLowerName { case util.InformationSchemaName.L: switch priv { case mysql.CreatePriv, mysql.AlterPriv, mysql.DropPriv, mysql.IndexPriv, mysql.CreateViewPriv, mysql.InsertPriv, mysql.UpdatePriv, mysql.DeletePriv: return false } return true

当检查 InformationSchema 库的 mysql.CreatePriv, mysql.AlterPriv, mysql.DropPriv, mysql.IndexPriv, mysql.CreateViewPriv,mysql.InsertPriv, mysql.UpdatePriv, mysql.DeletePriv 这些权限时候,返回失败;而其他权限直接返回成功,比如 Super、Select、Create User 等等。

通过以下方法实现用户授权自己没有的 Super、Select、Create User 等权限:

  1. grant Super、Select、Create User on *.* to ‘user’@‘host’,参考日志,*.* 会使得 [rawDB=] [rawTable=]
  2. 如果当前 CurrentDB 不为空,那么验证权限的 db 就被设置为 CurrentDB
  3. 然后会对当前用户对 CurrentDB 进行 priv 权限验证
  4. 而 CurrentDB 为 InformationSchema,priv 为 Super、Select、Create User,上面验证直接返回通过
  5. 那么权限验证成功
  6. 给 ‘user’@‘host’ 添加 Super、Select、Create User 权限成功,而这些 priv 权限赋权用户本来是没有的,它实现了对未拥有权限进行授权。

创建用户 ‘abc’@’%’,给 Select,GRANT 权限:

``` mysql> create user 'abc'@'%' identified by '123'; Query OK, 0 rows affected (0.01 sec)

mysql> grant select on test.* to 'abc'@'%' with grant option;
Query OK, 0 rows affected (0.01 sec)

mysql> show grants for 'abc'@'%'; +-------------------------------------------------------+ | Grants for abc@% | +-------------------------------------------------------+ | GRANT USAGE ON . TO 'abc'@'%' | | GRANT Select ON test.* TO 'abc'@'%' WITH GRANT OPTION | +-------------------------------------------------------+ 2 rows in set (0.00 sec) ```

使用 ‘abc’@’%’ 登录数据库,可以看到它拥有的权限:

``` mysql> show grants; +-------------------------------------------------------+ | Grants for User | +-------------------------------------------------------+ | GRANT USAGE ON . TO 'abc'@'%' | | GRANT Select ON test.* TO 'abc'@'%' WITH GRANT OPTION | +-------------------------------------------------------+ 2 rows in set (0.00 sec)

mysql> show databases; +--------------------+ | Database | +--------------------+ | INFORMATION_SCHEMA | | test | +--------------------+ 2 rows in set (0.00 sec) ```

尝试创建新用户,但是被禁止,因为 Create User 权限,所以失败符合预期;给自己添加 Create User 权限失败也符合预期:

mysql> create user 'efg'@'%'; ERROR 1227 (42000): Access denied; you need (at least one of) the CREATE User privilege(s) for this operation mysql> grant CREATE User on test.* to 'abc'@'%'; ERROR 8121 (HY000): privilege check fail

执行以下方法就可以顺利添加 Create User,并能创建新用户:

``` mysql> use information_schema Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A

Database changed mysql> grant CREATE User on . to 'abc'@'%'; Query OK, 0 rows affected (0.01 sec)

mysql> show grants; +-------------------------------------------------------+ | Grants for User | +-------------------------------------------------------+ | GRANT Create User ON . TO 'abc'@'%' | | GRANT Select ON test.* TO 'abc'@'%' WITH GRANT OPTION | +-------------------------------------------------------+ 2 rows in set (0.00 sec) mysql> create user 'efg'@'%'; Query OK, 0 rows affected (0.01 sec) ```

问题原因

出现问题的根本原因应该还是在这一步:

func collectVisitInfoFromGrantStmt(sctx sessionctx.Context, vi []visitInfo, stmt *ast.GrantStmt) []visitInfo { // To use GRANT, you must have the GRANT OPTION privilege, // and you must have the privileges that you are granting. dbName := stmt.Level.DBName tableName := stmt.Level.TableName // 这里就是出现问题的原因 if dbName == "" { dbName = sctx.GetSessionVars().CurrentDB } vi = appendVisitInfo(vi, mysql.GrantPriv, dbName, tableName, "", nil) ... }

TiDB 授权分为三种类型:

| 类型 | 说明 | | ---------------- | ------- | | GrantLevelGlobal | *.* | | grantDBLevel | test.* | | GrantLevelTable | test.t1 |

对于 GrantLevelGlobal 形式,dbName 为空,就会出现前面的问题。所以这里就要加判断,对于 GrantLevelGlobal 情形保持 dbName 为空即可。

但是这还不能解决所有问题,以 MySQL 5.7 做对比,执行以下语句:

``` Server version: 5.7.36-log MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners.

Type 'help;' or '\"h' for help. Type '\"c' to clear the current input statement.

mysql> grant select on * to 'bcd'@'%'; ERROR 1046 (3D000): No database select ```

在 tidb 上,用户却赋权成功,不过 GRANT Select ON .* TO ‘abc’@’%’ 这是一个无效的权限。

``` Type 'help;' or '\"h' for help. Type '\"c' to clear the current input statement.

mysql> grant select on * to 'abc'@'%';
Query OK, 0 rows affected (0.01 sec)

mysql> show grants for 'abc'@'%'; +---------------------------------+ | Grants for abc@% | +---------------------------------+ | GRANT USAGE ON . TO 'abc'@'%' | | GRANT Select ON .* TO 'abc'@'%' | +---------------------------------+ 2 rows in set (0.00 sec)

mysql> select @@version; +---------------------+ | @@version | +---------------------+ | 5.7.25-TiDB-v4.0.11 | +---------------------+ 1 row in set (0.00 sec) ```

TIDB 行为在于对 grant select on * to ‘abc’@’%’ 语句,解析为类型 grantDBLevel,而 [rawDB=] [rawTable=],* 被改写为 CurrentDB.* ,所以 CurrentDB 为空时候出现上面 GRANT Select ON .* TO ‘abc’@’%’ 权限情况:

case *ast.GrantStmt: if b.ctx.GetSessionVars().CurrentDB == "" && raw.Level.DBName == "" { // if raw.Level.Level == ast.GrantLevelTable { if raw.Level.Level == ast.GrantLevelTable || raw.Level.Level == ast.GrantLevelDB { return nil, ErrNoDB } } b.visitInfo = collectVisitInfoFromGrantStmt(b.ctx, b.visitInfo, raw)

在这里添加检查 GrantLevelDB 情况,直接返回报错,跟 MySQL 行为保持一致。

总结

  1. 修改buildSimple 函数,在 GrantLevelDB 情况下,db 为空时候保持与 MySQL 一致直接报错。

func (b *PlanBuilder) buildSimple(node ast.StmtNode) (Plan, error) { p := \&Simple{Statement: node} switch raw := node.(type) { … case *ast.GrantStmt: if b.ctx.GetSessionVars().CurrentDB == “” && raw.Level.DBName == “” { // if raw.Level.Level == ast.GrantLevelTable { if raw.Level.Level == ast.GrantLevelTable || raw.Level.Level == ast.GrantLevelDB { return nil, ErrNoDB } } b.visitInfo = collectVisitInfoFromGrantStmt(b.ctx, b.visitInfo, raw) …}

  1. 修改 collectVisitInfoFromGrantStmt 函数,在 GrantLevelGlobal 情况下,不设置 dbName = sctx.GetSessionVars().CurrentDB。

func collectVisitInfoFromGrantStmt(sctx sessionctx.Context, vi []visitInfo, stmt *ast.GrantStmt) []visitInfo { // To use GRANT, you must have the GRANT OPTION privilege, // and you must have the privileges that you are granting. dbName := stmt.Level.DBName tableName := stmt.Level.TableName if dbName == "" && (stmt.Level.Level == ast.GrantLevelTable || stmt.Level.Level == ast.GrantLevelDB){ dbName = sctx.GetSessionVars().CurrentDB } vi = appendVisitInfo(vi, mysql.GrantPriv, dbName, tableName, "", nil) ...}

其他

revoke 可能存在类型问题。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论