除了可以通过 GRANT 获得的 SQL 标准备的特权系统之外,表还可以具有行安全策略,它按每个用户限制普通查询可以返回的行数或数据修改命令可以插入、更新或删除的行数。此特性也被称为行级安全。默认情况下,表没有任何策略,因此,如果用户根据 SQL 特权系统具有对表的访问权限,那么其中的所有行都可以用于查询或更新。
如果对某个表格启用了行安全策略(使用 ALTER TABLE ... ENABLE ROW LEVEL SECURITY),那么必须通过行安全策略允许对该表格的所有常规访问(用于选择行或修改行)。(但是,表格的所有者通常不受行安全策略的约束。)如果表格没有任何策略,则使用默认拒绝策略,意味着不能看到或修改任何行。应用于整个表格的操作(如 TRUNCATE
和 REFERENCES
)不受行安全策略的约束。
行安全策略可以特定于命令或角色,或同时特定于两者。可以指定将策略应用于 ALL
命令,或 SELECT
、INSERT
、UPDATE
或 DELETE
。可以将多个角色分配给给定的策略,且应用常规角色成员资格和继承规则。
为了指定根据某个策略可以看到或可以修改哪些行,需要一个返回布尔结果的表达式。在求值任何来自用户查询的条件或函数之前,会对每一行求值该表达式。(仅有的例外是 leakproof
函数,这些函数保证不泄露信息;优化器可以选在行安全检查之前应用此类函数。)不会处理表达式未返回 true
的行。可以指定单独的表达式,以独立控制可以看见的行和可以允许修改的行。策略表达式作为查询的一部分运行,并使用运行查询用户的权限,尽管可以使用安全定义函数访问呼叫用户不可用的数据。
超级用户和带有 BYPASSRLS
属性的角色在访问表格时总是绕过后段安全系统。表格所有者通常也绕过后段安全,尽管表格所有者可以选择使用 ALTER TABLE ... FORCE ROW LEVEL SECURITY 使其受到行安全约束。
只有表格所有者才有权限启用和禁用行安全策略以及向表格添加策略。
使用 CREATE POLICY 命令创建策略,使用 ALTER POLICY 命令修改策略,使用 DROP POLICY 命令删除策略。要为给定的表格启用和禁用行安全策略,请使用 ALTER TABLE 命令。
每个策略都有名称,可以针对一个表格定义多个策略。由于策略特定于表格,因此表格的每个策略都必须有唯一的名称。不同的表格可以有相同名称的策略。
当多个策略适用于给定查询时,将使用 OR
(用于默认情况下的允许性策略)或 AND
(用于限制性策略)将它们合并。这类似于给定角色拥有其成员角色所有权限的规则。允许性与限制性策略将在下面进一步讨论。
作为一个简单的示例,以下是有关在 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);
上面的策略隐式提供一个 WITH CHECK
子句,其与 USING
子句相同,以使约束同时适用于由命令选择的行(因此经理无法 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
能够看到 “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
锁,这样就没有并发事务可以检查旧行值。或者,在提交对引用表的更新后,可以等到所有并发事务都结束,然后再做出依托于新的安全状况的更改。