受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

5.9. 行安全性策略 #

除了可通过GRANT使用的 SQL 标准权限系统之外,表还可以拥有行安全性策略,用来按用户限制普通查询可以返回哪些行,以及数据修改命令可以插入、更新或删除哪些行。这一特性也称为行级安全性。默认情况下,表没有任何策略,因此如果某个用户按照 SQL 权限系统拥有访问该表的权限,那么表中的所有行对查询或更新来说都是同等可用的。

当在表上启用行安全性时(使用ALTER TABLE ... ENABLE ROW LEVEL SECURITY),所有针对该表选择行或修改行的普通访问都必须得到某条行安全性策略的允许。(不过,表拥有者通常不受行安全性策略约束。)如果该表没有任何策略,则会采用一条默认拒绝策略,也就是说所有行都不可见,也不能被修改。作用于整张表的操作,例如TRUNCATEREFERENCES,不受行安全性约束。

行安全性策略可以针对特定命令、特定角色,或者同时针对两者。策略可以指定适用于ALL命令,或者适用于SELECTINSERTUPDATEDELETE。一条策略也可以分配给多个角色,并且正常的角色成员关系与继承规则同样适用。

要指定根据某条策略哪些行可见或可修改,需要提供一个返回布尔结果的表达式。对每一行来说,在计算来自用户查询的任何条件或函数之前,都会先计算这个表达式。(这条规则的唯一例外是leakproof函数,它们被保证不会泄露信息;优化器可能会选择在行安全性检查之前应用这类函数。)表达式结果不是true的行不会被处理。你还可以分别指定独立的表达式,以便独立控制哪些行可见,以及哪些行允许被修改。策略表达式作为查询的一部分运行,并使用执行该查询的用户的权限;不过,可以借助安全性定义者函数访问调用用户本来无权访问的数据。

超级用户以及带有BYPASSRLS属性的角色在访问表时总是会绕过行安全性系统。表拥有者通常也会绕过行安全性,不过表拥有者可以通过ALTER TABLE ... FORCE ROW LEVEL SECURITY选择让自己也受行安全性约束。

启用或禁用行安全性,以及向表添加策略,始终都只属于表拥有者的权限。

策略的创建可以使用CREATE POLICY命令,策略的修改 可以使用ALTER POLICY命令,而策略的删除可以使用 DROP POLICY命令。要为一个给定表启用或者禁用行 安全性,可以使用ALTER TABLE命令。

每条策略都有一个名称,而且同一张表可以定义多条策略。由于策略是表级对象,因此同一张表上的每条策略都必须具有唯一名称。不同的表则可以拥有同名策略。

当多条策略适用于某个给定查询时,它们会通过OR(对宽松策略,默认类型)或AND(对限制性策略)组合在一起。这里的OR行为,类似于某个角色拥有其所属全部角色权限的规则。下文还会进一步讨论宽松策略与限制性策略。

作为一个简单的示例,这里是如何在account关系上 创建一条策略以允许只有managers角色的成员能访问行, 并且只能访问它们账户的行:

CREATE TABLE accounts (manager text, company text, contact_email text);

ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;

CREATE POLICY account_managers ON accounts TO managers
    USING (manager = current_user);

上面的策略会隐式提供一个与其USING子句相同的WITH CHECK子句,因此该约束既作用于命令选中的行(也就是说,经理不能SELECTUPDATEDELETE属于其他经理的现有行),也作用于命令修改的行(也就是说,不能通过INSERTUPDATE创建属于其他经理的行)。

如果没有指定角色或者使用了特殊的用户名PUBLIC, 则该策略适用于系统上所有的用户。要允许所有用户访问users 表中属于他们自己的行,可以使用一条简单的策略:

CREATE POLICY user_policy ON users
    USING (user_name = current_user);

这个示例的效果和前一个类似。

为了对增加到表中的行使用与可见行不同的策略,可以组合多条策略。这一对策略将允许所有用户查看users表中的所有行,但只能修改他们自己的行:

CREATE POLICY user_sel_policy ON users
    FOR SELECT
    USING (true);
CREATE POLICY user_mod_policy ON users
    USING (user_name = current_user);

SELECT命令中,这两条策略会使用OR组合,最终效果就是可以选中所有行。在其他命令类型中,只有第二条策略适用,因此效果和之前相同。

也可以用ALTER TABLE命令禁用行安全性。禁用行安全性 不会移除定义在表上的任何策略,它们只是被简单地忽略。然后该表中的所有 行都是可见的并且可修改,服从于标准的 SQL 权限系统。

下面是一个更大的示例,展示这项特性如何用于生产环境。表 passwd模拟了一个 Unix 密码文件:

-- Simple passwd-file based example
CREATE TABLE passwd (
  user_name             text UNIQUE NOT NULL,
  pwhash                text,
  uid                   int  PRIMARY KEY,
  gid                   int  NOT NULL,
  real_name             text NOT NULL,
  home_phone            text,
  extra_info            text,
  home_dir              text NOT NULL,
  shell                 text NOT NULL
);

CREATE ROLE admin;  -- Administrator
CREATE ROLE bob;    -- Normal user
CREATE ROLE alice;  -- Normal user

-- Populate the table
INSERT INTO passwd VALUES
  ('admin','xxx',0,0,'Admin','111-222-3333',null,'/root','/bin/dash');
INSERT INTO passwd VALUES
  ('bob','xxx',1,1,'Bob','123-456-7890',null,'/home/bob','/bin/zsh');
INSERT INTO passwd VALUES
  ('alice','xxx',2,1,'Alice','098-765-4321',null,'/home/alice','/bin/zsh');

-- Be sure to enable row-level security on the table
ALTER TABLE passwd ENABLE ROW LEVEL SECURITY;

-- Create policies
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON passwd TO admin USING (true) WITH CHECK (true);
-- Normal users can view all rows
CREATE POLICY all_view ON passwd FOR SELECT USING (true);
-- Normal users can update their own records, but
-- limit which shells a normal user is allowed to set
CREATE POLICY user_mod ON passwd FOR UPDATE
  USING (current_user = user_name)
  WITH CHECK (
    current_user = user_name AND
    shell IN ('/bin/bash','/bin/sh','/bin/dash','/bin/zsh','/bin/tcsh')
  );

-- Allow admin all normal rights
GRANT SELECT, INSERT, UPDATE, DELETE ON passwd TO admin;
-- Users only get select access on public columns
GRANT SELECT
  (user_name, uid, gid, real_name, home_phone, extra_info, home_dir, shell)
  ON passwd TO public;
-- Allow users to update certain columns
GRANT UPDATE
  (pwhash, real_name, home_phone, extra_info, shell)
  ON passwd TO public;

对于任意安全性设置来说,重要的是测试并确保系统的行为符合预期。 使用上述的示例,下面展示了权限系统工作正确:

-- admin can view all rows and fields
postgres=> set role admin;
SET
postgres=> table passwd;
 user_name | pwhash | uid | gid | real_name |  home_phone  | extra_info | home_dir    |   shell
-----------+--------+-----+-----+-----------+--------------+------------+-------------+-----------
 admin     | xxx    |   0 |   0 | Admin     | 111-222-3333 |            | /root       | /bin/dash
 bob       | xxx    |   1 |   1 | Bob       | 123-456-7890 |            | /home/bob   | /bin/zsh
 alice     | xxx    |   2 |   1 | Alice     | 098-765-4321 |            | /home/alice | /bin/zsh
(3 rows)

-- Test what Alice is able to do
postgres=> set role alice;
SET
postgres=> table passwd;
ERROR:  permission denied for table passwd
postgres=> select user_name,real_name,home_phone,extra_info,home_dir,shell from passwd;
 user_name | real_name |  home_phone  | extra_info | home_dir    |   shell
-----------+-----------+--------------+------------+-------------+-----------
 admin     | Admin     | 111-222-3333 |            | /root       | /bin/dash
 bob       | Bob       | 123-456-7890 |            | /home/bob   | /bin/zsh
 alice     | Alice     | 098-765-4321 |            | /home/alice | /bin/zsh
(3 rows)

postgres=> update passwd set user_name = 'joe';
ERROR:  permission denied for table passwd
-- Alice is allowed to change her own real_name, but no others
postgres=> update passwd set real_name = 'Alice Doe';
UPDATE 1
postgres=> update passwd set real_name = 'John Doe' where user_name = 'admin';
UPDATE 0
postgres=> update passwd set shell = '/bin/xx';
ERROR:  new row violates WITH CHECK OPTION for "passwd"
postgres=> delete from passwd;
ERROR:  permission denied for table passwd
postgres=> insert into passwd (user_name) values ('xxx');
ERROR:  permission denied for table passwd
-- Alice can change her own password; RLS silently prevents updating other rows
postgres=> update passwd set pwhash = 'abc';
UPDATE 1

到目前为止,我们构造的所有策略都是宽松策略,也就是说,当多条策略都适用时,它们会通过OR布尔操作符组合起来。虽然宽松策略也可以构造得只在预期情况下允许访问行,但有时把宽松策略与限制性策略组合起来会更简单;后者是记录必须通过的策略,并且它们会通过AND布尔操作符组合。在上面的示例基础上,我们再增加一条限制性策略,要求管理员必须通过本地 Unix 套接字连接,才能访问passwd表中的记录:

CREATE POLICY admin_local_only ON passwd AS RESTRICTIVE TO admin
    USING (pg_catalog.inet_client_addr() IS NULL);

然后,由于这条限制性规则的存在,我们可以看到从网络连接进来的管理员将无法看到任何记录:

=> SELECT current_user;
 current_user
--------------
 admin
(1 row)

=> select inet_client_addr();
 inet_client_addr
------------------
 127.0.0.1
(1 row)

=> TABLE passwd;
 user_name | pwhash | uid | gid | real_name | home_phone | extra_info | home_dir | shell
-----------+--------+-----+-----+-----------+------------+------------+----------+-------
(0 rows)

=> UPDATE passwd set pwhash = NULL;
UPDATE 0

参照完整性检查,例如唯一约束或主键约束以及外键引用,总是会绕过行级安全性, 以确保数据完整性得到维护。在设计模式和行级策略时必须小心, 避免通过这类参照完整性检查形成隐蔽通道并泄露信息。

在某些场景下,确保没有应用行安全性很重要。例如在做备份时,如果行安全性静默地导致某些行被从备份中省略,那将是灾难性的。在这种情况下,可以把row_security配置参数设置为off。这本身并不会绕过行安全性;它的作用是,只要某个查询结果本会被策略过滤,就直接抛出错误。这样就可以调查并修复出错原因。

在上面的示例中,策略表达式只考虑了要被访问的行中的当前值。这是最简 单并且表现最好的情况。如果可能,最好设计行安全性应用以这种方式工作。 如果需要参考其他行或者其他表来做出策略决定,可以在策略表达式中通过 使用子SELECT或包含SELECT的函数 来实现。不过要注意这类访问可能会导致竞争条件,在不小心的情况下这可能 会导致信息泄露。作为一个示例,考虑下面的表设计:

-- definition of privilege groups
CREATE TABLE groups (group_id int PRIMARY KEY,
                     group_name text NOT NULL);

INSERT INTO groups VALUES
  (1, 'low'),
  (2, 'medium'),
  (5, 'high');

GRANT ALL ON groups TO alice;  -- alice is the administrator
GRANT SELECT ON groups TO public;

-- definition of users' privilege levels
CREATE TABLE users (user_name text PRIMARY KEY,
                    group_id int NOT NULL REFERENCES groups);

INSERT INTO users VALUES
  ('alice', 5),
  ('bob', 2),
  ('mallory', 2);

GRANT ALL ON users TO alice;
GRANT SELECT ON users TO public;

-- table holding the information to be protected
CREATE TABLE information (info text,
                          group_id int NOT NULL REFERENCES groups);

INSERT INTO information VALUES
  ('barely secret', 1),
  ('slightly secret', 2),
  ('very secret', 5);

ALTER TABLE information ENABLE ROW LEVEL SECURITY;

-- a row should be visible to/updatable by users whose security group_id is
-- greater than or equal to the row's group_id
CREATE POLICY fp_s ON information FOR SELECT
  USING (group_id <= (SELECT group_id FROM users WHERE user_name = current_user));
CREATE POLICY fp_u ON information FOR UPDATE
  USING (group_id <= (SELECT group_id FROM users WHERE user_name = current_user));

-- we rely only on RLS to protect the information table
GRANT ALL ON information TO public;

现在假设alice想修改slightly secret这条信息,但认为mallory不应该看到这一行的新内容,因此她这样做:

BEGIN;
UPDATE users SET group_id = 1 WHERE user_name = 'mallory';
UPDATE information SET info = 'secret from mallory' WHERE group_id = 2;
COMMIT;

这看起来是安全的;似乎不存在mallory能够看到secret from mallory这个字符串的窗口。不过,这里存在一个竞争条件。如果mallory正在并发执行例如:

SELECT * FROM information WHERE group_id = 2 FOR UPDATE;

并且她的事务处于READ COMMITTED模式,那么她就有可能看到 secret from mallory。这种情况会在她的事务恰好在alice之后到达 information中的那一行时发生。它会阻塞并等待 alice的事务提交,然后由于FOR UPDATE子句而取到更新后的行内容。 但是,对于来自users的隐式SELECT, 它不会取到更新后的行,因为该子SELECT没有 FOR UPDATE;相反,users中的那一行是用查询开始时取得的快照读取的。 因此,策略表达式测试的是mallory权限级别的旧值,并允许她看到更新后的行。

有多种方法能解决这个问题。一种简单的答案是在行安全性策略中的 子SELECT里使用SELECT ... FOR SHARE。 不过,这要求在被引用表(这里是users)上授予 UPDATE权限给受影响的用户,这可能不是我们想要的( 但是另一条行安全性策略可能被应用来阻止它们实际使用这个权限,或者 子SELECT可能被嵌入到一个安全性定义者函数中)。 还有,在被引用的表上过多并发地使用行共享锁可能会导致性能问题, 特别是表更新比较频繁时。另一种解决方案(如果被引用表上的更新 不频繁就可行)是在更新被引用表时对它取一个ACCESS EXCLUSIVE锁, 这样就没有并发事务能够检查旧的行值了。或者我们可以在提交对被引用表的更新 之后、在做依赖于新安全性情况的更改之前等待所有并发事务结束。

更多细节请见CREATE POLICYALTER TABLE

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。