2025年9月25日: PostgreSQL 18 釋出!
支援的版本: 當前 (18) / 17 / 16 / 15 / 14 / 13
開發版本: devel
不支援的版本: 12 / 11 / 10 / 9.6 / 9.5

5.9. 行安全策略 #

除了透過 許可權 系統提供的 SQL 標準 GRANT 命令外,表還可以設定 行安全策略,該策略可以基於每個使用者限制普通查詢返回哪些行,或者限制資料修改命令插入、更新或刪除哪些行。此功能也稱為 行級安全。預設情況下,表沒有策略,因此如果使用者根據 SQL 許可權系統對錶具有訪問許可權,則表中的所有行都可用於查詢或更新。

當表上啟用了行安全(使用 ALTER TABLE ... ENABLE ROW LEVEL SECURITY)時,所有對錶的正常訪問(用於選擇行或修改行)都必須由行安全策略允許。(然而,表的所有者通常不受行安全策略的約束。)如果表中不存在策略,則會使用預設的拒絕策略,這意味著沒有行可見或可修改。適用於整個表的命令(如 TRUNCATEREFERENCES)不受行安全策略的約束。

行安全策略可以針對命令、角色,或兩者都指定。可以指定一個策略適用於 ALL 命令,或適用於 SELECTINSERTUPDATEDELETE。可以將多個角色分配給給定的策略,並且適用正常的角色成員和繼承規則。

要根據策略指定哪些行可見或可修改,需要一個返回布林值的表示式。該表示式將在使用者查詢中的任何條件或函式之前對每一行進行求值。(此規則的唯一例外是 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 子句,因此該約束既適用於命令選擇的行(因此經理不能 SELECTUPDATEDELETE 屬於另一位經理的現有行),也適用於命令修改的行(因此不能透過 INSERTUPDATE 建立屬於另一位經理的行)。

如果沒有指定角色,或者使用了特殊使用者名稱 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 子句而獲取更新後的行內容。然而,它**不會**為隱式的 SELECTusers 中獲取更新後的行,因為該子 SELECT 沒有 FOR UPDATE;相反,users 行是在查詢開始時採取的快照下讀取的。因此,策略表示式測試了 mallory 的許可權級別的舊值,並允許她看到更新後的行。

這個問題有幾種解決辦法。一個簡單的答案是在行安全策略的子 SELECT 中使用 SELECT ... FOR SHARE。然而,這需要將引用表(此處為 users)的 UPDATE 許可權授予受影響的使用者,這可能不希望發生。(但可以應用另一個行安全策略來阻止他們實際行使該許可權;或者子 SELECT 可以嵌入到 security definer 函式中。)此外,對引用表進行重度併發的行共享鎖可能會造成效能問題,尤其是當它被頻繁更新時。另一個解決方案,當引用表更新不頻繁時實用,是在更新引用表時獲取該表的 ACCESS EXCLUSIVE 鎖,這樣就不會有併發事務能夠檢查舊的行值。或者,在提交對引用表的更新後,並且在進行依賴於新安全情況的更改之前,可以等待所有併發事務結束。

有關更多詳細資訊,請參閱 CREATE POLICYALTER TABLE

提交更正

如果您在文件中看到任何不正確、不符合您對特定功能的實際體驗或需要進一步澄清的內容,請使用 此表單 來報告文件問題。