0006. 工場スコープのアクセス制御 (profile_factories)¶
- ステータス: Accepted(実装済み)
- 関連: 0003 マルチテナント分離 / 0005 ロールモデル / 0007 can_drive 能力フラグ / 0012 日次運行ログ
コンテキストと課題¶
処理業者は複数の 工場 (factory) を持ち、車両・日次運行ログ・燃料費などは工場に紐付く。 事務スタッフ (normal) やドライバー (driver) には「自分の所属工場のデータだけ」を見せ、管理者 (admin) には全工場を見せたい。テナント分離 (ADR 0003) だけでは 「テナント内をさらに工場で絞る」ことはできない。
決定¶
メンバーと工場の M:N (profile_factories) を導入し、RLS で工場スコープの可視範囲を定義する。
profile_factories(migration 33):(profile_id, factory_id)を主キーとする join テーブル。 トリガでtenant_idを補完し、profile と factory が同一テナントであることを強制する。- 可視範囲ルール (migration 43):
admin… テナント内 全工場。normal/driver…profile_factoriesに登録された 所属工場のみ。- 燃料費 (
vehicle_cost_entries) …admin+normalのみ (driver は除外)。
- 判定は SECURITY DEFINER ヘルパーに集約する:
-- migration 43: テーブル横断参照のため security definer (set search_path = public)
create function public.current_user_can_access_factory(p_factory_id uuid)
returns boolean language sql stable security definer set search_path = public as $$
select exists (
select 1 from public.factories f
where f.id = p_factory_id and f.tenant_id = public.current_tenant_id()
and ( public.current_user_role() = 'admin'
or ( public.current_user_role() in ('normal','driver')
and exists ( select 1 from public.profiles p
join public.profile_factories pf on pf.profile_id = p.id
where p.user_id = auth.uid() and pf.factory_id = f.id ) ) )
);
$$;
このヘルパーが profiles / profile_factories を横断参照するため、JWT だけを読む他のヘルパー
(ADR 0003) と違い SECURITY DEFINER が必要になる。
permissive-OR の落とし穴と「FOR ALL の分割」¶
RLS の複数ポリシーは permissive (OR) で評価される。可視範囲を 狭める には、既存の広い
SELECT ポリシーを必ず DROP してから工場スコープ版を作る (追加するだけでは絞れない)。
さらに重要な罠として、FOR ALL の permissive ポリシーは USING 式が SELECT にも適用される。
そのため admin|normal 向けの「書込」を FOR ALL で書くと、その USING
(= tenant + admin|normal) が テナント全体の SELECT を許してしまい、工場スコープの
SELECT 制限を打ち消す。これを避けるため migration 43 / 46 では:
- 既存の広い SELECT と
FOR ALLポリシーをDROP、 - 書込を
INSERT/UPDATE/DELETEの個別ポリシーへ分割 (authorization は一字一句同じに維持)、 - 別途、工場スコープの
SELECTポリシー を張る、
という形に再構成している。
工場スコープ READ 化の副作用 (admin 限定書込)¶
- 新規作成を admin 限定化 (migration 44): SELECT を工場スコープ化すると、normal が「自分が
未所属の新規 factory / vehicle」を INSERT した瞬間、その行は SELECT スコープ外で不可視になり、
PostgREST の
.insert().select()(RETURNING) が42501で失敗する。これを避けるため factory / vehicle の INSERT を admin 限定 にした。 - 所属工場の付与を admin 限定化 (migration 46):
profile_factoriesの書込 (FOR ALL admin|normal) をINSERT/UPDATE/DELETEの個別ポリシーへ分割し、いずれも admin 限定 に絞った (メンバーの所属工場の付与・解除は管理者のみ)。
影響 (Consequences)¶
良い点¶
- 「所属工場だけ見える」を DB の RLS で強制 でき、アプリ層の絞り込み漏れに依存しない。
- 工場アクセス判定が
current_user_can_access_factory()/current_user_can_access_vehicle()に 集約され、複数テーブル (factories / vehicles / daily logs / cost entries) で再利用できる。 - 期待動作は RLS 検証スクリプト (
scripts/verify-factory-scope-rls.mjs等) で確認できる。
トレードオフ / 注意点¶
- RLS ポリシーが複雑化する。
FOR ALLを使えず INSERT/UPDATE/DELETE を個別に書くため ポリシー数が増え、可読性・保守性のコストが上がる。新規テーブルを工場スコープ化する際は 同じ「DROP → 分割 → スコープ SELECT」の手順を踏む必要がある。 - 工場スコープ READ の副作用として 作成系が admin 限定 に寄り、normal の権限が想定より狭くなった (副作用ドリブンの権限調整)。
current_user_can_access_factory()は SECURITY DEFINER なので、search_path固定など 権限昇格を伴う関数の取り扱いに注意が要る。
根拠 (典拠)¶
supabase/migrations/00000000000020_factories.sql:7-50—factoriesテーブルと初期 RLS (tenant read / admin FOR ALL)。supabase/migrations/00000000000033_profile_factories.sql:1-40— M:N テーブル、tenant 補完 + 同一テナント強制トリガ、初期 RLS (staff FOR ALL admin|normal)。supabase/migrations/00000000000043_factory_scoped_read.sql:1-27,38-64,106-147,254-259— permissive-OR と FOR ALL 分割の設計コメント、current_user_can_access_factory()(security definer)、factories の DROP→分割→スコープ SELECT、cost entries の admin|normal 限定。supabase/migrations/00000000000034_daily_logs_rls_profile_factories.sql— 日次ログの工場スコープ化。supabase/migrations/00000000000044_admin_only_create_factory_vehicle.sql:1-39— 工場スコープ READ の副作用 (42501) と factory/vehicle の INSERT admin 限定化。supabase/migrations/00000000000046_profile_factories_admin_only_write.sql:1-63—profile_factories書込を FOR ALL から INSERT/UPDATE/DELETE へ分割し admin 限定化。docs/superpowers/specs/2026-06-19-factory-scoped-access-design.md— 設計書 (migration 43 が参照)。