除了通过 权限 系统提供的 SQL 标准 GRANT 命令外,表还可以设置 行安全策略,该策略可以基于每个用户限制普通查询返回哪些行,或者限制数据修改命令插入、更新或删除哪些行。此功能也称为 行级安全。默认情况下,表没有策略,因此如果用户根据 SQL 权限系统对表具有访问权限,则表中的所有行都可用于查询或更新。
当表上启用了行安全(使用 ALTER TABLE ... ENABLE ROW LEVEL SECURITY)时,所有对表的正常访问(用于选择行或修改行)都必须由行安全策略允许。(然而,表的所有者通常不受行安全策略的约束。)如果表中不存在策略,则会使用默认的拒绝策略,这意味着没有行可见或可修改。适用于整个表的命令(如 TRUNCATE 和 REFERENCES)不受行安全策略的约束。
行安全策略可以针对命令、角色,或两者都指定。可以指定一个策略适用于 ALL 命令,或适用于 SELECT、INSERT、UPDATE 或 DELETE。可以将多个角色分配给给定的策略,并且适用正常的角色成员和继承规则。
要根据策略指定哪些行可见或可修改,需要一个返回布尔值的表达式。该表达式将在用户查询中的任何条件或函数之前对每一行进行求值。(此规则的唯一例外是 leakproof 函数,它们保证不泄露信息;优化器可以选择在行安全检查之前应用此类函数。)对于表达式不返回 true 的行,将不会进行处理。可以指定独立的表达式,为可见行和允许修改的行提供独立的控制。策略表达式作为查询的一部分运行,并使用运行查询的用户的特权,尽管可以使用 security-definer 函数来访问调用用户不可用的数据。
超级用户和具有 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 子句,因此该约束既适用于命令选择的行(因此经理不能 SELECT、UPDATE 或 DELETE 属于另一位经理的现有行),也适用于命令修改的行(因此不能通过 INSERT 或 UPDATE 创建属于另一位经理的行)。
如果没有指定角色,或者使用了特殊用户名 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 希望更改 “稍微秘密” 的信息,但她认为 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 子句而获取更新后的行内容。然而,它**不会**为隐式的 SELECT 从 users 中获取更新后的行,因为该子 SELECT 没有 FOR UPDATE;相反,users 行是在查询开始时采取的快照下读取的。因此,策略表达式测试了 mallory 的权限级别的旧值,并允许她看到更新后的行。
这个问题有几种解决办法。一个简单的答案是在行安全策略的子 SELECT 中使用 SELECT ... FOR SHARE。然而,这需要将引用表(此处为 users)的 UPDATE 权限授予受影响的用户,这可能不希望发生。(但可以应用另一个行安全策略来阻止他们实际行使该权限;或者子 SELECT 可以嵌入到 security definer 函数中。)此外,对引用表进行重度并发的行共享锁可能会造成性能问题,尤其是当它被频繁更新时。另一个解决方案,当引用表更新不频繁时实用,是在更新引用表时获取该表的 ACCESS EXCLUSIVE 锁,这样就不会有并发事务能够检查旧的行值。或者,在提交对引用表的更新后,并且在进行依赖于新安全情况的更改之前,可以等待所有并发事务结束。
有关更多详细信息,请参阅 CREATE POLICY 和 ALTER TABLE。