支援的版本:目前 (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 的資料列將不會被處理。可以指定單獨的運算式,以提供對可見資料列和允許修改的資料列的獨立控制。策略運算式作為查詢的一部分執行,並具有執行查詢的使用者的權限,儘管可以使用安全性定義器函數來存取呼叫使用者無法使用的資料。

具有 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);

上面的策略隱含地提供了一個與其 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 Socket 連線才能存取 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。但是,這需要在受影響的使用者上授予 UPDATE 權限(此處為 users),這可能是不希望的。(但是,可以套用另一個列安全性策略來防止他們實際行使該權限;或者子 SELECT 可以嵌入到安全性定義器函數中。)此外,在參考資料表上大量並行使用列共用鎖定可能會導致效能問題,尤其是在頻繁更新它的情況下。另一種解決方案是,如果參考資料表的更新不頻繁,則在更新它時,對參考資料表採用 ACCESS EXCLUSIVE 鎖定,以便沒有並行交易可以檢查舊的列值。或者,可以在提交參考資料表的更新之後,並在進行依賴於新安全性情況的變更之前,等待所有並行交易結束。

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

提交更正

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