5.7. 行安全策略

除了可通过GRANT使用的 SQL 标准privilege system之外,表还可以具有行安全策略,这些安全策略可以按用户限制每个行可以由普通查询返回或可以由数据修改命令插入,更新或删除。此功能也称为“行级安全性”。默认情况下,表没有任何策略,因此,如果用户根据 SQL 特权系统具有对表的访问特权,则该表中的所有行都可用于查询或更新。

当在表(具有ALTER TABLE ...启用行级别安全)上启用行安全性时,行安全策略必须允许对表的所有普通访问以选择行或修改行。 (但是,表的所有者通常不受行安全策略的约束.)如果表不存在任何策略,则使用默认拒绝策略,这意味着没有可见的行或可以修改的行。适用于整个表的操作(例如TRUNCATEREFERENCES)不受行安全性的限制。

行安全策略可以特定于命令,角色或两者。可以指定一个策略以应用于ALL命令或SELECTINSERTUPDATEDELETE。可以将多个角色分配给给定策略,并应用常规角色成员身份和继承规则。

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

具有BYPASSRLS属性的超级用户和角色在访问表时始终会绕过行安全系统。表所有者通常也绕过行安全性,尽管表所有者可以选择使用ALTER TABLE ...强制行级别安全来接受行安全性。

启用和禁用行安全性以及向表中添加策略始终仅是表所有者的特权。

使用CREATE POLICY命令创建策略,使用ALTER POLICY命令更改策略,然后使用DROP POLICY命令删除策略。要启用和禁用给定表的行安全性,请使用ALTER TABLE命令。

每个策略都有一个名称,并且可以为一个表定义多个策略。由于策略是特定于表的,因此表的每个策略都必须具有唯一的名称。不同的表可能具有相同名称的策略。

当多个策略应用于给定查询时,可以使用OR(对于宽松策略,这是默认设置)或使用AND(对于限制性策略)进行合并。这类似于给定角色具有其所隶属的所有角色的特权的规则。宽松 Policy 与限制性 Policy 在下面进一步讨论。

作为一个简单的示例,下面是如何在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子句,因此该约束既适用于命令选择的行(因此 Management 器不能SELECTUPDATEDELETE属于不同 Management 器的现有行)以及被修改的行命令(因此不能通过INSERTUPDATE创建属于其他 Management 器的行)。

如果未指定角色,或使用特殊用户名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 relation 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 relation 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 relation passwd
postgres=> insert into passwd (user_name) 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

到目前为止构造的所有策略都是允许的策略,这意味着当应用多个策略时,将使用“或”布尔运算符将它们组合在一起。虽然可以将许可策略构造为仅允许在预期的情况下访问行,但是将许可策略与限制性策略(记录必须通过并且使用“ AND”布尔运算符进行组合)相结合会更简单。在上面的示例的基础上,我们添加了一个限制性策略,要求 Management 员通过本地 Unix 套接字连接以访问passwd表的记录:

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

然后,我们可以看到由于限制策略,通过网络连接的 Management 员将看不到任何记录:

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

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

=> SELECT current_user;
 current_user 
--------------
 admin
(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。这本身并不绕过行安全性;如果任何查询的结果将被策略过滤,它会引发错误。然后可以调查并确定错误原因。

在上面的示例中,策略表达式仅考虑要访问或更新的行中的当前值。这是最简单,性能最好的情况;在可能的情况下,最好设计行安全性应用程序以这种方式工作。如果需要参考其他行或其他表来制定策略决策,则可以使用策略表达式中的 subSELECT s 或包含SELECT s 的函数来完成。但是请注意,如果不注意,此类访问可能会导致竞争条件,从而可能导致信息泄漏。作为示例,请考虑以下表格设计:

-- 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希望更改“稍有机密”的信息,但决定该行的新内容不信任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”字符串。但是,这里存在 match 条件。如果mallory正在同时做,例如,

SELECT * FROM information WHERE group_id = 2 FOR UPDATE;

并且她的 Transaction 处于READ COMMITTED模式,则有可能看到“马洛里的 Secret”。如果她的 Transaction 在alice之后刚到达information行,就会发生这种情况。它阻止 awaitalice的事务提交,然后通过FOR UPDATE子句获取更新的行内容。但是,它不会从users中获取隐式SELECT的更新行,因为该子SELECT没有FOR UPDATE。而是读取users行以及在查询开始时拍摄的快照。因此,策略表达式测试mallory的特权级别的旧值,并允许她查看更新的行。

有几种方法可以解决此问题。一个简单的答案是在行安全策略的SELECT子中使用SELECT ... FOR SHARE。但是,这需要向受影响的用户授予对引用表(此处为users)的UPDATE特权,这可能是不希望的。 (但是,可以应用另一行安全策略来阻止他们实际行使该特权;或者可以将 subSELECT嵌入到安全定义器函数中.)而且,在引用表上大量同时使用行共享锁可能会带来性能问题。问题,尤其是如果更新频繁。如果不经常更新被引用表,另一种可行的解决方案是在更新被引用表时对其进行排他锁,这样就不会有任何并发事务在检查旧的行值。或者,可以在提交引用表的更新之后,然后根据新的安全状况进行更改,然后 await 所有并发事务结束。

有关更多详细信息,请参见CREATE POLICYALTER TABLE