全部產品
Search
文件中心

PolarDB:行安全性策略

更新時間:Jul 06, 2024

本文介紹了行安全性策略的相關內容。

簡介

除可以通過GRANT使用 SQL 標準的特權系統之外,表還可以具有 行安全性策略,它針對每一個使用者限制哪些行可以被普通的查詢返回或者可以被資料修改命令插入、更新或刪除。這種特性也被稱為行級安全性。預設情況下,表不具有任何策略,這樣使用者根據 SQL 特權系統具有對錶的存取權限,對於查詢或更新來說其中所有的行都是平等的。

當在一個表上啟用行安全性時(使用 ALTER TABLE ... ENABLE ROW LEVEL SECURITY),所有對該表選擇行或者修改行的普通訪問都必須被一條行安全性策略所允許(不過,表的擁有者通常不服從行安全性策略)。如果表上不存在策略,將使用一條預設的否定策略,即所有的行都不可見或者不能被修改。應用在整個表上的操作不服從行安全性,例如TRUNCATEREFERENCES

行安全性策略可以針對特定的命令、角色或者兩者。一條策略可以被指定為適用於ALL命令,或者SELECTINSERTUPDATE、或者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);

上面的策略隱含地提供了一個與其該約束適用於被一個命令選擇的行(這樣一個經理不能SELECTUPDATE或者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 口令檔案:

    -− 簡單的口令檔案例子
    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;  -− 管理員
    CREATE ROLE bob;    -− 普通使用者
    CREATE ROLE alice;  -− 普通使用者

    -− 填充表
    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');

    -− 確保在表上啟用行級安全性
    ALTER TABLE passwd ENABLE ROW LEVEL SECURITY;

    -− 建立策略
    -− 管理員能看見所有行並且增加任意行
    CREATE POLICY admin_all ON passwd TO admin USING (true) WITH CHECK (true);
    -− 普通使用者可以看見所有行
    CREATE POLICY all_view ON passwd FOR SELECT USING (true);
    -− 普通使用者可以更新它們自己的記錄,但是限制普通使用者可用的 shell
    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')
      );

    -− 允許管理員有所有普通許可權
    GRANT SELECT, INSERT, UPDATE, DELETE ON passwd TO admin;
    -− 使用者只在公用列上得到選擇訪問
    GRANT SELECT
      (user_name, uid, gid, real_name, home_phone, extra_info, home_dir, shell)
      ON passwd TO public;
    -− 允許使用者更新特定行
    GRANT UPDATE
      (pwhash, real_name, home_phone, extra_info, shell)
      ON passwd TO public;

對於任意安全性設定來說,重要的是測試並確保系統的行為符合預期。 使用上述的例子,下面展示了許可權系統工作正確:

    -− admin 可以看到所有的行和域
    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)

    -− 測試 Alice 能做什麼
    postgres=> set role alice;
    SET
    postgres=> table passwd;
    ERROR:  permission denied for relation 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 relation passwd
    -− Alice 被允許更改她自己的 real_name,但不能改其他的
    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 relation passwd
    postgres=> insert into passwd (user_name) values ('xxx');
    ERROR:  permission denied for relation passwd
    -− Alice 可以更改她自己的口令;行級安全性會悄悄地阻止更新其他行
    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)

    => SELECT current_user;
     current_user
    --------------
     admin
    (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的函數來實現。不過要注意這類訪問可能會導致競爭條件,在不小心的情況下這可能會導致資訊泄露。作為一個例子,考慮下面的表設計:

    -− 特權組的定義
    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 是管理員
    GRANT SELECT ON groups TO public;

    -− 使用者的特權層級的定義
    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;

    -− 儲存要被保護的資訊的表
    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;

    -− 對於安全性 group_id 大於等於一行的 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));

    -− 我們只依賴於行級安全性來保護資訊表
    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模式,她就可能看到 “s 對 mallory 保密”的東西。如果她的事務在alice 做完之後就到達資訊行,這就會發生。它會阻塞等待 alice的事務提交,然後拜FOR UPDATE子句所賜取得更新後的行內容。不過,對於來自users的隱式 SELECT,它不會取得一個已更新的行, 因為子-SELECT沒有FOR UPDATE,相反會使用查詢開始時取得的快照讀取users行。因此, 策略運算式會測試mallory的特權層級的舊值並且允許它看到被更新的行。

有多種方法能解決這個問題。一種簡單的答案是在行安全性策略中的子-SELECT裡使用SELECT ... FOR SHARE。 不過,這要求在被參考資料表(這裡是users)上授予 UPDATE特權給受影響的使用者,這可能不是我們想要的( 但是另一條行安全性策略可能被應用來阻止它們實際使用這個特權,或者子-SELECT可能被嵌入到一個安全性定義者函數中)。 還有,在被引用的表上過多並發地使用行共用鎖定可能會導致效能問題, 特別是表更新比較頻繁時。另一種解決方案(如果被參考資料表上的更新不頻繁就可行)是在更新被參考資料表時對它取一個獨佔鎖定,這樣就沒有並發事務能夠檢查舊的行值了。或者我們可以在提交對被參考資料表的更新之後、在做依賴於新安全性情況的更改之前等待所有並發事務結束。