コンテンツにスキップ

アクセス制御設計書 — 機能 × 権限マトリクス

目的: テナント種別(discharger / collector / external_partner)× ロール(admin / normal / driver)で、各機能の利用可否と強制レイヤ(route guard / nav / API middleware / RLS / mobile)を定義する設計書。

作成: 2026-07-07(origin/develop 実コードの横断監査。監査時点 99 migrations)。最終更新: 2026-07-10(設計書ドリフト訂正・監査時点 115 migrations)。file:line は監査時点の origin/develop実装変更時は本書も更新すること(特に RLS ポリシー・ルートガード・middleware)。

関連: 認証・テナント運用ADR-0005 ロールモデルADR-0007 can_drive 能力フラグADR-0017 RLS ハードニング


1. 権限モデル概説

1.1 テナント種別(3 値)

app_metadata.tenant_type に denormalize され、JWT 経由で RLS に反映される。Web は TENANT_TYPES allowlist で受理(未知値は null に落として fail-closed)。

種別 意味 データ所有 実装根拠
discharger 排出事業者(発注側)。案件・マニフェスト発行 自社データを持つ AuthContext.tsx:21, nav-config.tsx:49-67
collector 処理業者(受注側・収集/運搬/処分)。本システムの主要ユーザー 自社データを持つ AuthContext.tsx:21
external_partner 委託先(衛星テナント)。自前業務データを持たず、collector 保持の自社帰属委託料精算を越境読取のみで閲覧。role='admin' 固定 データを持たない AuthContext.tsx:16-21,244-246, TENANT_TYPES = ['discharger','collector','external_partner'] AuthContext.tsx:140
  • 公開サインアップ(POST /signup)は collectordischarger のみ。external_partner は運営スクリプト(invite-org.mjs)でのみ作成(AuthContext.tsx:139 コメント)。

1.2 ロール(3 値)

app_metadata.role 由来。collector 専用の概念で、discharger は現状ロール未設定(null)でも admin 相当として動作する。

ロール 意味 実装根拠
admin テナント管理者。メンバー管理・車両/工場新規作成(mig44)・精算 finalize(mig94)など admin 限定操作 AuthContext.tsx:29,44-46
normal 一般事務スタッフ(collector 専用)。車両/工場の新規作成は不可 AuthContext.tsx:32,35-37
driver 現場ドライバー(collector 専用)。事務操作不可・配車/実績の自分の行のみ AuthContext.tsx:29,60-62
  • COLLECTOR_STAFF_ROLES = ['admin','normal']AuthContext.tsx:32)— driver を除外する「事務ロール」の集合。

1.3 can_drive(能力フラグ・ロールと直交)

app_metadata.can_drive(boolean)。ドライバー画面・配車候補・「本日のルート」ナビの唯一の判定基準。

canActAsDriver(role, canDrive) = role==='driver'
  || ((role==='normal' || role==='admin') && canDrive===true)
AuthContext.tsx:60-62

  • admin + can_drive は admin 権限(メンバー管理・作成等)を一切失わず、運転も兼務できる(AuthContext.tsx:58)。
  • ナビは can_drive=true の normal/admin に「本日のルート」(/driver) を先頭追加する(nav-config.tsx:155-181,206-215)。

1.4 強制レイヤ(多層防御・5〜6 層)

役割 実装
Web ルートガード 種別/ロール/運転能力でページ到達を制御 RoleGuard.tsx:41-144ProtectedRoute が前段の認証ゲート ProtectedRoute.tsx
Web ナビ絞り メニュー表示を種別+role+can_drive で出し分け(表示制御) nav-config.tsx:201-215resolveNav
API middleware エンドポイント認可 auth.ts: requireAuth/requireCollectorAdmin/requireCollectorStaff/requireDischarger/requireManifestCreator
DB RLS 行レベル最終防御(current_tenant_id()/current_user_role() supabase/migrations/**
DEFINER RPC 越境読取のみを制御(RLS をバイパスし body 内で再フィルタ) mig98(委託精算), mig94(finalize/recompute)
Mobile ルーティング ネイティブ起動時の種別ベース封鎖 routing.ts:16-36homeForRole/shouldLockoutMobile

認可の権威は JWT の app_metadata(tenant_id/tenant_type/role/can_drive)。profiles.role/can_drive は表示コピーでドリフト対策(team.ts が両方を更新)。

1.5 ランディング(初期遷移先)

resolveLandingPath(tenantType, role)AuthContext.tsx:231-249):

種別/ロール 着地先
discharger /discharger
collector + driver /driver
collector + admin/normal/null /collector
external_partner /external-partner/settlements
不整合(tenant_type 未設定) null(未割当エラー)
  • Mobile: discharger→(discharger) / collector+driver→(driver) / collector+staff→(collector) / それ以外(external_partner・null・未知)→(locked) で起動時 signOut(fail-closed・routing.ts:20-35)。
  • 特例(error-no-role): collector かつ role=null の拒否だけはリダイレクトすると /collector へ戻り無限ループになるため、Navigate せずインラインのエラー画面を出す(RoleGuard.tsx:87-101,123-141)。

2. 機能一覧(業務領域別)

各機能に (対象テナント種別, 対象ロール, web/mobile, 主な強制箇所) を付す。

2.1 認証・組織オンボーディング

機能 種別 ロール web/mobile 主な強制
サインイン 全(公開) 不問 web/mobile ガードなし App.tsx:148
組織登録(サインアップ) collector, discharger 公開 web App.tsx:149POST /signup(招待コード照合 + register_signup_invite_attempt DEFINER RPC・原子的試行上限)signup.ts / mig48・mig50
Auth コールバック 全(公開) web ガードなし App.tsx:150

2.2 メンバー・組織管理

機能 種別 ロール web/mobile 主な強制
メンバー一覧画面 collector admin/normal(閲覧)※書込導線は admin web App.tsx:347-355allowedRoles=COLLECTOR_STAFF_ROLES
メンバー作成/招待 collector admin web→API POST /team/members team.ts:177 + requireCollectorAdmin auth.ts:66-75
パスワード初期化 collector admin API team.ts:248
ロール変更 collector admin API team.ts:290 + 最後の admin ロック(409・TOCTOU 対策)
can_drive 変更 collector admin API team.ts:400
メンバー削除 collector admin API team.ts:469 + 最後の admin ロック
会社情報/プロフィール collector, discharger collector: admin/normal web App.tsx:325-333/collector/profile

RLS: profiles は SELECT がテナント全開放mig8:38-40)、UPDATE/DELETE は admin のみ(mig8:47-63)。書込 API は service_role 経由。

2.3 案件(Jobs・discharger)

機能 種別 ロール web/mobile 主な強制
案件一覧/新規/詳細 discharger admin 相当(role 不問) web/mobile App.tsx:163-192allowedTenantType='discharger')。Mobile (discharger) group
見積(AI マッチング) discharger web App.tsx:194-207ComingSoonPage(準備中)

2.4 マニフェスト・OCR

機能 種別 ロール web/mobile 主な強制
排出側 マニフェスト一覧/新規/詳細 discharger admin 相当 web/mobile App.tsx:208-236allowedTenantType='discharger'
自社マニフェスト一覧 collector admin/normal web App.tsx:303-312
自社マニフェスト新規(KYO-104 Phase A) collector admin/normal(driver 除外) web App.tsx:315-324
マニフェスト OCR(Claude/Vision 課金) discharger, collector discharger: 全 / collector: admin|normal API POST /manifests/ocr manifests.ts:42 + requireManifestCreatorcanCreateManifest auth.ts:143-164

RLS carve-out(mig88:182-197 INSERT / :199-222 UPDATE): tenant_id 一致 AND (tenant_type='discharger' OR role IN ('admin','normal'))。driver は弾かれる。API canCreateManifest はこの判定面と一致。collector が担当段階(carrier/disposal)を stamp する UPDATE は mig89(ja.status in ('accepted_by_discharger','completed') の担当割当が前提・列制限はガードトリガ)。

2.5 取引先・委託先マスタ

機能 種別 ロール web/mobile 主な強制
取引先(partners)一覧/詳細/CSV 取込(本体・回収先) collector admin/normal web App.tsx:357-397(COLLECTOR_STAFF_ROLES)。RLS mig87 書込 admin|normal
委託先(external_partners)一覧/詳細 collector admin/normal web App.tsx:399-408,454-463。RLS mig91:72-100(SELECT/INSERT/UPDATE=admin|normal・DELETE=admin ソフトデリート)
委託先 CSV 取込(委託先/支払単価/車両リース) collector admin/normal web App.tsx:411-440
discharger 側 処理業者一覧/詳細 discharger admin 相当 web App.tsx:238-256

RLS mig92: 金額 2 表(external_partner_item_rates/vehicle_leases)は SELECT も書込も admin|normal のみ(driver は 0 行・原価秘匿 mig92:12-16)。external_partner_drivers(タグ付け)は staff=テナント全体・driver=自分の active 所属のみ(mig99:88-109 で封鎖)。

2.6 配車・ドライバー現場

機能 種別 ロール web/mobile 主な強制
配車ボード collector admin/normal web App.tsx:563-572(COLLECTOR_STAFF_ROLES)
コースビルダー collector admin/normal web App.tsx:578-587
本日のルート collector driver | can_drive な normal/admin web/mobile App.tsx:590-598allowDriverCapable)。Mobile (driver)/index
運行ログ collector 同上 web/mobile App.tsx:610-618。Mobile (driver)/daily-log
履歴 collector 同上 web/mobile App.tsx:600-608
工場ボード collector 同上 web/mobile App.tsx:620-628

RLS dispatch_stops: 書込 admin|normal(mig41:29-31)、SELECT はテナント全件"tenant reads dispatch stops" mig41:35-36・mig55/mig72 で再宣言)、driver の自 stop UPDATE は列をガードトリガで status/completed_at/note に限定(mig25:92-108)。→ §4 の受容リスク参照。

2.7 計量(weighings・法定記録)

機能 種別 ロール web/mobile 主な強制
計量一覧/新規/CSV 取込 collector admin/normal web App.tsx:464-483(COLLECTOR_STAFF_ROLES)
自分の計量実績(driver 可視化・KYO-106 Wave C) collector driver | can_drive な normal/admin web/mobile App.tsx:630-638allowDriverCapable)→ DriverWeighingsPage(notes 除外)

RLS(mig99・二段変更): weighings/weighing_items SELECT を「staff 限定(admin|normal・mig99:27-33,55-65)」+「driver-own 派生(attribution_driver_id→profiles で自分のみ・mig99:37-49,69-83)」に分割。permissive OR 合成漏れを避けるため既存 SELECT を staff 限定へ絞ってから driver-own を加算。INSERT は collector(mig66:51-53)。complete_weighing RPC は SECURITY INVOKER。

2.8 車両・コスト

機能 種別 ロール web/mobile 主な強制
車両一覧/詳細 collector admin/normal web App.tsx:498-507
車両 CSV 取込(新規作成含む) collector admin のみ web App.tsx:510-519allowedRole="admin")。RLS mig44 vehicles INSERT=admin
コスト集計/新規 collector admin/normal web App.tsx:520-539
請求書 OCR 取込(KYO-100・Claude 課金) collector admin/normal(PR#139 で admin 限定→開放) web→API App.tsx:543-552 + POST /vehicle-costs/ocr vehicle-costs.ts:26 + requireCollectorStaff auth.ts:87-96。RLS mig43 INSERT=admin|normal
車両日報 新規 collector admin/normal web App.tsx:553-561

RLS: vehicles/factoriesINSERT(新規作成)は admin のみ(mig44:46-52)。UPDATE/DELETE・日報・コストは admin|normal(mig43)。vehicle_cost_entries は admin=全車両、driver=自担当車両のみ SELECT/INSERT/UPDATE(mig22:67-145)。

2.9 締め・委託精算

機能 種別 ロール web/mobile 主な強制
委託精算(collector 所有・Phase2) collector admin/normal(閲覧・recompute・adjustment)/finalize=admin web App.tsx:444-452/collector/external-partner-settlements)。RLS mig94 SELECT=admin|normal(:214)/直接 INSERT/UPDATE/DELETE=全て false:216-222
精算 recompute / adjustment collector admin/normal DEFINER RPC mig94 recompute_*:281 staff ゲート)/ add_*_adjustment(admin|normal)
精算 finalize(draft→finalized) collector admin のみ DEFINER RPC mig94 finalize_external_partner_settlement:447 is distinct from 'admin' → raise・NULL fail-closed)
委託精算 越境読取(external_partner 側・Phase3) external_partner admin(固定) web/mobile 不可 App.tsx:271-279/external-partner/settlements)→ DEFINER RPC mig98
月次集計 collector admin/normal web /collector/monthlyApp.tsxCollectorMonthlyPage ルート)。ナビからは PR #207 でメニュー除外済nav-config.tsx:109 コメント「月次集計は準備中のため一旦メニューから除外…ルート実体は温存」)=status:'soon' ではなく nav 項目自体が非掲載。ルート実体・URL 直打ちは温存

精算不変性は方針B=スナップショット: 金額 4 列は RLS 全面 false + eps_guard_finalized トリガ(finalized→finalized の金額改竄を authenticated 直書きで遮断・mig94:182-202)+ recompute 本体の if status='finalized' then raise

2.10 外部パートナー(衛星テナント・越境読取)

機能 種別 ロール web/mobile 主な強制
委託精算サマリ/明細 閲覧 external_partner admin(固定) web のみ App.tsx:271-279allowedTenantType='external_partner'allowedRole付けない=role='admin' 固定運用)。専用ナビ EXTERNAL_PARTNER_NAVnav-config.tsx:75-80・fail-open G7 是正)。Mobile は (locked) 封鎖

越境読取は DEFINER RPC のみ(mig98): - get_external_partner_settlement_summary(date) / _items(date) — GET 相当・引数に tenant を取らない(越権封じ)。 - gate: coalesce(tenant_type='external_partner' AND tenant_id is not null, false) is not true → '[]'(fail-closed・裸比較の NULL fail-open を回避 mig98:35-36)。 - body 内で ep.external_tenant_id = current_tenant_id()ep.deleted_at is nullstatus='finalized' を再明示(RLS バイパスのため)。draft/soft-delete/unlink は遮断。 - 共栄内部列(driver_id/attribution_driver_id/note/原価・vehicle_lease 明細・生 sign 列・quantity)は非返却。sign は signed_amount = amount×sign に潰す(mig98:76)。

2.11 設定・課金

機能 種別 ロール web/mobile 主な強制
設定(一般/通知/セキュリティ) 全(ロール制約なし) web App.tsx:642-664ProtectedRoute のみ・RoleGuard なし)
請求・サブスク collector, discharger(external_partner 除外) admin web→API App.tsx:667-679allowedTenantTypes=['collector','discharger'] + allowedRole='admin')。API billing.tsisExternalPartnerTenant→403(auth.ts:113-115
JWNET 連携 discharger admin 相当 web App.tsx:267-275/discharger/jwnet)。ナビ status:'soon'・UI 到達不可・ページ実体温存(nav-config.tsx:54-60

3. 権限マトリクス(主要機能 × テナント×ロール)

凡例: ○=可 / ×=不可 / △=条件付き(脚注)/ —=そのロールは当該種別に存在しない

機能 col-admin col-normal col-driver dis-admin ep-admin
ダッシュボード(自種別) ○(driver) ○(settlements)
案件(Jobs) × × × ×
マニフェスト作成/OCR ×¹ ×
マニフェスト閲覧 △² ×
取引先マスタ × × ×
委託先マスタ × × ×
配車ボード/コース ׳ × ×
配車 stop 閲覧(RLS) ○⁴
ドライバー画面(本日/実績/ログ/工場) △⁵ △⁵ × ×
計量 一覧/新規(staff 面) × × ×
自分の計量実績(driver 可視化) △⁵ △⁵ ○⁶ × ×
車両 新規作成/CSV 取込 ×⁷ × × ×
車両 閲覧/コスト/OCR/日報 ×⁸ × ×
委託精算(collector・閲覧/recompute) × × ×
委託精算 finalize ×⁹ × × ×
委託精算 越境読取(衛星) × × × ×
メンバー管理(作成/ロール/削除) ×¹⁰ × ×¹¹ ×
請求・課金設定 × × ×¹²
設定(一般/通知/セキュリティ)
JWNET 連携 △¹³

※列: col=collector / dis=discharger / ep=external_partner。dis-normal / dis-driver / ep-normal / ep-driver / ep-driver は実運用上存在しない(discharger は role 未設定=admin 相当、external_partner は admin 固定)ため列から省略。

脚注 1. マニフェスト作成/OCR: driver は canCreateManifest で false(auth.ts:143-150)・RLS mig88 carve-out も admin|normal のみ。 2. マニフェスト閲覧: collector driver は SELECT ポリシー依存(staff/driver 面の詳細は各表 RLS)。creation は不可。 3. 配車ボード(web): allowedRoles=COLLECTOR_STAFF_ROLES で driver は到達不可。ただし RLS 上の stop 閲覧は別(脚注4)。 4. 配車 stop 閲覧(RLS): dispatch_stops SELECT は "tenant reads dispatch stops"mig41:35-36)でテナント全件開放 → driver も RLS 上は全 stop を読める。UI は配車ボードを staff に絞るだけ(受容リスク・§4)。 5. can_drive=true な admin/normal はドライバー画面・自分の計量実績にアクセス可(canActAsDriver AuthContext.tsx:60-62)。can_drive=false なら不可。 6. 自分の計量実績: driver は RLS 上 attribution_driver_id=自分 の行のみ(mig99:37-49)。 7. 車両新規作成/CSV 取込: normal は RLS mig44(INSERT=admin)で 0 行 + web allowedRole="admin"App.tsx:514)。 8. 車両コスト等: driver は自担当車両のみ(mig22)だが web の staff 面(/collector/fleet/*)は COLLECTOR_STAFF_ROLES で到達不可。 9. finalize: mig94:447 で admin 以外は raise(NULL fail-closed)。normal は recompute/adjustment までは可。 10. メンバー管理書込: requireCollectorAdminauth.ts:66-75)で normal は 403。閲覧画面は staff で到達可。 11. discharger のメンバー管理 API は requireCollectorAdmintenant_type==='collector' を要求するため discharger は 403(現状 collector 専用機能)。 12. external_partner は allowedTenantTypes から除外(web)+ API isExternalPartnerTenant→403(auth.ts:113-115)。 13. JWNET はナビ status:'soon' で UI 到達不可(機能未提供・ページ実体は温存)。


4. RLS レベルの補足(UI で隠れても DB で防がれる/防がれない点)

4.1 UI 制限だけで RLS はテナント全開放(受容リスク)

テーブル RLS SELECT UI 制限 リスク
profiles テナント全開放(mig8:38-40・role 述語なし) メンバー画面は staff 到達 同一テナントの authenticated(driver 含む)が API を直叩けば全メンバーの表示名/ロール/login_id 相当を読める。書込は admin のみで防御。受容(同一組織内の名簿共有は許容範囲)
dispatch_stops テナント全開放("tenant reads dispatch stops" mig41:35-36 配車ボードは COLLECTOR_STAFF_ROLES driver も RLS 上は全 stop を読める(自分の割当外含む)。mig41 コメントが意図的な設計と明記。書込は admin|normal(mig41:29-31)+ driver 自 stop の限定列 UPDATE(mig25:92-108)で防御。受容
dispatch_route_templates テナント全開放(mig41:24-25 同上 同上。受容

これらは「UI で隠れていても RLS では防がれない」典型。ただし書込は role/自行に限定されており、越境(他テナント)は tenant_id = current_tenant_id() で常に遮断される。

4.2 UI で隠れており RLS でも防がれる(多層一致)

  • 金額秘匿(driver 除外): weighings/weighing_items(mig99 staff 限定+driver-own)、external_partner_item_rates/vehicle_leases(mig92 admin|normal のみ・driver 0 行)、external_partner_settlements(mig94:214 SELECT=admin|normal)。UI ガードだけでなく RLS でも driver に金額を見せない。
  • 越境(テナント跨ぎ): 全表 tenant_id = current_tenant_id()。external_partner の越境読取は通常 RLS では構造的に不可能で、DEFINER RPC(mig98)のみが唯一の経路。RPC は finalizeddeleted_at is null・自社紐付けを body 内で再適用し fail-closed で [] を返す。
  • 精算改竄: 金額列 RLS 全面 false + finalized 後は eps_guard_finalized トリガ(authenticated 直書き遮断)+ recompute の finalized-raise。UI に finalize ボタンが無くても DB が防ぐ。
  • 車両新規作成: RLS mig44(INSERT=admin)が web allowedRole="admin" と一致。normal は RLS 上も INSERT 0 行。

4.3 fail-closed の徹底

current_tenant_id()/current_user_role() は JWT claim 欠落時 NULL を返し、=/in の 3 値論理で行が落ちる。DEFINER RPC の gate は coalesce(..., false)is distinct from で NULL fail-open を明示的に潰している(mig98:35-36, mig94:447)。


5. レイヤ不整合の指摘

実装は概ね多層一致だが、以下は「層ごとに強制水準が異なる」箇所として設計上把握しておくべき点。

  1. 配車 stop 閲覧: nav/route は staff 限定・RLS はテナント全開放(意図的)
  2. Web は /collector/dispatch* を COLLECTOR_STAFF_ROLES に絞る(App.tsx:563-587)が、RLS dispatch_stops SELECT はテナント全件(mig41:35-36)。driver は API 直叩きで全 stop を読める。mig41 が「SELECT=テナント全件」を明示設計しておりバグではなく受容リスク。ただし将来 driver に自 stop のみ見せたい要件が出たら RLS 側の絞り込み(mig99 の weighings と同型の staff+driver-own 二段化)が必要。

  3. profiles: 名簿がテナント全 authenticated に開放

  4. メンバー管理 UI は staff 到達だが、profiles SELECT は role 述語なし(mig8:40)。driver も全メンバーを読める。書込は admin のみで実害は限定的だが、RLS 単体では「driver に他メンバー情報を見せない」保証はない(UI と RLS の水準差)。

  5. external_partner の role 固定は「運用規約」で機械強制されない層がある

  6. Web ルートは allowedTenantType='external_partner' のみで allowedRole を付けない(App.tsx:270-279・G9 コメント)=role='admin' 固定は「衛星は admin でしか作られない」運用前提。もし将来 external_partner に normal/driver を発行すると、ルートガードは素通りする(越境データは DEFINER RPC が gate するため実害はないが、種別だけで通す設計であることを明記)。

  7. 課金除外は Web/API 二重で一致(不整合なし・良い例)

  8. external_partner の billing 除外は Web(allowedTenantTypes 除外)と API(isExternalPartnerTenant→403)の両層。多層防御が正しく効いている参考例。

  9. discharger のロール未設定(null)を admin 相当で通す設計

  10. discharger 系ルートは allowedRole を付けず role=null を許容(resolveLandingPath も /discharger へ)。collector と異なり error-no-role 特例の対象外(RoleGuard.tsx:96-99)。discharger に将来ロール概念を入れる場合はこの暗黙 admin 相当を明示化する必要がある。

検証根拠(file:line は origin/develop・当初 99 migrations で実確認・2026-07-10 に 115 migrations で再点検)

  • 3 値テナント: AuthContext.tsx:16-21,140 / mig97-99 実在(ls supabase/migrations = 115 本・2026-07-10 時点)
  • ルート表: App.tsx:148-703(全ルート実読)
  • ガード純粋関数: RoleGuard.tsx:41-101
  • API middleware: auth.ts:66-164
  • ナビ: nav-config.tsx:49-215
  • Mobile: routing.ts:16-36
  • RLS: mig8(profiles)/ mig25・41・55・72(dispatch_stops)/ mig44(vehicles admin)/ mig88-89(manifests)/ mig92(rates/leases/drivers)/ mig94(settlements finalize)/ mig98(越境 RPC)/ mig99(driver 計量可視化)
  • team API: team.ts:177,248,290,400,469

矛盾採用: レンズ間で異なっていた「dispatch_stops driver SELECT」は、mig25 の driver-only ポリシーが mig27→mig41 で "tenant reads dispatch stops"(テナント全開放)に置換されているため最新の mig41:35-36 を採用(一部レンズが mig25 の古い記述を引いていたのを訂正)。stale tree(ad4347/mig96/2値テナント)は一切参照していない。