From af80cc58a6cf8825229c4715bbebf52df2d723f2 Mon Sep 17 00:00:00 2001 From: yufoxda Date: Wed, 9 Sep 2026 21:09:56 +0900 Subject: [PATCH] docs: schedule closing the role-grant escalation gap before U-3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A review of U-1 found the new policies on member_roles trust the role the application claims, and nothing else: with check (current_setting('app.current_user_role', true) = 'admin') members is not defended that way. It carries the enforce_member_workflow trigger alongside its policies, which checks the claimed role against what app_accounts actually says, so a compromised application layer cannot promote itself. That is what constraint C-4 is for. member_roles is where every permission now comes from — a higher-value target than members — and has the weaker of the two defences. Nothing reads the table until U-3 moves the checks across, so there is no exposure today, but U-3 is exactly the point where this stops being theoretical. It becomes U-2.5, ordered before it. Also corrects two things in U-1 itself. Its requirement list claimed FR-4.1, which it does not implement: there is no way to create or delete a role, only select is granted, and role management is U-5's work. And the is_system comment described an enforcement that does not exist, while the ON CONFLICT clause on the admin backfill could never fire, since app_accounts.member_id is unique. Co-Authored-By: Claude Opus 4.8 --- docs/aidlc/unit-of-work.md | 31 +++++++++++++++++-- .../20260904000000_add_role_tables.sql | 3 +- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/aidlc/unit-of-work.md b/docs/aidlc/unit-of-work.md index 13f1b7d..0a5feef 100644 --- a/docs/aidlc/unit-of-work.md +++ b/docs/aidlc/unit-of-work.md @@ -25,7 +25,7 @@ V1 を実装可能な単位に分ける。各単位は**単独で本番に出せ | | | | --- | --- | -| 満たす要件 | FR-4.1・4.2・4.3・4.6 | +| 満たす要件 | FR-4.2・4.3・4.6 | | 内容 | ロール・権限・部員ロール・ロール権限のテーブルを作る。部員でない相手に付けられない制約を入れる | | 影響 | 追加のみ。既存の動作は変わらない | | 完了条件 | 現在の管理者2名に相当するロールと割り当てが入っており、`app_accounts.role` と一致する | @@ -41,6 +41,31 @@ V1 を実装可能な単位に分ける。各単位は**単独で本番に出せ | 影響 | 追加のみ。まだ誰も使わない | | 完了条件 | 検査が動き、存在しない権限名を書くと失敗する | +### U-2.5 ロール付与を偽装できないようにする + +| | | +| --- | --- | +| 満たす要件 | C-4、NFR-1.1 | +| 内容 | `member_roles` への書き込みを、アプリが申告したロールだけで許さない。`members` と同じく、データベース側で `app_accounts`(U-4 以降は `member_roles` 自身)の実値と突き合わせる | +| 影響 | 追加のみ | +| 完了条件 | 申告だけでは付与できない。突き合わせに失敗した書き込みが拒否される | + +**U-3 より前に必ず終わらせる。** U-1 のレビューで見つかった不足への対応。 + +U-1 が入れたポリシーは `app.current_user_role` の値だけを見ている。 + +```sql +with check ((select current_setting('app.current_user_role', true)) = 'admin') +``` + +`members` にはこれに加えて `app_private.enforce_member_workflow` があり、 +申告されたロールを `app_accounts` の実値と突き合わせている。アプリ層が侵害されても +権限を偽装できないようにするためで、これが C-4 の趣旨である。 + +`member_roles` は全権限の源泉であり、`members` より価値の高い標的でありながら、 +現状その突き合わせがない。U-3 で判定を移すまでは誰もこのテーブルを使わないため +現時点の危険はないが、U-3 の前提として閉じておく必要がある。 + ### U-3 判定を置き換える | | | @@ -137,7 +162,7 @@ V1 を実装可能な単位に分ける。各単位は**単独で本番に出せ 依存関係で決まる部分と、選べる部分がある。 ``` -U-1 → U-2 → U-3 → U-4 ロールの土台(この順でなければならない) +U-1 → U-2 → U-2.5 → U-3 → U-4 ロールの土台(この順でなければならない) ↓ U-5, U-6 引き継ぎと退部連動(U-4 完了後) @@ -151,7 +176,7 @@ U-10 → U-11 分離(最後。他が安定してから) ### 推奨する順 1. **U-9** — 先にやると U-3 での衝突が減る。外から見た挙動が変わらないため安全 -2. **U-1 → U-2 → U-3 → U-4** — ロールの土台 +2. **U-1 → U-2 → U-2.5 → U-3 → U-4** — ロールの土台 3. **U-5・U-6** — 引き継ぎ。ここで運用上の欠陥が解消する 4. **U-7 → U-8** — 表示と一覧 5. **U-10 → U-11** — 分離 diff --git a/supabase/migrations/20260904000000_add_role_tables.sql b/supabase/migrations/20260904000000_add_role_tables.sql index 40834f1..65006dd 100644 --- a/supabase/migrations/20260904000000_add_role_tables.sql +++ b/supabase/migrations/20260904000000_add_role_tables.sql @@ -126,8 +126,7 @@ insert into public.member_roles (member_id, role_key, granted_by_user_id) select account.member_id, 'admin', 'system:20260904000000' from public.app_accounts account join public.members member on member.member_id = account.member_id -where account.role = 'admin' -on conflict do nothing; +where account.role = 'admin'; grant select on public.permissions to app_rls; grant select on public.app_roles to app_rls;