コンテンツにスキップ

0006. 工場スコープのアクセス制御 (profile_factories)

コンテキストと課題

処理業者は複数の 工場 (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 / driverprofile_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 では:

  1. 既存の広い SELECT と FOR ALL ポリシーを DROP
  2. 書込を INSERT / UPDATE / DELETE の個別ポリシーへ分割 (authorization は一字一句同じに維持)、
  3. 別途、工場スコープの 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-50factories テーブルと初期 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-63profile_factories 書込を FOR ALL から INSERT/UPDATE/DELETE へ分割し admin 限定化。
  • docs/superpowers/specs/2026-06-19-factory-scoped-access-design.md — 設計書 (migration 43 が参照)。