行安全策略

除了通过GRANT提供的SQL标准权限系统,表还可以设置行安全策略,按用户限制普通查询能够返回哪些行,以及数据修改命令能够插入、更新或删除哪些行。这也称为行级安全(Row-Level Security)。

默认情况下,表没有任何策略。如果用户通过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组合。OR的行为类似角色拥有其所有所属角色的权限。下文会进一步说明宽松与限制性策略。

一个简单例子是在accounts关系上创建策略,只允许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想修改“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没有FOR UPDATE,因此它不会获取更新后的users行,而是使用查询开始时的快照读取。于是策略表达式检查的是mallory旧的权限等级,允许她看到更新后的信息行。

解决方式有几种。简单的方法是在行安全策略的子SELECT中使用SELECT … FOR SHARE。但这要求向受影响的用户授予被引用表(此处为users)的UPDATE权限,这可能并不合适。

可以再用另一条行安全策略防止他们真正行使该权限,或将子SELECT放进安全定义者函数。不过,并发大量使用被引用表的行共享锁也可能造成性能问题,特别是在该表频繁更新时。

如果被引用表很少更新,另一种实用方法是在更新时获取ACCESS EXCLUSIVE锁,使并发事务无法读取旧行。也可以在提交被引用表的更新后,等待所有并发事务结束,再执行依赖新安全状态的修改。

更多细节见CREATE POLICY和ALTER TABLE。

来源与许可

PostgreSQL Global Development Group,5.9 Row Security Policies。核验日期:2026-10-03;当时current指向PostgreSQL 18。对应18版固定链接。本稿完整翻译指定章节正文,保留所有SQL、英文注释及psql原输出;删除站点导航、提交勘误等附属元素。输出来自原文,未执行数据库测试。

参考:CREATE POLICY、ALTER TABLE、row_security参数。静态核验确认row_security=off是过滤报错而非绕过,表/列权限与RLS共同生效,引用完整性检查与READ COMMITTED跨表快照风险均保留;示例需要对应角色与表上下文,并非无前置条件的单个可运行脚本。

依官方Legal Notice复制及改编,保留完整版权、许可与免责声明如下:

PostgreSQL Database Management System (also known as Postgres, formerly known as Postgres95)

Portions Copyright © 1996-2026, PostgreSQL Global Development Group
Portions Copyright © 1994, The Regents of the University of California

Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies.

IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN “AS-IS” BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容