こんにちは、電通総研の瀧川亮弘です。
現在、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大作戦
- PostgREST: RLSを有効化(全てのCRUD処理についてロールベース制御を行う)
- Edge Functions: RLSを有効化(PostgRESTと同様)
※Edge FunctionsのDrizzle ORMからDBへのアクセスにおいて、RLSを有効化する方法は以下のissueが参考になりました。
https://github.com/drizzle-team/drizzle-orm/issues/594#issuecomment-2016607868
参照系のみ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で執筆されました)