PostgreSQL 中如何控制行級安全和列級安全

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/05/0555b2772b98e3f4ff064aeb12f1939e.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"彭佔元","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2018年8月加入去哪兒網,現負責公司 PostgreSQL/GreenPlum 運維工作,對數據庫日常運維和日常調優有大量優化實踐經驗。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1. 需求提出","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最近有業務線的同學向 DBA 提出這樣的需求:“我的表裏有很多敏感數據,怎麼給使用者(從 DBA 角度來看就是 DB User)指定查看某些特定行或某些列的權限?”","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是我們經常用到的業務場景,比如最典型全公司的短信數據和用戶管理平臺場景,如何限制各部門僅可查看屬於本部門的某些非敏感數據?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PostgreSQL 爲了符合各種場景的安全需求,它的權限控制非常完善,它在各個級別上都具有廣泛的安全功能。接下來我們來看一下在 PostgreSQL 中利用行級安全和列級安全來解決上述問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2. 行級安全","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"行級安全(Row Level Security),這一特性首次出現在 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"PostgreSQL 9.6","attrs":{}},{"type":"text","text":"中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顧名思義,就是管理員控制普通用戶對於表的查看和操作,我們可以把他理解爲一個過濾器,通過指定的策略實現對錶中展示數據進行篩選。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過行級安全控制,我們可以數據避免受到其他用戶的破壞,也可以確保該行的數據只有指定的用戶可以查看。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1. 實驗示例","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"CREATE TABLE passwd (\n id bigserial primary key,\n user_name varchar(32) UNIQUE NOT NULL,\n pwhash varchar(32),\n real_name varchar(32) NOT NULL,\n home_phone varchar(12),\n home_dir text NOT NULL,\n shell text NOT NULL\n);\n \n-- 填充表\nINSERT INTO passwd (user_name,pwhash,real_name,home_phone,home_dir,shell) VALUES\n ('appuser','xxxx','appuser','111-222-3333','/root','/bin/dash');\nINSERT INTO passwd(user_name,pwhash,real_name,home_phone,home_dir,shell) VALUES\n ('appuser1','xxxx','appuser1','123-456-7890','/home/appuser1','/bin/zsh');\nINSERT INTO passwd(user_name,pwhash,real_name,home_phone,home_dir,shell) VALUES\n ('appuser2','xxxx','appuser2','098-765-4321','/home/appuser2','/bin/zsh');","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據表 passwd 記錄的信息,現在我們有這樣的需求:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"用戶只能看到包含自己信息的行,而超級用戶可以查看所有信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們創建對應用戶並對用戶進行授權操作。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"my_testdb=# \\c my_testdb appuser\nYou are now connected to database \"my_testdb\" as user \"appuser\".\n \nmy_testdb=> select * from passwd ;\n id | user_name | pwhash | real_name | home_phone | home_dir | shell \n----+-----------+--------+-----------+--------------+----------+-----------\n 1 | appuser | xxxx | appuser | 111-222-3333 | /root | /bin/dash\n(1 row)\n \nmy_testdb=> \\c my_testdb appuser1\nYou are now connected to database \"my_testdb\" as user \"appuser1\".\n \nmy_testdb=> select * from passwd ;\n id | user_name | pwhash | real_name | home_phone | home_dir | shell \n----+-----------+--------+-----------+--------------+----------------+----------\n 2 | appuser1 | xxxx | appuser1 | 123-456-7890 | /home/appuser1 | /bin/zsh\n(1 row)\n \nmy_testdb=> \\c my_testdb appuser2\nYou are now connected to database \"my_testdb\" as user \"appuser2\".\n \nmy_testdb=> select * from passwd ;\n id | user_name | pwhash | real_name | home_phone | home_dir | shell \n----+-----------+--------+-----------+--------------+----------------+----------\n 3 | appuser2 | xxxx | appuser2 | 098-765-4321 | /home/appuser2 | /bin/zsh\n(1 row)\n \nmy_testdb=> \\c my_testdb postgres\nYou are now connected to database \"my_testdb\" as user \"postgres\".\n \nmy_testdb=# select * from passwd ;\n id | user_name | pwhash | real_name | home_phone | home_dir | shell \n----+-----------+--------+-----------+--------------+----------------+-----------\n 1 | appuser | xxxx | appuser | 111-222-3333 | /root | /bin/dash\n 2 | appuser1 | xxxx | appuser1 | 123-456-7890 | /home/appuser1 | /bin/zsh\n 3 | appuser2 | xxxx | appuser2 | 098-765-4321 | /home/appuser2 | /bin/zsh\n(3 rows)\n \n \n-- 將表的所有者指定爲appuser,那他將可以看到表中全部數據\nmy_testdb=# alter table passwd owner to appuser;\nALTER TABLE\n \nmy_testdb=# \\c my_testdb appuser\nYou are now connected to database \"my_testdb\" as user \"appuser\".\n \nmy_testdb=> select * from passwd ;\n id | user_name | pwhash | real_name | home_phone | home_dir | shell \n----+-----------+--------+-----------+--------------+----------------+-----------\n 1 | appuser | xxxx | appuser | 111-222-3333 | /root | /bin/dash\n 2 | appuser1 | xxxx | appuser1 | 123-456-7890 | /home/appuser1 | /bin/zsh\n 3 | appuser2 | xxxx | appuser2 | 098-765-4321 | /home/appuser2 | /bin/zsh\n(3 rows)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在現有的環境下,如果 appuser1 需要去訪問數據表中所有的數據,那麼他應該如何“繞過”行級安全策略呢?","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"-- BYPASSRLS屬性只能超級用戶才能修改\nmy_testdb=# alter user appuser1 bypassrls;\nALTER ROLE\n \n \nmy_testdb=# \\c my_testdb appuser1\nYou are now connected to database \"my_testdb\" as user \"appuser1\".\n \nmy_testdb=> select * from passwd ;\n user_name | pwhash | uid | gid | real_name | home_phone | extra_info | home_dir | shell \n-----------+--------+-----+-----+-----------+--------------+------------+----------------+-----------\n appuser | xxx | 0 | 0 | appuser | 111-222-3333 | | /root | /bin/dash\n appuser1 | xxx | 1 | 1 | appuser1 | 123-456-7890 | | /home/appuser1 | /bin/zsh\n appuser2 | xxx | 2 | 1 | appuser2 | 098-765-4321 | | /home/appuser2 | /bin/zsh\n(3 rows)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PostgreSQL 具有 BYPASSRLS 和 NOBYPASSRLS 權限,可以將其分配給角色。默認情況下分配 NOBYPASSRLS。表所有者和超級用戶具有 BYPASSRLS 權限,擁有 BYPASSRLS 權限可以跳過行級安全策略限制。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們刪除了這條策略會發現,appuer2 用戶變得","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"不能訪問","attrs":{}},{"type":"text","text":"表中數據了,這時我們需要","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"禁用行級安全策略","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"my_testdb=> \\c my_testdb appuser\nYou are now connected to database \"my_testdb\" as user \"appuser\".\n \nmy_testdb=> drop POLICY passwd_rls_policy ON passwd;\nDROP POLICY\n \nmy_testdb=> \\c my_testdb appuser2\nYou are now connected to database \"my_testdb\" as user \"appuser2\".\n \nmy_testdb=> select * from passwd ;\n id | user_name | pwhash | real_name | home_phone | home_dir | shell\n----+-----------+--------+-----------+------------+----------+-------\n(0 rows)\n \nmy_testdb=> \\c my_testdb appuser\nYou are now connected to database \"my_testdb\" as user \"appuser\".\n \nmy_testdb=> alter table passwd disable row level security;\nALTER TABLE\n \nmy_testdb=> \\c my_testdb appuser2\nYou are now connected to database \"my_testdb\" as user \"appuser2\".\n \nmy_testdb=> select * from passwd ;\n user_name | pwhash | uid | gid | real_name | home_phone | extra_info | home_dir | shell \n-----------+--------+-----+-----+-----------+--------------+------------+----------------+-----------\n appuser | xxx | 0 | 0 | appuser | 111-222-3333 | | /root | /bin/dash\n appuser1 | xxx | 1 | 1 | appuser1 | 123-456-7890 | | /home/appuser1 | /bin/zsh\n appuser2 | xxx | 2 | 1 | appuser2 | 098-765-4321 | | /home/appuser2 | /bin/zsh\n(3 rows)\n \n \n-- 恢復初始實驗環境\nmy_testdb=# alter table passwd owner to postgres;\nALTER TABLE\nmy_testdb=# revoke all ON passwd from appuser;\nREVOKE\nmy_testdb=# revoke all ON passwd from appuser1;\nREVOKE\nmy_testdb=# revoke all ON passwd from appuser2;\nREVOKE","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時我們的用戶又能訪問所有數據了,至此行級安全策略限制解除。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3. 列級安全","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"列級安全是允許用戶僅能查看特定的列,對不想要被其他用戶查看的列進行隱藏。針對這種需求,PostgreSQL 主要提供了兩種方式,創建視圖和授權。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"創建視圖的方式就是要這個視圖僅包含要顯示給用戶指定的列,然後對用戶僅提供視圖名稱而不是表名稱。這裏不做過多的描述了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過授權的方式實現列級訪問第一步就是先撤銷用戶對錶訪問,然後對指定的列進行單獨授權。在這種情況下用戶不應該對錶有訪問權限,否則不能訪問指定列。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1. 實驗示例","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"my_testdb=# revoke all ON passwd from public;\nREVOKE\n \nmy_testdb=# grant select(user_name,home_dir) on passwd to appuser2;\nGRANT\n \nmy_testdb=# \\c - appuser2\nYou are now connected to database \"my_testdb\" as user \"appuser2\".\n \nmy_testdb=> select * from passwd ;\nERROR: permission denied for table passwd\n \nmy_testdb=> select user_name,home_dir from passwd ;\n user_name | home_dir \n-----------+----------------\n appuser | /root\n appuser1 | /home/appuser1\n appuser2 | /home/appuser2\n(3 rows)\n \n \n-- 恢復實驗環境\nmy_testdb=# revoke select(user_name,home_dir) on passwd from appuser2;\nREVOKE","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此通過權限控制我們實現了對列數據訪問的控制。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4. 行列級安全的結合","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在日常的應用中,我們在創建行級策略時並不能保證 current_user 與表中存在的用戶必然匹配,這時我們可以通過修改會話級變量來實現對數據的訪問。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實驗中我們依舊使用上面實驗的數據,但是我們只使用 appuer 用戶來模擬應用連接數據庫。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"-- 列級安全授權,目的是隻允許用戶訪問指定列信息\nmy_testdb=# grant select(user_name,home_dir) on passwd to appuser;\nGRANT\n \n-- 行級安全授權,目的是允許用戶查看指定的行\nmy_testdb=# CREATE POLICY passwd_rls_policy ON passwd FOR ALL TO PUBLIC USING (user_name=current_setting('rls.user_name'));\nCREATE POLICY\nmy_testdb=# ALTER TABLE passwd ENABLE ROW LEVEL SECURITY;\nALTER TABLE\n \n-- 指定會話級變量實現訪問不同的數據行需求\nmy_testdb=> set rls.user_name = 'appuser';\nSET\n \nmy_testdb=> select * from passwd ;\nERROR: permission denied for table passwd\n \n \nmy_testdb=> select user_name,home_dir from passwd ;\n user_name | home_dir\n-----------+----------\n appuser | /root\n(1 row)\n \nmy_testdb=> set rls.user_name = 'appuser1';\nSET\n \nmy_testdb=> select user_name,home_dir from passwd ;\n user_name | home_dir \n-----------+----------------\n appuser1 | /home/appuser1\n(1 row)\n \n \nmy_testdb=> set rls.user_name = 'appuser2';\nSET\n \n \nmy_testdb=> select user_name,home_dir from passwd ;\n user_name | home_dir \n-----------+----------------\n appuser2 | /home/appuser2\n(1 row)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過這樣的方法,我們就可以實現通過指定會話級的變量完成對指定數據的訪問了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"5. 小節","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"添加 RLS 意味着在每個查詢中添加了 where 子句,必須滿足對應的條件才能夠通過行級安全性驗證,這樣也就自然而然的會影響性能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時,行級安全性規則還附加了一個 CHECK 子句,因此制定規則的規模越大,對性能影響也就越大。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,數據庫行級訪問的功能雖好,也需根據實際場景,適當選用。","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章