动态数据脱敏

云原生数据仓库AnalyticDB PostgreSQL版新增了动态数据脱敏(Dynamic Data Masking)功能,并提供多种脱敏函数,支持设置列级别的数据脱敏,且支持指定用户匹配脱敏策略。数据脱敏能够根据不同的用户,应用不同的脱敏函数将原始数据以脱敏后的形式展示,从而实现对敏感隐私数据的保护,提高数据的安全性。本文介绍如何使用动态数据脱敏功能。

背景信息

数据脱敏是行之有效的数据库隐私保护方案之一,可以在一定程度上限制非授权用户对隐私数据的窥探。动态数据脱敏是一种通过定制化脱敏策略实现对隐私数据保护的技术,可以在不改变原始数据的前提下有效解决非特权用户对敏感信息访问的问题。

要使用动态脱敏功能,需要先对每个敏感列定义一个脱敏函数,AnalyticDB PostgreSQL版提供了常见的脱敏函数,可以涵盖大部分脱敏场景,也支持用户使用自定义的脱敏函数。将脱敏函数和敏感列进行绑定后,其他用户查询的结果就从原始值变成脱敏函数的输出值。例如,电话号码在数据库中存储的是明文13900001111。特权用户(例如,表的Owner、具备RDS_SUPERUSER权限的账号)查询后看到的是明文,而其他用户则会看到脱敏后的结果,示例为139****1111

通过CREATE REDACTION POLICY命令可以为一张表的某些敏感字段指定脱敏函数,同时定义一个表达式,决定何时进行脱敏。脱敏策略被创建后,可以使用ALTER REDACTION POLICY对其进行修改,或者使用DROP REDACTION POLICY删除脱敏策略。您也可以通过系统视图REDACTION_POLICIESREDACTION_COLUMNS,更加方便地查看脱敏策略及脱敏列信息。

前提条件

小版本为v6.6及以上的AnalyticDB PostgreSQL 6.0版实例和小版本为v7.0.3及以上AnalyticDB PostgreSQL 7.0版实例支持动态数据脱敏。如何查看和升级内核小版本,请参见查看内核小版本版本升级

说明

Serverless版本实例暂不支持。

使用约束

  • 只有表的Owner和具备RDS_SUPERUSER权限的账号具有创建、修改和删除脱敏策略的权限。

  • 无论定义任何脱敏策略,表的Owner和具备RDS_SUPERUSER权限的账号总能看到明文数据。

  • 脱敏数据支持通过COPY TOINSERT INTO导出,但由于脱敏的不可逆性,针对脱敏数据的二次运算无任何实际意义。

  • 仅支持在普通表创建动态数据脱敏策略,不支持为系统表、外表、临时表、UNLOGGED表以及视图、物化视图创建脱敏策略。

  • 表和脱敏策略一一对应。一个脱敏策略是表所有脱敏列的集合,可以给一张表的多个列指定不同的脱敏函数,且支持为不同列设置不同的脱敏规则。单个列只能指定一个脱敏函数。

  • 建议只对必要的敏感列进行脱敏,脱敏后的数据参与计算无任何意义。

使用方法

CREATE REDACTION POLICY

该命令定义指定表的脱敏策略。

语法

CREATE REDACTION POLICY <policy_name> ON <table_name>
    [ WHEN (<when_expression>) ]
    [ ADD COLUMN <column_name> WITH <mask_function_name> ( [ argument [, ...] ] )] [, ... ];

参数说明

参数

说明

policy_name

脱敏策略名称。

table_name

需要脱敏的表名。

WHEN (<when_expression>)

WHEN子句指定一个脱敏策略生效的表达式。仅当此表达式为真时,脱敏策略生效。

通常,采用WHEN子句来限定脱敏策略的生效用户范围,具有较严格的约束,约束如下:

  • 表达式的结果必须是Boolean类型,类似“1+1”的表达式无法做为生效表达式。

  • 表达式可以是AND、OR连接的多个子表达式。

  • 子表达式可以是IN、NOT IN表达式。

  • 当脱敏策略永远成立时,即对所有用户均生效,建议使用表达式(1=1)创建脱敏策略。

  • 当WHEN子句缺失时,脱敏策略默认不生效,需用户手动指定WHEN子句表达式。

column_name

应用脱敏策略的表的列名。

mask_function_name

脱敏函数名。

脱敏函数决定了如何处理脱敏列,脱敏函数的返回值类型必须和脱敏列类型一致。

argument

脱敏函数的参数列表。

说明

AnalyticDB PostgreSQL版提供丰富的内置脱敏函数,也支持使用SQL、PL/PGSQL语言创建自定义脱敏函数。

示例

创建一张简单的表,并使用自定义函数对其中一列进行脱敏,此脱敏规则只对bob生效。

-- 准备一张表。
CREATE TABLE test_mask(id INT, name TEXT);

-- 创建自定义函数。
CREATE OR REPLACE FUNCTION mask_text(input_text text) RETURNS text AS $$
BEGIN
  RETURN REPEAT('*', CHAR_LENGTH(input_text));
END;
$$ LANGUAGE plpgsql IMMUTABLE;

-- 配置脱敏策略。
CREATE REDACTION POLICY test_mask_policy ON test_mask
  WHEN (CURRENT_USER = 'bob')
  ADD COLUMN name WITH mask_text(name);

也可以使用AnalyticDB PostgreSQL版内置的脱敏函数进行配置,这里WHEN表达式对所有用户都生效,设置为true。内置脱敏函数,请参见附录:脱敏函数

CREATE REDACTION POLICY test_mask_policy ON test_mask
  WHEN (true)
  ADD COLUMN name WITH mask_full_str(name);

ALTER REDACTION POLICY

该命令可以修改表的脱敏策略。

语法

  • 修改脱敏策略生效表达式。

    ALTER REDACTION POLICY <policy_name> ON <table_name> WHEN (<new_when_expression>);
  • 使脱敏策略生效或失效。

    ALTER REDACTION POLICY <policy_name> ON <table_name> ENABLE | DISABLE;
  • 重命名脱敏策略名称,脱敏策略不变。

    ALTER REDACTION POLICY <policy_name> ON <table_name> RENAME TO <new_policy_name>;
  • 修改脱敏列,包括新增、修改、删除脱敏列。

    ALTER REDACTION POLICY policy_name ON table_name 
        Action;

    其中,脱敏列操作Action可以是以下子句之一.

    ADD COLUMN <column_name> WITH <function_name> ( arguments )
      | ALTER COLUMN <column_name> WITH <function_name> ( arguments )
      | DROP COLUMN <column_name>

参数说明

参数

说明

policy_name

待修改的脱敏策略名称。

table_name

待修改脱敏策略的表名。

new_when_expression

脱敏策略新的生效表达式。

ENABLE | DISABLE

当前脱敏策略是否生效。

  • ENABLE:使脱敏策略重新生效。

  • DISABLE:使脱敏策略失效。

new_policy_name

新的脱敏策略名称。

column_name

应用脱敏策略的表的列名。

  • 如果新增脱敏列,则指定列尚未绑定任何脱敏函数。

  • 如果修改或删除脱敏列,则指定列为已存在的脱敏列。

function_name

脱敏函数名。

argument

脱敏函数的参数列表。

示例

  • 修改脱敏策略生效表达式,使其对指定角色生效。

    ALTER REDACTION POLICY mask_emp ON employees WHEN(current_user in ('alice', 'bob'));
  • 修改脱敏策略生效表达式,使其对所有用户均生效(只有表的Owner能够看到明文数据)。

    ALTER REDACTION POLICY mask_emp ON employees WHEN(1=1);
  • 修改脱敏策略,使其失效或生效。

    ALTER REDACTION POLICY mask_emp ON employees DISABLE;
    ALTER REDACTION POLICY mask_emp ON employees ENABLE;
  • 重命名脱敏策略名称。

    ALTER REDACTION POLICY mask_emp ON employees RENAME TO new_mask_emp;
  • 新增脱敏列。

    ALTER REDACTION POLICY mask_emp_new ON emp ADD COLUMN name WITH mask_none(name);
  • 修改脱敏列的脱敏函数。

    ALTER REDACTION POLICY mask_emp_new ON emp ALTER COLUMN name WITH mask_none(name);
  • 删除已存在的脱敏列。

    ALTER REDACTION POLICY mask_emp_new ON emp DROP COLUMN name;

DROP REDACTION POLICY

DROP REDACTION POLICY命令删除指定表的脱敏策略。

语法

DROP REDACTION POLICY [ IF EXISTS ] <policy_name> ON <table_name>;

参数

参数

说明

IF EXISTS

如果待删除的脱敏策略不存在,则返回NOTICE,而不是报错。

policy_name

待删除的脱敏策略名称。

table_name

待删除脱敏策略的表名。

脱敏用户执行SQL

SELECT

动态脱敏对用户透明,因此无需修改业务逻辑,配置好脱敏策略后,查询中涉及到的敏感列都会自动进行脱敏。

例如,employeesphone列配置了脱敏策略,那么对敏感列的查询SELECT phone FROM employees;,如果触发脱敏条件,则会被自动改写成类似于SELECT mask_func(phone) FROM employees;的查询SQL。

同理,对脱敏列的计算(包括等值条件、JOIN等)也是对脱敏后的结果进行计算,因此不建议对脱敏列进行计算,没有实际意义。当进行如下查询时:

CREATE TABLE t1(phone TEXT);
SELECT employees.phone FROM employees JOIN t1 ON t1.phone = employees.phone;

SQL会被改写成类似于如下的查询:

-- 脱敏后的等价查询。
SELECT mask_func(employees.phone) FROM employees JOIN t1 ON t1.phone = mask_func(employees.phone);

因此JOIN的结果会不符合预期,为了使计算正确,可以参见脱敏范围设置参数。

INSERT/UPDATE/DELETE

通常而言,如果一张表对某用户脱敏,则该用户不具有INSERT,UPDATE和DELETE的权限,因为即使写入了明文数据,查询结果也是脱敏后的结果。因此,不建议给脱敏用户赋予除SELECT之外的权限。

  • INSERT

    INSERT插入明文数据,但查询出来的仍是脱敏后的数据。

    -- 假设employees表的phone列对当前用户脱敏。
    INSERT INTO employees (id, name, ssn, phone, birthday, salary, email) 
      VALUES (1, 'Sally Sample', '020-78-9345', '13900001111', '1961-02-02', 51234.34,
              'sally.sample@alibaba.com');
    
    -- INSERT之后进行查询。
    postgres=> SELECT * FROM employees;
     id |     name     |     ssn     |    phone    |  birthday  |   salary   |          email
    ----+--------------+-------------+-------------+------------+------------+--------------------------
      1 | Sally Sample | 020-78-9345 | 139****1111 | 1961-02-02 | $51,234.34 | sally.sample@alibaba.com
    (1 row)

    如上所示,对于脱敏用户即使写入明文,查询出来的phone列仍然是脱敏后的结果。

  • UPDATE

    UPDATE语句通常涉及到计算WHERE条件,但涉及脱敏列的计算会导致计算结果不正确,此时可以设置脱敏范围,保证计算的正确性。

    -- 假设employees表的phone列对当前用户脱敏。
    postgres=> UPDATE employees SET id = 2 WHERE phone = '13900001111';
    UPDATE 0
    
    -- 设置脱敏范围。
    postgres=> SET redaction.scope = 'query_except_equal';
    SET
    
    -- 再次进行UPDATE。
    postgres=> UPDATE employees SET id = 2 WHERE phone = '13900001111';
    UPDATE 1
  • DELETE

    DELETE和UPDATE类似,通常也涉及计算WHERE条件,因此脱敏用户如果要执行的DELETE语句涉及脱敏列,也应该设置脱敏范围

    -- 假设employees表的phone列对当前用户脱敏。
    postgres=> DELETE FROM employees WHERE phone = '13900001111';
    DELETE 0
    
    postgres=> SET redaction.scope = 'query_except_equal';
    SET
    
    postgres=> DELETE FROM employees WHERE phone = '13900001111';
    DELETE 1

查看脱敏策略

用户可以通过系统视图REDACTION_POLICIES和REDACTION_COLUMNS查看当前数据库中的脱敏策略及脱敏列。

  • REDACTION_POLICIES视图展示当前数据库内所有脱敏策略。

    REDACTION_POLICIES字段信息。

    名称

    类型

    描述

    schema_name

    name

    脱敏表的模式名。

    table_name

    name

    脱敏表的表名。

    policy_name

    name

    脱敏策略名称。

    enable

    boolean

    脱敏策略状态(开启、关闭)。

    expression

    text

    策略生效表达式。

  • REDACTION_COLUMNS视图展示当前数据库内所有脱敏列信息。

    REDACTION_COLUMNS字段信息。

    名称

    类型

    描述

    table_name

    name

    脱敏表的名称。

    column_name

    name

    脱敏列的名称。

    column_type

    name

    脱敏列的类型。

    function_info

    text

    脱敏函数信息。

脱敏范围

AnalyticDB PostgreSQL版支持两种脱敏范围:queryquery_except_equal,可以通过配置redaction.scope参数进行选择。其中query是默认级别,表示会将查询语句任意位置中出现过的脱敏列都进行脱敏处理。

例如,查询语句SELECT name, email, phone FROM employees WHERE email = 'sally.***@alibaba.com';中email、phone是脱敏列,在使用脱敏功能后,原始的查询语句会被改写成等同于SELECT name, masked(email), masked(phone) FROM employees WHERE masked(email) = 'sally.***@alibaba.com';的查询。从脱敏用户的视角看,脱敏列的行为类似于对敏感列进行过一次全改写UPDATE employees SET email = masked(email);。因此脱敏后的值不再适合参与计算,为了满足部分用户的计算需求,可以将脱敏范围设置为query_except_equal,计算中涉及到等值条件均不进行脱敏。设置方式如下:

SET redaction.scope = 'query_except_equal';
说明

SET redaction.scope = 'query_except_equal';是会话级别设置脱敏范围。您也可以使用ALTER DATABASE xxx SET redaction.scope = 'query_except_equal';进行数据库级别设置脱敏范围。

query_except_equal脱敏范围下,WHERE子句中的email列将不会进行脱敏,所以经过脱敏改写后,原始的查询语句会被改写成等同于SELECT name, masked(email), masked(phone) FROM employees WHERE email = 'sally.sample@alibaba.com';的查询。

使用示例

本文以公司的员工表employees为例,简要介绍数据脱敏全流程。其中,表的Owner是alice及其他用户bob和eve,表包含员工姓名、手机号、邮箱、SSN、薪资等隐私数据。

  1. 使用管理员用户连接数据库后,创建角色alice、bob、eve。

    CREATE ROLE alice PASSWORD 'password';
    CREATE ROLE bob PASSWORD 'password';
    CREATE ROLE eve PASSWORD 'password';
  2. 赋予alice、bob和eve当前数据库的模式权限。

    GRANT ALL ON SCHEMA PUBLIC TO alice,bob,eve;
  3. 切换至角色alice,创建表employees并插入数据。

    SET role alice;
    
    CREATE TABLE employees (id SERIAL PRIMARY KEY, name varchar(40) NOT NULL, ssn varchar(11) NOT NULL,
                            phone varchar(11), birthday date, salary money, email varchar(100));
    
    INSERT INTO employees (name, ssn, phone, birthday, salary, email)
    VALUES
    ( 'Sally Sample', '020-78-9345', '13900001111', '1961-02-02', 51234.34,
    'sally.***@alibaba.com'),
    ( 'Jane Doe', '123-33-9345', '13900002222', '1963-02-14', 62500.00,
    'jane.***@gmail.com'),
    ( 'Bill Foo', '123-89-9345', '13900003333','1963-02-14', 45350.00,
    'william.***@163.com');
  4. alice将表employees的读取权限授予bob和eve。

    GRANT SELECT ON employees to bob,eve;
  5. 创建脱敏策略mask_emp,仅alice可查看员工所有信息,bob和eve对员工手机号(phone列)、薪资(salary列)和邮箱(email列)数据不可见。需要创建3个脱敏函数:

    • phone:字符类型,采用内置脱敏函数mask_partial对中间4位脱敏成*

    • salary:数值类型,采用内置脱敏函数mask_full_num脱敏成0。

    • email:字符类型,采用内置脱敏函数mask_email对@之前的内容进行脱敏。

      CREATE REDACTION POLICY mask_emp ON employees WHEN (current_user IN ('bob', 'eve'))
        ADD COLUMN phone WITH mask_partial(phone, '****', 3, 4),
        ADD COLUMN salary WITH mask_full_num(salary),
        ADD COLUMN email WITH mask_email(email);
  6. 切换到bob和eve,查看员工表employees。

    SET ROLE bob;
    SELECT * FROM employees;
     id |     name     |     ssn     |    phone    |  birthday  | salary |          email
    ----+--------------+-------------+-------------+------------+--------+--------------------------
      2 | Jane Doe     | 123-33-9345 | 139****1111 | 1963-02-14 |  $0.00 | xxxxxxxx@gmail.com
      3 | Bill Foo     | 123-89-9345 | 139****2222 | 1963-02-14 |  $0.00 | xxxxxxxxxxx@163.com
      1 | Sally Sample | 020-78-9345 | 139****3333 | 1961-02-02 |  $0.00 | xxxxxxxxxxxx@alibaba.com
    (3 rows)
    
    SET ROLE eve;
    SELECT * FROM employees;
     id |     name     |     ssn     |    phone    |  birthday  | salary |          email
    ----+--------------+-------------+-------------+------------+--------+--------------------------
      1 | Sally Sample | 020-78-9345 | 139****1111 | 1961-02-02 |  $0.00 | xxxxxxxxxxxx@alibaba.com
      2 | Jane Doe     | 123-33-9345 | 139****2222 | 1963-02-14 |  $0.00 | xxxxxxxx@gmail.com
      3 | Bill Foo     | 123-89-9345 | 139****3333 | 1963-02-14 |  $0.00 | xxxxxxxxxxx@163.com
    (3 rows)
  7. 若需要bob也具有员工所有信息的查看权限,仅eve角色不可见,可以修改策略生效范围。

    SET ROLE alice;
    ALTER REDACTION POLICY mask_emp ON employees WHEN(current_user = 'bob');
  8. 切换到bob和eve,重新查看员工表employees。

    SET ROLE bob;
    SELECT * FROM employees;
     id |     name     |     ssn     |    phone    |  birthday  |   salary   |          email
    ----+--------------+-------------+-------------+------------+------------+--------------------------
      2 | Jane Doe     | 123-33-9345 | 13900001111 | 1963-02-14 | $62,500.00 | jane.***@gmail.com
      3 | Bill Foo     | 123-89-9345 | 13900002222 | 1963-02-14 | $45,350.00 | william.***@163.com
      1 | Sally Sample | 020-78-9345 | 13900003333 | 1961-02-02 | $51,234.34 | sally.***@alibaba.com
    (3 rows)
    
    SET ROLE eve;
    SELECT * FROM employees;
     id |     name     |     ssn     |    phone    |  birthday  | salary |          email
    ----+--------------+-------------+-------------+------------+--------+--------------------------
      1 | Sally Sample | 020-78-9345 | 139****1111 | 1961-02-02 |  $0.00 | xxxxxxxxxxxx@alibaba.com
      2 | Jane Doe     | 123-33-9345 | 139****2222 | 1963-02-14 |  $0.00 | xxxxxxxx@gmail.com
      3 | Bill Foo     | 123-89-9345 | 139****3333 | 1963-02-14 |  $0.00 | xxxxxxxxxxx@163.com
    (3 rows)
  9. 通过视图REDACTION_POLICIESREDACTION_COLUMNS查看当前脱敏策略mask_emp的详细信息。

    SELECT * FROM redaction_policies;
     schema_name | table_name | policy_name | enable |            expression
    -------------+------------+-------------+--------+----------------------------------
     public      | employees  | mask_emp    | t      | ("current_user"() = 'eve'::name)
    (1 row)
    
    SELECT * FROM redaction_columns;
     table_name | column_name | column_type |              function_info
    ------------+-------------+-------------+-----------------------------------------
     employees  | phone       | varchar     | mask_partial(phone, '****'::text, 3, 4)
     employees  | salary      | money       | mask_full_num(salary)
     employees  | email       | varchar     | mask_email(email)
    (4 rows)
  10. 将字符类型的ssn列脱敏成###-##-####,除了使用正则表达式脱敏函数mask_regexp外,还可以通过自定义函数实现。此处采用SQL语言定义脱敏函数mask_ssn,创建脱敏列时,只需要自定义脱敏的函数名和参数列表。

    SET ROLE alice;
    
    CREATE FUNCTION mask_ssn() RETURNS varchar AS
    $$
      SELECT '###-##-####'::varchar;
    $$
    LANGUAGE SQL;
    
    ALTER REDACTION POLICY mask_emp ON employees ADD COLUMN ssn WITH mask_ssn();
    
    SET ROLE eve;
    SELECT * FROM employees;
     id |     name     |     ssn     |    phone    |  birthday  | salary |          email
    ----+--------------+-------------+-------------+------------+--------+--------------------------
      1 | Sally Sample | ###-##-#### | 139****1111 | 2001-01-01 |  $0.00 | xxxxxxxxxxxx@alibaba.com
      2 | Jane Doe     | ###-##-#### | 139****2222 | 2001-01-01 |  $0.00 | xxxxxxxx@gmail.com
      3 | Bill Foo     | ###-##-#### | 139****3333 | 2001-01-01 |  $0.00 | xxxxxxxxxxx@163.com
    (3 rows)
  11. 当表内数据不需要脱敏时,可以删除脱敏策略mask_emp。

    SET ROLE alice;
    DROP REDACTION POLICY mask_emp ON employees;

附录:脱敏函数

函数

描述

参数

返回值类型

mask_none(column anyelement)

不作任何脱敏处理,仅内部测试用。

任意类型的列。

与入参column数据类型相同。

mask_tonull(column anyelement)

全脱敏成NULL。

任意类型的列。

NULL

mask_full_num(column anyelement)

数值类型全脱敏成0。

数值类型的列,支持INT、BIGINT、FLOAT、NUMERIC等。

与入参column数据类型相同。

mask_full_str(column anyelement)

字符类型全脱敏成固定字符串[redact]

定长或变长的字符类型,支持TEXT、VARCHAR、CHAR等。

与入参column数据类型相同。

mask_full_time(column anyelement)

时间类型全脱敏成00:00:00

TIME或TIME WITH TIME ZONE类型。

与入参column数据类型相同。

mask_full_timestamp(column anyelement)

时间戳或日期类型全脱敏成2001.01.01 00:00

DATE、TIMESTAMP或TIMESTAMP WITH TIME ZONE类型。

与入参column数据类型相同。

mask_full_bytea()

BYTEA类型全脱敏成固定BYTEA。

BYTEA类型。

mask_email(column anyelement, letter char default 'x')

脱敏电子邮箱。

  • column:定长或变长的字符类型,支持TEXT、VARCHAR、CHAR等。

  • letter:替换字符,默认值为'x'

与入参column数据类型相同。

mask_shuffle(column anyelement)

返回原数据的乱序版本,长度和原数据相同。

定长或变长的字符类型,支持TEXT、VARCHAR、CHAR等。

与入参column数据类型相同。

mask_random(column anyelement)

返回一个长度和原数据相同的随机字符串。

定长或变长的字符类型,支持TEXT、VARCHAR、CHAR等。

与入参column数据类型相同。

mask_regexp(column anyelement, reg text, replace_text text, pos INTEGER default 0, reg_len INTEGER default -1)

正则表达式匹配替换。

  • column:定长或变长的字符类型,支持TEXT、VARCHAR、CHAR等。

  • reg text:正则匹配的内容。

  • replace_text text类型,替换的内容。

  • pos int:匹配的起始位置,默认为0,表示从开头开始匹配。

  • reg_len int:匹配长度,默认为-1,表示匹配到结尾。

与入参column数据类型相同。

mask_partial(column anyelement, padding text, prefix INTEGER, suffix INTEGER)

脱敏替换部分内容。

  • column:定长或变长的字符类型,支持TEXT、VARCHAR、CHAR等。

  • padding text:替换的内容。

  • prefix int:前缀,表示前prefix个字符不脱敏。

  • suffix int:后缀,表示后suffix个字符不脱敏。

与入参column数据类型相同。

  • 示例一:使用正则表达式将字符串中的数值类型脱敏成#

    SELECT mask_regexp('本月营收为27412.45元'::text, '[\d+]', '#');
         mask_regexp
    ----------------------
     本月营收为#####.##元
    (1 row)
  • 使用mask_partial函数将以下字符串中间的具体数字脱敏成#

    SELECT mask_partial('本月营收为27412.45元'::text, '###.##', 5, 1);
        mask_partial
    --------------------
     本月营收为###.##元
    (1 row)