写点什么

TiDB 赋权问题

  • 2022 年 7 月 11 日
  • 本文字数:8251 字

    阅读完需:约 27 分钟

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


【是否原创】是


【首发渠道】TiDB 社区


【目录】


【正文】

复现问题

  1. 创建了一个用户 abc

  2. use information_schema

  3. 给新建的 abc 添加 insert 权限失败,提示权限验证失败


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


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 testDatabase changedmysql> grant insert on *.* to 'abc'@'%';Query OK, 0 rows affected (0.01 sec)
复制代码


只要在 information_schema,就会报错:


mysql> use information_schemaDatabase changedmysql> 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 ignore `mysql` 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_schemaDatabase changedmysql> 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 failmysql> 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 testDatabase changedmysql> 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 operationmysql> grant CREATE User on test.* to 'abc'@'%'; ERROR 8121 (HY000): privilege check fail
复制代码


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


mysql> use information_schemaReading table information for completion of table and column namesYou can turn off this feature to get a quicker startup with -A
Database changedmysql> 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 形式,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 itsaffiliates. Other names may be trademarks of their respectiveowners.
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。

其他

revoke 可能存在类型问题。


发布于: 刚刚阅读数: 2
用户头像

TiDB 社区官网:https://tidb.net/ 2021.12.15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
TiDB 赋权问题_故障排查/诊断_TiDB 社区干货传送门_InfoQ写作社区