PostgreSQL 9.5 Row Level Security(複数POLICY定義)
今日のエントリはRow Level Securityの小ネタ。
同一テーブル/同一ロール/同一操作に対して、複数のPOLICY定義を書いたらどうなるのっと。
元のテーブル
元のテーブルはこんな感じ。
(RLSをBYPASSする)superuserで検索するとこんなレコードが格納されている。
rls=# TABLE workflow; id | jobname | status ----+---------+---------- 1 | job_a | checking 2 | job_b | working 4 | job_d | complete 3 | job_c | working 5 | job_e | new (5 rows)
まあ、なんかタスクを管理するようなテーブルだと思ってください。
で、このテーブルに、empロールがSELECTするときのPOLICY定義を以下のように設定しておく。
CREATE POLICY emp_select_new ON workflow FOR SELECT TO emp USING (status = 'new'); CREATE POLICY emp_select_working ON workflow FOR SELECT TO emp USING (status = 'working'); CREATE POLICY emp_select_checking ON workflow FOR SELECT TO emp USING (status = 'checking');
psqlから \d コマンドでテーブルに設定されたPOLICYが確認できる。
rls=# \d workflow Table "public.workflow" Column | Type | Modifiers ---------+---------+----------- id | integer | not null jobname | text | status | text | Indexes: "workflow_pkey" PRIMARY KEY, btree (id) Policies: POLICY "emp_select_checking" FOR SELECT TO emp USING ((status = 'checking'::text)) POLICY "emp_select_new" FOR SELECT TO emp USING ((status = 'new'::text)) POLICY "emp_select_working" FOR SELECT TO emp USING ((status = 'working'::text))
ちなみに、 \dp コマンドだと"Access privileges"も一緒に確認できる。参考までに。
rls=> \dp workflow Access privileges Schema | Name | Type | Access privileges | Column privileges | Policies --------+----------+-------+-------------------+-------------------+------------------------------------ public | workflow | table | nuko=arwdDxt/nuko+| | emp_select_new (r): + | | | boss=arwdDxt/nuko+| | (u): (status = 'new'::text) + | | | emp=arwdDxt/nuko | | to: emp + | | | | | emp_select_working (r): + | | | | | (u): (status = 'working'::text) + | | | | | to: emp + | | | | | emp_select_checking (r): + | | | | | (u): (status = 'checking'::text)+ | | | | | to: emp (1 row)
empで検索すると・・・
で、empロールでrlsデータベースにログインして、workflowテーブルを検索してみる。
[nuko@localhost 9.5-rls]$ psql rls -U emp -c "TABLE workflow" id | jobname | status ----+---------+---------- 1 | job_a | checking 2 | job_b | working 3 | job_c | working 5 | job_e | new (4 rows)
この結果から推測すると、3つ設定したPOLICY定義のUSING句に設定したconditionをOR接続しているのではないかと。
EXPLAINをとってみると
[nuko@localhost 9.5-rls]$ psql rls -U emp -c "EXPLAIN TABLE workflow" QUERY PLAN ------------------------------------------------------------------------------------------------- Seq Scan on workflow (cost=0.00..24.88 rows=13 width=68) Filter: ((status = 'working'::text) OR (status = 'new'::text) OR (status = 'checking'::text)) (2 rows)
のように、やっぱりOR接続されている。
つまり、今回の例の3つのPOLICY定義は、以下のようなINを使った1つのPOLICY定義とだいたい同じ。
CREATE POLICY emp_select ON workflow FOR SELECT TO emp USING (status IN ('new','working','checking'));
上記のPOLICY定義の結果を試してみる。
[nuko@localhost 9.5-rls]$ psql rls -U emp -c "\d workflow" Table "public.workflow" Column | Type | Modifiers ---------+---------+----------- id | integer | not null jobname | text | status | text | Indexes: "workflow_pkey" PRIMARY KEY, btree (id) Policies: POLICY "emp_select" FOR SELECT TO emp USING ((status = ANY (ARRAY['new'::text, 'working'::text, 'checking'::text]))) [nuko@localhost 9.5-rls]$ psql rls -U emp -c "TABLE workflow" id | jobname | status ----+---------+---------- 1 | job_a | checking 2 | job_b | working 3 | job_c | working 5 | job_e | new (4 rows) [nuko@localhost 9.5-rls]$ psql rls -U emp -c "EXPLAIN TABLE workflow" QUERY PLAN ------------------------------------------------------------- Seq Scan on workflow (cost=0.00..21.69 rows=13 width=68) Filter: (status = ANY ('{new,working,checking}'::text[])) (2 rows) [nuko@localhost 9.5-rls]$
(´-`).oO(そういえば、INって、ANY (配列) に展開されちゃうんだっけ・・・)
まあ、ちょっとクエリの形は違うけど、同一の効果になる。
INSERT/UPDATEのWITH CHECK
POLICY定義には、INSERT/UPDATE操作専用の条件を指定する、WITH CHECKという句もある。
これは、挿入しようとする/更新しようとする行の値を条件を定義するもので、条件が偽の場合には、その挿入/更新は実行されない。
ということで、こんなUPDATE用のPOLICY定義を用意する。
CREATE POLICY emp_update_new2working ON workflow FOR UPDATE TO emp USING (status = 'new') WITH CHECK (status = 'working'); CREATE POLICY emp_update_new2checking ON workflow FOR UPDATE TO emp USING (status = 'new') WITH CHECK (status = 'checking');
これは、
- statusが 'new' の行に対して statsusを 'working' に変更する操作を許容する定義
- statusが 'new' の行に対して statsusを 'checking' に変更する操作を許容する定義
になる。
で、empで検索したときに、
[nuko@localhost 9.5-rls]$ psql rls -U emp -c "TABLE workflow" id | jobname | status ----+---------+---------- 1 | job_a | checking 2 | job_b | working 3 | job_c | working 5 | job_e | new (4 rows)
現状はこうなっているので、これに対してUPDATEをかけてみる。
ちなみにidが1,2,3の行はそもそもUPDATE POLICYのUSINGで対象になっていないので、更新操作自体が発生しない(ように見える)。
[nuko@localhost 9.5-rls]$ psql rls -U emp -c "UPDATE workflow SET status = 'complete' WHERE id = 1" UPDATE 0
EXPLAIN ALANYZEをとると、いろんなことが見えてくる。
[nuko@localhost 9.5-rls]$ psql rls -U emp -c "EXPLAIN ANALYZE UPDATE workflow SET status = 'complete' WHERE id = 1" QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------- - Update on workflow workflow_1 (cost=0.15..8.19 rows=1 width=42) (actual time=0.027..0.027 rows=0 loops=1) -> Subquery Scan on workflow (cost=0.15..8.19 rows=1 width=42) (actual time=0.027..0.027 rows=0 loops=1) -> LockRows (cost=0.15..8.18 rows=1 width=42) (actual time=0.027..0.027 rows=0 loops=1) -> Index Scan using workflow_pkey on workflow workflow_2 (cost=0.15..8.17 rows=1 width=42) (actual time=0.025..0.025 rows=0 loops=1) Index Cond: (id = 1) Filter: (status = 'new'::text) Rows Removed by Filter: 1 Planning time: 0.412 ms Execution time: 0.102 ms (9 rows)
- primary keyによるIndexScan結果にFilterで'new'かどうかFilterをかけ、結果として適用対象から1件削除される(0件になる)。
- なので、実行時のrowsも0になる。推定時には1だけど。
・・・ちょい脱線しちゃった。
ではid=5の行(statusは'new)のstatsusを'complete'に変更しようとする。
これは、2つのUPDATEののPOLICYに書いた条件に合致しないのでエラーになる。
rls=> BEGIN; UPDATE workflow SET status = 'complete' WHERE id = 5;ROLLBACK; BEGIN ERROR: new row violates row level security policy for "workflow" ROLLBACK
これが、'working'や'checking'になるとUPDATEは1件成功する。
(実際に値を変更すると後の復旧が面倒なので、今回はBEGIN〜ROLLBACKで囲んで、実際に更新しないようにしている)
rls=> BEGIN; UPDATE workflow SET status = 'working' WHERE id = 5;ROLLBACK; BEGIN UPDATE 1 ROLLBACK rls=> BEGIN; UPDATE workflow SET status = 'checking' WHERE id = 5;ROLLBACK; BEGIN UPDATE 1 ROLLBACK rls=>
ただ、EXPLAIN結果には、USINGの条件は出てくるが、WITH CHECKの条件は出てこない・・・ので、挙動からそう推定しただけではある。
rls=> BEGIN; EXPLAIN ANALYZE UPDATE workflow SET status = 'checking' WHERE id = 5;ROLLBACK; BEGIN QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------- - Update on workflow workflow_1 (cost=0.15..8.19 rows=1 width=42) (actual time=0.039..0.039 rows=0 loops=1) -> Subquery Scan on workflow (cost=0.15..8.19 rows=1 width=42) (actual time=0.023..0.024 rows=1 loops=1) -> LockRows (cost=0.15..8.18 rows=1 width=42) (actual time=0.022..0.023 rows=1 loops=1) -> Index Scan using workflow_pkey on workflow workflow_2 (cost=0.15..8.17 rows=1 width=42) (actual time=0.014..0.015 rows=1 loops=1) Index Cond: (id = 5) Filter: (status = 'new'::text) Planning time: 0.110 ms Execution time: 0.082 ms (8 rows) ROLLBACK
ということで・・・
実は、POLICY定義のINSERT規則やUPDATE規則を組み合わせて、DDLだけでワークフローっぽいことできないかなーと思っていたけど、
複数の(同一テーブル/同一ロール/同一操作)に対するPOLICY定義のUSINGやWITH CHECKがOR接続されちゃうと、ワークフローっぽいチェックは難しそうだなーと思って諦めることになったのであった。
本当は、'new'のときなら、このWITH CHECK、'working'のときならこのWITH CHECK、とかやりたかったけど、簡単にはできそうにないな・・・。