技術ガイド
Supabaseのインポート、RLS、テナント関連フィールド
CSVの値とテナントに関する権限を分離します。ユーザーJWT、service-roleでのアクセス、RLS適用時のPostgreSQL COPYの制限を理解します。
インポート先のテナントは、認証と認可を経たアプリケーションの判断から取得します。顧客が行う列マッピングの対象からは除外してください。そのうえで、実際に実行するロールのもと、取り込み先で許可された書き込みだけを認めます。
RLSの有効化は、その設計の一部にすぎません。ユーザーJWTを持つSupabase APIリクエスト、service-roleの権限を使うサーバーリクエスト、データベース所有者のpsqlセッションは、同じ権限を持ちません。ダッシュボードでインポートできても、顧客のセッションから実行できることや、テナント分離が機能したことの証明にはなりません。
アップロード手順だけが必要なら、CSVをSupabaseにインポートする方法から始めてください。本ページでは、そうした書き込みの背後にある権限の境界を扱います。
対象範囲と前提条件. PostgreSQL 17とSupabase Auth/Data APIの動作を対象とします。ポリシーは説明用の例であり、記載したメンバーシップと権限付与のモデルを前提とします。
最初に実行経路を特定する
| 経路 | 確認すべき権限 | 書き込みの成功が証明すること |
|---|---|---|
| サインイン済みユーザーのJWTを使うData API | 実効的な認証済みロール、JWTクレーム、付与された権限、ポリシー | その時点で、リクエストが適用対象の検査を通過したこと |
| service-roleの認可を使うバックエンド | 特権ロールとバックエンド自身の認可処理 | RLSが適用されたと推測しないこと。service_roleはBYPASSRLSを持つ |
| ダッシュボード/特権を持つデータベース接続 | 実際のセッションロール、所有権、迂回に関する属性 | 通常のアプリケーションユーザーがアクセスできる証拠にはならない |
| 権限範囲を限定した、顧客が操作するデータベースログイン | ロールへの権限付与、テーブル所有権、適用されるRLS、セッションのコンテキスト | そのログインで実際に使われた権限と検査のみ |
SupabaseのAPIキーガイドでは、publishable/anonアクセスとsecret/service-roleアクセスを区別しています。RLSガイドでは、クライアントに関する重要な点も説明しています。service keyで初期化したクライアントでも、認可情報がサインイン済みユーザーのセッションで置き換わると、そのユーザーのRLSコンテキストに従う場合があります。生成時に使ったキーだけでなく、実効的なAuthorizationの認証情報を確認してください。
PostgreSQLでは、スーパーユーザーとBYPASSRLSを持つロールは行セキュリティを迂回します。テーブル所有者も、所有者にFORCE ROW LEVEL SECURITYが適用されない限り、通常は迂回します。FORCEはスーパーユーザーやBYPASSRLSロールを制限しません。そのため、テーブル所有者としてのみテストするのでは不十分です。
権限付与とRLSは、異なる問題を扱います。ロールには対象テーブルまたは列の適切な権限が必要であり、RLSが適用される場合は、行ポリシーもその操作を許可しなければなりません。アクセスを許可する適用可能なポリシーがない場合、RLSが有効なテーブルは通常のロールに対してデフォルトで拒否します。PostgreSQLの行セキュリティ規則を参照してください。
1つのインポートを認可済みのテナントに結び付ける
創業者がワークスペースの連絡先をインポートする場面を考えます。CSVには、以前のCRMのaccount_idが含まれています。自社のデータベースにもアカウント識別子があります。旧システムの値は外部参照として役立つかもしれませんが、インポーターが自社システムの対応するワークスペースに書き込んでよい証拠にはなりません。
アプリケーションは操作者を認証し、選択したワークスペースへのインポート権限を確認し、そのワークスペースをインポートジョブに結び付けるべきです。承認済みの行マッピングには、メールアドレスや表示名などのフィールドだけを含めます。テナントのコンテキストは、書き込み処理が独立して設定します。
subject = authenticate(request)
tenant = authorizeImport(subject, selectedWorkspace)
job = createJob({ tenant, actor: subject.id, contractVersion })
for each submitted row:
reject keys outside the approved source fields
validate source values again on the server
write approved values with tenant = job.tenant認証だけでは認可になりません。サインイン済みでも選択したワークスペースに所属していないユーザーや、所属していてもインポート権限がないユーザーがいます。この操作の権限を確認し、権限の取り消しが待機中のジョブにどう影響するかを定義してください。昨日有効だったセッションを根拠に、長時間のジョブへ暗黙に無期限の権限を与えてはいけません。
ユーザーが編集できるメタデータを、テナントに関する権限の根拠にしてはいけません。JWTクレームを使う場合は、発行者、変更できる主体、古くなった所属情報の扱いを明確にします。署名済みのクレームが役立つかどうかは、それを作成した信頼できるプロセス次第です。
WITH CHECKで新しい行を制限する
次のポリシーは、信頼できるメンバーシップテーブルに、各テナントへインポートできる人を記録するアプリケーションの例です。public.contacts(tenant_id uuid, email text)が存在し、両列がNOT NULLであるとします。また、public.tenant_memberships(tenant_id uuid, user_id uuid, can_import boolean)は、ユーザーが自分で権限を付与できないよう保護され、SELECT権限とポリシーによってユーザーが自身の所属情報を読めるものとします。
ALTER TABLE public.contacts ENABLE ROW LEVEL SECURITY;
CREATE POLICY contacts_member_import
ON public.contacts
FOR INSERT TO authenticated
WITH CHECK (
EXISTS (
SELECT 1
FROM public.tenant_memberships AS membership
WHERE membership.tenant_id = contacts.tenant_id
AND membership.user_id = (SELECT auth.uid())
AND membership.can_import
)
);これはポリシーの一部であり、完全な導入手順ではありません。ロールには、適切なINSERT権限も必要です。リクエストが行を返す場合は、その応答に必要なSELECT権限とポリシーも確認してください。INSERTポリシーはWITH CHECKを使って挿入予定の行を検査します。SELECTポリシーでは代用できません。
既存のポリシーをすべて確認します。許容的ポリシーは通常ORで結合されるため、広いアクセスを認める許容的なINSERTポリシーと並べてこのポリシーを追加しても、アクセスを狭められないことがあります。制限的ポリシーは結合方法が異なり、適用可能な許容的ポリシーも必要です。適切に見える式1つを境界のすべてとみなさず、ポリシー全体を評価してください。
この所属確認は、認可されたテナントへの操作を許可しますが、リクエストを事前に選択した1つのジョブのテナントには結び付けません。書き込み処理では、ファイルから指定されたテナントフィールドを引き続き拒否し、ジョブで認可された値を使う必要があります。書き込み処理の迂回も脅威モデルに含むなら、取り込み先で強制するジョブの境界も設計してください。この断片で、そのより強い保証が得られると説明してはいけません。
COPYはユーザーRLSを使うインポートの近道にはならない
PostgreSQL 17は、実行ロールにRLSが適用される場合のCOPY FROMをサポートしていません。ドキュメントでは、同等のINSERT文を推奨しています。一時ステージングテーブルに読み込んでから、認可されたINSERTで取り込み先に書き込む設計は、セッション、行の値、権限付与、ポリシーをレビューしたうえであれば、適切な方法になり得ます。
テーブル所有者や迂回権限を持つロールでは、行セキュリティが適用されない場合があるため、動作が異なることがあります。これは元のユーザーポリシーの保証を維持する回避策ではありません。特権を使う移行経路であり、テナントの検査と運用上のレビューを別途確立する必要があります。
直接SQL接続にも、SupabaseユーザーのJWTコンテキストが自動的に引き継がれるわけではありません。auth.uid()のような関数は、対象の経路を通じてリクエストまたはセッションのクレームを利用できることに依存します。同じデータベースにつながるというだけで、psqlのログインがエンドユーザーを表すと考えてはいけません。
購入者側が一度だけ実施する移行では、特権での実行を意図的に選ぶ場合もあります。購入者側のエンジニアに、入力データからテナントへの割り当て、正確な列、取り込み先、処理後の行を確認してもらってください。この手順は、RLSで制御するエンドユーザー向けインポーターと区別して扱います。データベース側の制限は、COPYのドキュメントに記載されています。
成功する書き込みに加え、禁止すべき書き込みもテストする
分離したテスト用テナントと架空の行を使います。実際のインポーターと同じロール、リクエスト経路、権限付与、完全なポリシーの組み合わせでテストを実行してください。
- テナントAへのインポート権限を持つユーザーは、Aに承認済みの行を作成できる。
- テナントAにしかインポート権限を持たないユーザーがBを要求すると、拒否される。
- すでにAに結び付けられたジョブでは、ユーザーが別途Bの権限を持っていても、指定されたテナントBは拒否される。これには、メンバーシップポリシーに加えて、ジョブ単位での制御が必要。
- インポート権限のないメンバーは拒否される。
- 未認証または期限切れのセッションに、代替となる特権の書き込み経路が与えられない。
- 指定されたテナントフィールドは、偶然正しいテナントと一致していても拒否される。
- 所属が取り消された後、待機中のジョブは文書化した規則に従う。
成功した後は、AとBそれぞれの通常のアプリケーション経路で行を読みます。挿入だけでなく、可視性も確認してください。エラーが別テナントの識別子やデータを漏らさないようにします。所有者としてのみ成功するテストでは、所有者の権限をテストしたのであって、意図した顧客の経路をテストしたことにはなりません。
連絡先から会社への外部キーも、確認が必要です。全体として有効な会社IDでも、別のテナントに属している場合があります。適切なテナント単位の参照制約と解決方法を使ってください。外部キーのガイドで、この例を詳しく扱います。
ImportFlowとの関係
ImportFlowは、アプリケーションの認可と、表計算ファイルから指定されたテナントの値を分離します。製品レベルの責任分担は、現在のアーキテクチャと実装範囲を参照してください。
ユーザーとテナントに関する権限の責任は、引き続きアプリケーションが担います。ImportFlowは、保護されたテナントやシステムの値を、顧客が編集できる表計算フィールドではなく、アプリケーションのコンテキストとして扱います。RLSだけでは、その権限は確立されません。