MySQL 8.4 Reference Manual  /  Stored Objects  /  Stored Object Access Control

27.6 存储对象访问控制

存储程序(过程、函数、触发器和事件)和视图在使用前定义,并在引用时在安全上下文中执行,该上下文确定了存储对象的权限。存储对象的权限是由其DEFINER属性和SQL SECURITY特性控制的。

存储对象定义可以包含一个DEFINER属性,该属性指定一个 MySQL 账户。如果定义省略了DEFINER属性,则默认对象定义者是创建它的用户。

以下规则确定了可以指定为存储对象DEFINER属性的账户:

  • 如果您拥有SET_ANY_DEFINER特权,可以指定任何账户为DEFINER属性。如果账户不存在,会生成警告。另外,要将存储对象DEFINER属性设置为拥有SYSTEM_USER特权的账户,您必须拥有SYSTEM_USER特权。

  • 否则,只能指定自己的账户,或者使用CURRENT_USERCURRENT_USER()指定账户。您不能将定义者设置为其他账户。

创建存储对象时指定不存在的DEFINER账户将创建孤儿对象,这可能会产生负面影响;请参阅孤儿存储对象

对于存储程序(过程和函数)和视图,对象定义可以包含一个SQL SECURITY特性,指定执行对象的上下文为定义者或调用者上下文。如果定义省略了SQL SECURITY特性,缺省为定义者上下文。

触发器和事件没有SQL SECURITY特性,总是以定义者上下文执行。服务器自动根据需要 invoke 这些对象,因此没有调用用户。

定义者和调用者安全上下文不同:

  • 在定义者安全上下文中执行的存储对象,以其DEFINER属性指定的账户的权限执行。这些权限可能与调用用户的权限完全不同。调用者必须具有引用对象的适当权限(例如EXECUTE以调用存储过程或SELECT以从视图中选择),但是,在对象执行期间,调用者的权限将被忽略,只有定义者账户的权限有影响。如果定义者账户具有有限权限,对象的操作将相应地受到限制。如果定义者账户是高权限账户(例如,管理员账户),对象可以执行强大的操作无论谁调用它。

  • 在调用者安全上下文中执行的存储程序或视图只能执行调用者具有权限的操作。DEFINER属性对对象执行无影响。

考虑以下存储过程,它以SQL SECURITY DEFINER声明,以定义者安全上下文执行:

CREATE DEFINER = 'admin'@'localhost' PROCEDURE p1()
SQL SECURITY DEFINER
BEGIN
  UPDATE t1 SET counter = counter + 1;
END;

任何具有p1EXECUTE权限的用户可以使用CALL语句调用它。然而,当p1执行时,它以定义者安全上下文执行,因此以'admin'@'localhost'账户的权限执行。这账户必须具有p1EXECUTE权限,以及对表t1UPDATE权限。否则,过程将失败。

现在考虑以下存储过程,它与p1相同,但是其SQL SECURITY特性为INVOKER:

CREATE DEFINER = 'admin'@'localhost' PROCEDURE p2()
SQL SECURITY INVOKER
BEGIN
  UPDATE t1 SET counter = counter + 1;
END;

p1不同的是,p2以调用者安全上下文执行,因此以调用者的权限执行,无论DEFINER属性的值。p2将失败,如果调用者缺乏p2EXECUTE权限或对表t1UPDATE权限。

孤儿存储对象

孤儿存储对象是指其DEFINER属性命名的不存在账户:

  • 孤儿存储对象可以通过在对象创建时指定不存在的DEFINER账户来创建。

  • 现有的存储对象可以通过执行DROP USER语句或RENAME USER语句变成孤儿存储对象。

孤儿存储对象可能会出现以下问题:

  • 由于DEFINER账户不存在,对象可能在执行时不工作正常:

    • 对于存储程序,如果SQL SECURITY值为DEFINER但定义账户不存在,则在程序执行时会出现错误。

    • 对于触发器,不建议在账户存在之前激活触发器。否则,对于权限检查的行为是未定义的。

    • 对于事件,如果账户不存在,则在事件执行时会出现错误。

    • 对于视图,如果SQL SECURITY值为DEFINER但定义账户不存在,则在引用视图时会出现错误。

  • 对象可能会存在安全风险,如果不存在的DEFINER账户后续被重新创建用于与对象无关的目的。在这种情况下,账户adopt对象,并且具有适当权限,可以执行对象,即使不是预期的。

服务器实施以下账户管理安全检查,以防止可能无意地导致存储对象变成孤儿或导致当前孤儿存储对象被adopt的操作:

  • DROP USER语句失败,如果要删除的账户是任何存储对象的DEFINER属性命名的账户。 (即语句失败,如果删除账户将导致存储对象变成孤儿。)

  • RENAME USER语句失败,如果要重命名的账户是任何存储对象的DEFINER属性命名的账户。 (即语句失败,如果重命名账户将导致存储对象变成孤儿。)

  • CREATE USER语句失败,如果要创建的账户是任何存储对象的DEFINER属性命名的账户。 (即语句失败,如果创建账户将导致账户adopt当前孤儿存储对象。)

在某些情况下,可能需要故意执行那些帐户管理语句,即使它们将否失败。为了使这可能,用户拥有ALLOW_NONEXISTENT_DEFINER特权时,该特权将覆盖孤儿对象安全检查语句将成功而不是失败。

要获取MySQL安装中存储对象定义者的帐户信息,可以查询INFORMATION_SCHEMA

这个查询标识哪些INFORMATION_SCHEMA表格描述了具有DEFINER属性的对象:

mysql> SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.COLUMNS
       WHERE COLUMN_NAME = 'DEFINER';
+--------------------+------------+
| TABLE_SCHEMA       | TABLE_NAME |
+--------------------+------------+
| information_schema | EVENTS     |
| information_schema | ROUTINES   |
| information_schema | TRIGGERS   |
| information_schema | VIEWS      |
+--------------------+------------+

结果告诉你哪些表格可以查询以发现哪些存储对象DEFINER值存在,并且哪些对象具有特定的DEFINER值:

  • 要确定每个表格中存在的DEFINER值,可以使用这些查询:

    SELECT DISTINCT DEFINER FROM INFORMATION_SCHEMA.EVENTS;
    SELECT DISTINCT DEFINER FROM INFORMATION_SCHEMA.ROUTINES;
    SELECT DISTINCT DEFINER FROM INFORMATION_SCHEMA.TRIGGERS;
    SELECT DISTINCT DEFINER FROM INFORMATION_SCHEMA.VIEWS;

    查询结果对任何帐户的显示都是有意义的:

    • 如果帐户存在,删除或重命名它将导致存储对象变为孤儿。如果您计划删除或重命名帐户,请首先删除或重命名与其关联的存储对象或将其重新定义为具有不同的定义者。

    • 如果帐户不存在,创建它将导致它继承当前孤儿存储对象。如果您计划创建帐户,请考虑孤儿对象是否应该与之关联。如果不是,请将其重新定义为具有不同的定义者。

    要重新定义具有不同定义者的对象,可以使用ALTER EVENTALTER VIEW来直接修改事件和视图的DEFINER帐户。对于存储程序和函数,以及触发器,您必须删除对象并重新创建以将不同的DEFINER帐户分配给它。

  • 要确定哪些对象具有给定的DEFINER帐户,可以使用这些查询,替换感兴趣的帐户名称为user_name@host_name:

    SELECT EVENT_SCHEMA, EVENT_NAME FROM INFORMATION_SCHEMA.EVENTS
    WHERE DEFINER = 'user_name@host_name';
    SELECT ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_TYPE
    FROM INFORMATION_SCHEMA.ROUTINES
    WHERE DEFINER = 'user_name@host_name';
    SELECT TRIGGER_SCHEMA, TRIGGER_NAME FROM INFORMATION_SCHEMA.TRIGGERS
    WHERE DEFINER = 'user_name@host_name';
    SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS
    WHERE DEFINER = 'user_name@host_name';

    对于ROUTINES表格,查询包括ROUTINE_TYPE列,以便输出行区分存储程序或存储函数的DEFINER

    如果您搜索的帐户不存在,那么显示的对象都是孤儿对象。

为了减少存储对象创建和使用的风险,请遵循以下指南:

  • 不要创建孤儿存储对象,即对象的DEFINER属性命名了不存在的帐户。不要导致存储对象变为孤儿的帐户删除或重命名任何现有对象的DEFINER属性。

  • 对于存储程序或视图,使用SQL SECURITY INVOKER在对象定义中,以便它只能由具有适当权限的用户使用的操作。

  • 如果您使用具有SET_ANY_DEFINER特权的帐户创建定义上下文存储对象,请指定明确的DEFINER属性,指定一个拥有所需操作的帐户的名称。只有在绝对必要时才指定高权限的DEFINER帐户。

  • 管理员可以通过不授予用户SET_ANY_DEFINER特权来防止用户创建指定高权限的DEFINER帐户的存储对象。

  • 定位上下文对象应考虑它们可能访问的数据可能没有权限。某些情况下,您可以通过不授予未经授权的用户特定的权限来防止这些对象的引用:

    • 存储程序不能被没有EXECUTE特权的用户引用。

    • 视图不能被没有适当权限的用户引用(例如SELECT从中选择、INSERT到其中等)。

    然而,对于触发器和事件,因为它们总是以定义上下文执行。服务器自动 invoke 这些对象,以便在必要时执行用户不直接引用它们:

    • 触发器由与其关联的表的访问激活,即使是普通用户没有特殊权限的访问。

    • 事件在服务器上按计划执行。

    在这两种情况下,如果DEFINER帐户是高权限的,那么对象可能能够执行敏感或危险的操作。这仍然成立,即使撤销了创建对象的用户帐户的权限。管理员应该特别小心地授予用户对象创建权限。

  • 默认情况下,当具有SQL SECURITY DEFINER特性的存储程序被执行时,MySQL Server 不会为 MySQL 帐户在DEFINER子句中指定的帐户设置任何活动角色。只有在activate_all_roles_on_login系统变量启用时,MySQL Server 才会设置DEFINER用户授予的所有角色,包括强制角色。因此,默认情况下,不会检查通过角色授予的权限。对于存储程序,如果执行应该使用不同的角色,可以在程序体中执行SET ROLE来激活所需的角色。这需要小心,因为授予的角色权限可以被更改。