所以我们执行以下语句,创建一个用户 messi,并且只赋予所有数据库上的 SELECT 权限:

接着用刚刚创建的账号登录 MySQL,执行如下操作:

  1. mysql> CREATE TABLE `chelsea` (`id` int, `goal` int);
  2. Query OK, 0 rows affected (0.01 sec)
  3. mysql> insert into chelsea values(2, 3);
  4. Query OK, 1 row affected (0.01 sec)
  5. mysql> select * from chelsea;
  6. +------+------+
  7. | id | goal |
  8. +------+------+
  9. | 2 | 3 |
  10. +------+------+
  11. 1 row in set (0.00 sec)

这是怎么一回事呢? 明明在创建用户的时候只赋予了 SELECT 权限,竟然可以执行 INSERT 操作了。

Super 权限

Super 权限相当于 Linux 的 Root 权限,但是它能够为所欲为吗?

  1. mysql> use performance_schema;
  2. mysql> CREATE TABLE `chelsea` (`id` int, `goal` int);
  3. ERROR 1142 (42000): CREATE command denied to user 'root'@'127.0.0.1' for table 'chelsea'

我们尝试在 performance_schema 表中创建一张表,可是看到 root 用户被无情的告知权限不足。

以上述两个问题为引,这篇文章简单介绍一下 MySQL 的权限体系。

官方文档对权限有比较详细的描述,为了方便我把其中的表格列在下面。第一列表示所有的权限,可以在 Grant 语句中指定的,第二列是对应权限存储在系统数据库 mysql 几张表中的定义,第三列表示权限作用的范围,Global(Server administration)对应 mysql.user 表,Database 对应 mysql.db 表,Tables 对应 mysql.tables_priv 表,Columns 对应 mysql.columns_priv 表,Stored routines 对应 mysql.procs_priv 表。

赋予对应用户相应的权限,会根据不同的语法存储到不同的表中,以链接中官方文档中的语句为例:

Global Privileges

  1. GRANT ALL ON *.* TO 'someuser'@'somehost';
  2. GRANT SELECT, INSERT ON *.* TO 'someuser'@'somehost';

其中 *.* 表示所有数据的所有表,对应的权限会保存在 mysql.user 表中,和 user 相关联。

Database Privileges

  1. GRANT ALL ON mydb.* TO 'someuser'@'somehost';
  2. GRANT SELECT, INSERT ON mydb.* TO 'someuser'@'somehost';

其中 mydb.* 表示 mydb Database 下的所有表,对应的权限会保存在 mysql.db 表中,和 db 相关联。

Table Privileges

  1. GRANT ALL ON mydb.mytbl TO 'someuser'@'somehost';
  2. GRANT SELECT, INSERT ON mydb.mytbl TO 'someuser'@'somehost';

对应的权限保存在 mysql.tables_priv 中,和 db , user 关联。

Column Privileges

  1. GRANT SELECT (col1), INSERT (col1,col2) ON mydb.mytbl TO 'someuser'@'somehost';

对应的权限保存在 mysql.tables_priv 中,和 db, table, user 关联。

Stored Routine Privileges

  1. GRANT CREATE ROUTINE ON mydb.* TO 'someuser'@'somehost';
  2. GRANT EXECUTE ON PROCEDURE mydb.myproc TO 'someuser'@'somehost';

对应的权限保存在 mysql.procs_priv 中,和 routine_name, db,user 关联。

认证过程

整体认证的思路比较简单,对于权限的判断自然是自上而下的,假如一个用户有对某个数据库的写权限,自然不必继续判断对该数据库下的某个表是否有写权限。 从上述存储的表中可以查到对应的权限,然后和 want_access 进行位操作,判断是否包含了 want_access 需要的全部权限。 考虑一下这种情况,假如一个用户对某个数据库只有SELECT 权限,但是对该数据库其中的一张表只有 INSERT 权限,假如对表请求的 want_access 是 3 (二进制 11 表示 SELECT 和 INSERT 权限),自上而下先先判断数据库的权限,无法满足,接着再判断表的权限,依然无法满足。但是数据库的 SELECT 权限实际上表示对表也有 SELECT 权限,只是没有保存到mysql.tables_priv 表中罢了。所以在自上而下认证的过程中需要把上级已经获得的权限传递给下级。权限的使用频率非常高,如果每次都从数据库中查找效率太低,MySQL 将其缓存起来,在早期月报中就讨论过权限的缓存,可以参考。

下面从源码角度看一下上述过程是怎么实现的,主要的函数有两个,check_access 判断 Global 和 Database 级别, check_grant 判断 Table 级别,一般会先调用 check_access 再接着调用 check_grant ,对于 Column 级别的需要查表判断对应列是否存在等,暂且不讨论,判断的原理都相似。

其中 save_priv 就是传递给下级的权限,一般会在 check_grant 中使用。在函数开头就初始化为 0.

  1. if ((db != NULL) && (db != any_db))
  2. const ACL_internal_schema_access *acces
  3. ...
  4. }

这部分是对 Performance_schema 和 Informantion_schema 判断的逻辑,下一节会详细介绍。

  1. if ((sctx->master_access & want_access) == want_access)
  2. {
  3. ...
  4. if(..)
  5. *save_priv|= sctx->master_access | db_access;
  6. *save_priv|= sctx->master_access;
  7. DBUG_RETURN(FALSE);
  8. }

sctx->master_access 是从 mysql.user 表中获得的 Global 级别的权限,在用户和数据库建立连接就会初始化,上述代码表示全局的级别已经满足了 want_access 申请的权限。由于还要调用 check_grant , 在末尾把全局权限放到 save_priv 中。

  1. if (((want_access & ~sctx->master_access) & ~DB_ACLS) ||
  2. (! db && dont_check_global_grants))
  3. {
  4. ...
  5. DBUG_RETURN(TRUE);
  6. }

DB_ACLS 是一个宏定义,表示 db 级别的所有权限集合,根据判断条件来看,如果申请的权限没有全部在 sctx->master_access 中满足,并且也不属于 DB_ACLS 的一种,那么认为是无法获得的。或者是传入参数 db 为空,并且参数 dont_check_global_grants 为 true,也返回校验失败。这个逻辑还没有走到过,暂且记下。

  1. if (db == any_db)
  2. {
  3. /*
  4. Access granted; Allow select on *any* db.
  5. [out] *save_privileges= 0
  6. */
  7. DBUG_RETURN(FALSE);
  8. }

这个是处理一些通用的情况,不涉及具体的 db。

  1. if (db && (!thd->db || db_is_pattern || strcmp(db,thd->db)))
  2. db_access= acl_get(sctx->get_host()->ptr(), sctx->get_ip()->ptr(),
  3. sctx->priv_user, db, db_is_pattern);
  4. else
  5. db_access= sctx->db_access;

到这里 Global 级别就判断完了,thd->db 表示当前的数据库是哪一个,也就是执行了 use db 命令之后切换的数据库,切换之后该数据的权限会放到 sctx->db_access 中,上述判断就是如果 db 不是当前 db,就从缓存里面查找。

  1. db_access= (db_access | sctx->master_access);
  2. *save_priv|= db_access;

传递 db_access 下去。

  1. if ( (db_access & want_access) == want_access ||
  2. (!dont_check_global_grants &&
  3. need_table_or_column_check))
  4. {
  5. DBUG_RETURN(FALSE);
  6. }
  7. ...
  8. DBUG_RETURN(TRUE);

表示 db_access 已经可以满足 want_access 或者需要 table/column 级别的校验。如果上述校验都没有通过,则返回校验失败。

仔细看完 check_access 函数,check_grant 就相对简单一些, 看下主要逻辑

问题分析

再来看一下文章开头说的 test Database 权限问题,我们执行的 grant 语句相当于是给 mysql.user 表增加一条记录,是全局级别的。根据上述判断逻辑,Global 的权限满足不了,就要去 mysql.db 中判断,查一下很容易就可以发现,对于任意的用户都可以在 test Database 上增删改查。

  1. mysql> select * from db\G
  2. *************************** 1. row ***************************
  3. Db: test
  4. User:
  5. Select_priv: Y
  6. Insert_priv: Y
  7. Update_priv: Y
  8. Delete_priv: Y
  9. ...

因为 test 是系统初始化的数据库,意图是让更多的用户可以使用,其实这个也可以在 mysql_system_tables_data.sql 中找到一条记录,赋予了 test Database 权限。

  1. -- Fill "db" table with default grants for anyone to
  2. -- access database 'test' and 'test_%' if "db" table didn't exist
  3. CREATE TEMPORARY TABLE tmp_db LIKE db;
  4. INSERT INTO tmp_db VALUES ('%','test','','Y','Y','Y','Y','Y','Y','N','Y','Y','Y','Y','Y','Y','Y','Y','N','N','Y','Y');
  5. INSERT INTO tmp_db VALUES ('%','test\_%','','Y','Y','Y','Y','Y','Y','N','Y','Y','Y','Y','Y','Y','Y','Y','N','N','Y','Y');
  6. INSERT INTO db SELECT * FROM tmp_db WHERE @had_db_table=0;
  7. DROP TABLE tmp_db;
  1. grant SELECT on test.* to 'messi'@'%';

这样 INSERT 语句就会失败,因为在查找的时候,并不是随意找一个可以匹配的,而是找最匹配的一个,看看是不是有需要的权限。

这两个系统表比较特殊,Performance_schema 只有表结构定义文件,没有数据文件,数据来自 mysql 表,而 Information_schema 表连表结构定义都没有,当需要查询的时候在内存中构造。所以对于这两个表的存取权限就是独立的一套机制。同样分为 db 级别和 table 级别,在 check_access 和 check_grant 中调用。

ACL_internal_shcema_access

ACL_internal_shcema_access 是一个父类,有两个子类分别表示两种数据库,类图如下:

校验的时候首先根据传入的 db 名称获得对应的子类:

  1. const ACL_internal_schema_access *access;
  2. access= get_cached_schema_access(grant_internal_info, db);

然后再调用子类的 check 函数完成校验,对于 Information_schema 表,只允许 SELECT 权限,如果申请其它权限并且在 DB_ACL 宏定义中,那么就继续从 table 级别判断,否则的话就拒绝。

对于 Performance_schema 表,函数里表述的也非常清楚,定义了一个变量 always_forbbiden , 如果申请的权限全部包括在其中,就拒绝,否则走 table 级别判断。源码中屏蔽的权限有:

  1. const ulong always_forbidden= /* CREATE_ACL | */ REFERENCES_ACL
  2. | INDEX_ACL | ALTER_ACL | CREATE_TMP_ACL | EXECUTE_ACL
  3. | CREATE_VIEW_ACL | SHOW_VIEW_ACL | CREATE_PROC_ACL | ALTER_PROC_ACL
  4. | EVENT_ACL | TRIGGER_ACL ;

同样的,ACL_internal_table_access 也是父类,但是却有众多子类,使用方式和上述 schema_access 有较大区别。校验首先根据 db 名称和 table 名称查找对应的 ACL_internal_table_access 子类,查找过程分为两步:

  1. 根据 db 名称找到对应的 ACL_internal_schema_access
  2. 调用 ACL_internal_schema_access 的 look_up 方法查找

其中 IS_internal_schema_access 的 look_up 非常简单,直接返回 NULL,表示 information_schema 不支持 table 级别的校验。

相对 PFS_internal_schema_access 复杂一些,首先根据 table name 去查 PFS_engine_table_share,这个类里面有对应 table 的 acl 信息:

  1. const ACL_internal_table_access *
  2. PFS_internal_schema_access::lookup(const char *name) const
  3. {
  4. const PFS_engine_table_share* share;
  5. share= PFS_engine_table::find_engine_table_share(name);
  6. if (share)
  7. return share->m_acl
  8. ...
  9. return &pfs_unknown_acl
  10. }

而 PFS_engine_table::find_engine_table_share(name) 这个函数是根据 name 从一个静态数组 all_shars 中比较获取, 而 all_share 的初始化是从不同的类中的静态成员变量 m_share 中获取,以表 performance_schema.user 为例,有一个类 table_users :

  1. /** Table PERFORMANCE_SCHEMA.USERS. */
  2. class table_users : public cursor_by_user
  3. {
  4. public:
  5. /** Table share */
  6. static PFS_engine_table_share m_share;
  7. ...

其实 performance_schema 中的每一张表都对应一个类,它们有共同的父类,继承结构查看。而每一个类中都有一个静态变量 m_share,编译时就会初始化,仍然以 table_users 为例:

其中 pfs_truncatable_acl 就是我们需要的 ACL_internal_table_access 具体的子类,它像 ACL_internal_shcema_access 的校验一样,在 check 函数里定义了 always_forbidden 变量,代表这个类型的权限都被拒绝。这里权限并不是每一个表对应一种,代码中定义几种不同类型的权限,提供给所有的表去使用,看一下类图: MySQL · 源码分析 · 权限浅析 - 图1 如果想知道具体某个表的权限,代码里查一下就清楚了。所以其实 performance_schema 中 table 的权限都是写死在代码里的(显然 super 用户也无能为力)。

问题分析

最后我们看下文章开头提出的问题,super 用户无法在 performance_schema 中创建一个表,其实很明显,一个新创建的表名是在代码中是没有定义的,所以根本找不到对应的 PFS_engine_table_share, 看上面的 look_up 代码,返回的是 pfs_unknown_acl ,而这个类的 always_forbidden 变量屏蔽了 CREATE 权限,自然 Super 用户就没办法了~(PS,可以试验一下 DROP 一个现有的表,重新 CREATE 是没问题的)