9.3 9.4 9.5 9.6 10 11 12
阿里云PostgreSQL 问题报告 纠错本页面

5.7. 行安全策略

除了SQL标准通过GRANT权限系统可用之外, 表可以有行安全策略,每个用户基础上, 限制通过正常查询或者插入,更新,或者数据修改命令删除的返回行。 该功能看做行级安全。缺省情况下,表没有任何策略, 因此如果用户按照SQL权限系统有对表的访问权限,在它内的所有行同样 可以用于查询或者更新。

当表上(使用ALTER TABLE ... ENABLE ROW LEVEL SECURITY)启用行安全时, 通过行安全策略允许正常访问表进行选择行或者修改行。(然而,表的所有者通常 不受限于行安全策略。)如果没有策略存在于表中,使用缺省拒绝策略,意味着 没有行可见或者被修改。适用整个表的操作, 比如TRUNCATEREFERENCES不受行安全限制。

行安全策略特定于命令,或者角色,或者两者。指定策略适用于ALL 命令,或者SELECTINSERTUPDATE, 或者DELETE。分配多个角色给特定策略,适用正常角色从属关系和继承规则。

指定哪些行按政策可见或者可修改,表达式是必需的,它返回一个布尔结果。 表达式将对先于任何条件或者来自用户查询的任何函数之前的每一行进行评估。 (这个规则的唯一例外是leakproof函数,它保证不泄露信息;该优化器 可能选择行安全检查前应用这个函数。)不处理那些表达式不返回true的行。 可以指定单独的表达式,用以提供对可见的行和允许进行修改的行的独立控制。 策略表达式作为查询部分运行,并具有用户运行查询权限, 尽管安全定义函数可用于访问对主叫用户不可见的数据。

当访问表的时候,超级用户以及具有BYPASSRLS属性的角色总是避开 行安全系统。表的所有者通常也绕过行安全,尽管表所有者可以选择从属于具有 ALTER TABLE ... FORCE ROW LEVEL SECURITY的行安全。

启用和禁用行安全,以及增加表策略,总是表所有者的特权。

使用CREATE POLICY命令创建策略, 使用ALTER POLICY命令修改策略, 使用DROP POLICY命令删除策略。 为了启用和禁用特定表的行安全性,使用ALTER TABLE命令。

Each policy has a name and multiple policies can be defined for a table. As policies are table-specific, each policy for a table must have a unique name. Different tables may have policies with the same name. 每个策略都有一个名字并且可以为表定义多个策略。 由于策略是表特定的,每一个表策略必须有唯一的名称。 不同的表可能有相同名称的策略。

当多个策略应用于特定查询时,使用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);

如果不指定任何角色,或者使用特定用户名PUBLIC, 那么策略应用于系统上的所有用户。允许所有用户访问用户 表中的行,可以使用简单策略:

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

相比较那些可见行为添加到表中行使用不同策略,可以使用WITH CHECK子句。 这个策略允许所有用户查看users表中的所有行,但是仅仅修改自己:

CREATE POLICY user_policy ON users
    USING (true)
    WITH CHECK (user = current_user);

使用ALTER TABLE命令禁用行安全性。 禁用行安全性不移除在表上定义的任何策略;他们只是忽略。 然后表上所有的行是可见的并且可修改的,属于标准的SQL权限系统。

下面是一个在生产环境中如何使用这个功能的例子。表passwd模拟 Unix密码文件:

-- 简单密码文件基本例子
CREATE TABLE passwd (
  username              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

-- 填充表
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');

-- 一定启用表上行级安全性
ALTER TABLE passwd ENABLE ROW LEVEL SECURITY;

-- 创建策略
-- 管理员可以查看所有行并且添加行
CREATE POLICY admin_all ON passwd TO admin USING (true) WITH CHECK (true);
-- 普通用户可以查看所有行
CREATE POLICY all_view ON passwd FOR SELECT USING (true);
-- 普通用户可以更新自己的记录,但是限制普通用户设置哪个脚本
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')
  );

-- 允许管理所有普通权限
GRANT SELECT, INSERT, UPDATE, DELETE ON passwd TO admin;
-- 用户只在public列上获取select访问
GRANT SELECT
  (user_name, uid, gid, real_name, home_phone, extra_info, home_dir, shell)
  ON passwd TO public;
-- 允许用户更新某些列
GRANT UPDATE
  (pwhash, real_name, home_phone, extra_info, shell)
  ON passwd TO public;

正如任何安全设置,重要的是要测试,并确保该系统的行为如预期。 使用上面的例子,这演示了权限系统正常工作。

-- 管理员可以查看所有行和字段
postgres=> set role admin;
SET
postgres=> table passwd;
 username | 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)

-- 测试Alice能做什么
postgres=> set role alice;
SET
postgres=> table passwd;
ERROR:  permission denied for relation passwd
postgres=> select username,real_name,home_phone,extra_info,home_dir,shell from passwd;
 username | 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 username = 'joe';
ERROR:  permission denied for relation passwd
-- Alice可以修改她的real_name,但是没有其他人
postgres=> update passwd set real_name = 'Alice Doe';
UPDATE 1
postgres=> update passwd set real_name = 'John Doe' where username = '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 relation passwd
postgres=> insert into passwd (username) values ('xxx');
ERROR:  permission denied for relation passwd
-- Alice can change her own password; RLS silently prevents updating other rows
postgres=> update passwd set pwhash = 'abc';
UPDATE 1

引用完整性检查,比如唯一或者主键约束和外键引用,总是绕开行安全性确保 维持数据完整性。当开发模式时必须小心, 并且行级策略通过引用完整性检查避开"隐蔽通道"泄露信息。

在某些情况下重要的是要确保行安全不被应用。 例如,当备份时,它可能是灾难性的,如果行安全默默地导致从备份中省略了一些行。 在这种情况下,你可以设置row_security配置参数为 off。这本身不绕开行安全;如果任何 查询的结果可以过滤策略,那么它会抛出一个错误。错误的原因可以调查并且修复。

在上面的例子中,策略表达式只考虑当前被访问或者被更新行中的值。 这是最简单和表现最好的情况下;如果可能,最好设计行安全应用程序以这种方式工作。 如果有必要咨询其它行或其它表采取策略决定,可以通过策略表达式中使用子-SELECT, 或包含SELECT的函数完成。 然而要注意这样的访问可以创建竞态条件,如果不小心可能造成信息泄漏。 作为一个例子,考虑下面的表设计:

-- 权限组定义
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;

-- 用户的权限级别定义
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;

-- 保持受保护信息的表
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;

-- 行对于安全group_id大于或者等于行的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));

-- 我们只依赖RLS保护信息表
GRANT ALL ON information TO public;

现在假设alice想要改变"秘密"信息, 但是决定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中可以"从mallory查看秘密"字符串。 然而,这有个竞态条件。如果mallory是同时执行的,即

SELECT * FROM information WHERE group_id = 2 FOR UPDATE;

并且事务是READ COMMITTED模式,对于她有可能看到"来自mallory的秘密"。 如果她的事务在alice之后到达information行,则会发生。 他阻塞等待alice事务提交, 那么获取由于FOR UPDATE子句更新的行内容。然而,它确实没有users为隐含的SELECT抓取更新行, 因为子-SELECT没有FOR UPDATE;相反users行读取查询开始时采取的快照。 因此,策略表达式测试mallory权限级别的旧值,并且允许她查看更新的行。

有几种方法可以解决这个问题。一个简单的答案是在子SELECT行安全策略中使用 SELECT ... FOR SHARE。然而,这需要授予引用表(这里users)的 受影响用户UPDATE权限,这可能是不可取的。 (但是可以应用另一个行安全策略以防止它们实际上行使这一 特权;或子SELECT可以嵌入到一个安全 定义函数中。)同时,行共享锁在引用表上大并发使用可能会造成性能问题, 尤其是如果更新它是频繁的。 如果引用表更新不频繁,另一个解决方案是当更新时在引用表上采取排它锁。 所以没有并发事务会检查旧的行值。 或者提交引用表的更新之后,并在修改依赖于新安全方法之前只是等待所有并发事务结束。

更多详细信息参阅 CREATE POLICYALTER TABLE

<
/BODY >