除了通过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。











暂无评论内容