電通総研 テックブログ

電通総研が運営する技術ブログ

Supabase: 参照はRLS(Row Level Security)、登録更新削除はEdge Functionsで認可制御を実装した話

こんにちは、電通総研の瀧川亮弘です。

現在、Supabaseによるアプリ開発を行っています。
本記事ではSupabaseの認可制御をどのような方針で実装しているのか紹介します。

前提

アプリからSupabaseへのリクエストは2つのAPIを使い分けています。

一つ目にSupabaseがスキーマ情報をもとに自動生成するRESTful APIです。
内部的にはPostgRESTというライブラリが用いられています。
こちらは単純なCRUD操作しかできません。

二つ目にEdge Functionsです。
こちらはTypeScriptベースのサーバーレス関数です。
任意の処理をかけるため複雑な要件に対応できます。

また、SupabaseはpostgreSQLの機能である、RLS(Row Level Security)による認可制御を推奨しています。
https://supabase.com/docs/guides/database/postgres/row-level-security

実装方針

認可制御の実装方針として2つの作戦を考えました。

一律RLS大作戦

参照系のみRLS大作戦

  • PostgREST: RLSの有効化(R(参照)のみロールベース制御を行い、CUD(副作用を伴う処理)は全て拒否する)
  • Edge Functions: RLSを無効化し、Edge Functionsにてロールベース制御を実装する

結論、「参照系のみRLS大作戦」を採用しました。
つまり、参照機能とそれ以外の副作業を伴う機能とで実装方法を分けました。
前者はRLSで認可を行い、後者はEdge Functionsで認可を行います。

理由は、PostgRESTからの操作は拒否したいが、Edge Functions からの操作は許可したいというケースが存在するからです。
たとえば、Aテーブル、Bテーブルの2つのテーブルに同じトランザクション内で更新をかけたい場合です。
「一律RLS大作戦」で認可制御を行う場合、各テーブルへの更新権限を許可することとなります。
しかし、そのするとPostgRESTを通して個々のテーブルへの更新処理が可能となるため、Aテーブルのみを単独で更新することができてしまいます。
そのため、PostgRESTからの操作はRLSでブロックし、Edge Functionsからの操作についてはRLSをバイパスすることとしました。
PostgREST側は厳しめに安全な方に寄せて認可制御を行い、その分、Edge Functions側では要件に応じた柔軟な認可制御を行います。

実装

ロールごとの各テーブルへのCRUD権限を定義します。
GRANT文で参照権限のみ許可した上で、RLSでさらに詳細な権限設定を行っています。

/*
    SELECT権限のみ付与する
*/
REVOKE ALL ON ALL TABLES IN SCHEMA public FROM anon,authenticated;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anon,authenticated;

/*
    将来的にテーブル追加された場合も同様とする
*/
ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE ALL ON TABLES FROM anon,authenticated;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO anon,authenticated;
DROP POLICY IF EXISTS "read access" ON public.booking_info;
DROP POLICY IF EXISTS "read access for anon" ON public.booking_info;

CREATE POLICY "read access" ON public.booking_info FOR
SELECT
  TO authenticated
  USING (
    CASE
      WHEN ((select auth.jwt()) ->> 'user_role') = 'vendor' THEN true
      WHEN ((select auth.jwt()) ->> 'user_role') = 'client' THEN client_user_id = (select auth.uid())
      ELSE false
    END
  );

CREATE POLICY "read access for anon" ON public.booking_info FOR
SELECT
  TO anon
  USING (is_public);

ALTER TABLE public.booking_info ENABLE ROW LEVEL SECURITY;

また、Edge Functionsでロールに基づく認可制御が行えるよう、ログイン時のフックポイントでアクセストークン(JWT)にロール情報を格納します。
API呼び出し時はアクセストークンのロール情報を参照し、権限判定を行います。
以下が参考になりました。
https://supabase.com/docs/guides/database/postgres/custom-claims-and-role-based-access-control-rbac

終わりに

Supabaseに限らず、FirebaseなどのBaaSにおいても、DBスキーマからAPIを自動生成してくれます。
便利な一方、認可制御の考え方が通常のスクラッチ開発と異なる点は要注意だなと感じました。

Supabaseはとても好きなサービスなので、今後も楽しいSupabase生活を送っていこうと思います!

執筆:@takigawa.akihiro
Shodoで執筆されました