RLS(Row-Level Security)を簡単解説
はじめに
Webアプリのデータ保存場所+認証関係で Supabase を使用しているのですが,各TableにはRLS(Row-Level Security) をつけられるということで,勉強したので共有します.
自分は,セキュリティに関して完全な初心者なので,初心者でもわかるように解説していきます.
対象読者
- RLS について知りいたい人
- セキュリティに関して完全な初心者の人
RLS(Row-Level Security)とは?
Row-Level Security(行レベルセキュリティ) は,データベースのテーブルに格納されている“各行(レコード)”ごとに,誰が閲覧・更新できるかを制限する仕組みです.
簡単にいうと,「あるユーザはこの行だけ見えるが,別のユーザは別の行しか見られないようにする」というように,1 つのテーブルの中でも利用者ごとに見えるデータや編集できるデータを細かく制御できる機能を指します.
なぜ RLS が大事なのか
- セキュリティの基本として,「見て良い人だけがデータを見られる」状態にすることはとても大事
- もし,RLS がなければ,「このユーザはこのデータを見ていい」というコードを自分で書く必要があり,とても面倒
- RLS を使うと,データベース自体がアクセス制御を行なってくれるので,実装と保守が楽!かつ実装が簡単
Supabase での RLS の仕組み
3-1. RSL の基本
- Supabase では,PostgreSQL を使用しているため,PostgreSQL の RLS の機能をそのまま使用可能
- RLS を有効にしたいテーブルに対して,「ポリシー(誰が何をできるのかを定義したルール)」を設定
- Supabase では,
Enable RLS
をオンにするだけで,テーブルに対して RLS を適用することができる
3-2. ポリシー(Policy)とは?
-
ポリシーとは,具体的に「このユーザは,このテーブルのどの行を
SELECT
できるか」「どの行に対して,INSERT
,UPDATE
を行えるか」を決めるためのルール -
ポリシー作成の例(user_id は Supabase の認証で 1 アカウントずつ付与される固有の ID)
以下では,「task
テーブルに対するSELECT
の際に認証済みのユーザは,自分のuser_id
と行のuser_id
が一致しているタスクだけ読み込みめる」というポリシー
create policy "Users can view their own tasks"
on tasks
for select
to authenticated
using ( user_id = auth.uid() );
- RLS の具体的な使い方
実際に Supabase で実装する例
Supabase プロジェクト上で実際に RLS を設定する流れを示します.
1. テーブルの作成
まずは例として,タスク管理用のテーブルを作成します.
CREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES auth.users(id),
content TEXT NOT NULL,
is_done BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
このテーブルには:
-
id
: 各タスクの一意の ID -
user_id
: タスクの所有者(Supabase の認証ユーザ ID) -
content
: タスクの内容 -
is_done
: タスクの完了状態 -
created_at
: タスク作成日時
のカラムがあります.
2. RLS の有効化
テーブルを作成したら,RLS を有効にします.Supabase ダッシュボードの「テーブルエディタ」から「Enable RLS」トグルをオンにするか,SQL エディタで以下のコマンドを実行します.
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
RLS を有効にすると,デフォルトではすべてのアクセスが拒否されます.つまり,明示的なポリシーを作成しない限り,どのユーザもテーブルにアクセスできなくなります.
3. 読み取り(SELECT)用のポリシー作成
まず,ユーザが自分のタスクだけを閲覧できるようにするポリシーを作成します.
CREATE POLICY "Users can view their own tasks"
ON tasks
FOR SELECT
TO authenticated
USING (user_id = auth.uid());
この設定により:
-
authenticated
(認証済みユーザ)に対して -
tasks
テーブルのSELECT
操作において -
user_id = auth.uid()
の条件を満たす行のみにアクセスを許可する
ポリシーを作成しています.auth.uid()
は Supabase が提供する関数で,現在ログインしているユーザの ID を返します.
4. 書き込み(INSERT)用のポリシー作成
次に,ユーザが自分自身のタスクのみを追加できるようにするポリシーを作成します.
CREATE POLICY "Users can insert their own tasks"
ON tasks
FOR INSERT
TO authenticated
WITH CHECK (user_id = auth.uid());
WITH CHECK
句は,挿入される新しい行が条件を満たしているかをチェックします.これにより,ユーザは自分の ID を持つタスクのみ作成できます.
5. 更新(UPDATE)用のポリシー作成
ユーザが自分のタスクのみを更新できるようにするポリシーを作成します.
CREATE POLICY "Users can update their own tasks"
ON tasks
FOR UPDATE
TO authenticated
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
このポリシーでは:
-
USING
句: 更新対象の行が条件を満たしているかをチェック -
WITH CHECK
句: 更新後の行が条件を満たしているかをチェック
両方の条件が必要なのは,ユーザが更新する際に自分のタスクであることを確認し,かつ更新後も自分のタスクであり続けることを保証するためです.
6. 削除(DELETE)用のポリシー作成
最後に,ユーザが自分のタスクのみを削除できるようにするポリシーを作成します.
CREATE POLICY "Users can delete their own tasks"
ON tasks
FOR DELETE
TO authenticated
USING (user_id = auth.uid());
7. クライアントサイドからのテスト
ポリシーを設定した後,クライアントサイドのコードからテストします.例えば,JavaScript を使用して Supabase クライアントからアクセスする場合:
// Supabaseクライアントの初期化
const supabase = createClient("YOUR_SUPABASE_URL", "YOUR_SUPABASE_ANON_KEY");
// ログイン後,自分のタスクだけが取得できることを確認
const { data, error } = await supabase.from("tasks").select("*");
console.log("My tasks:", data);
// 他のユーザのタスクは含まれていないはず
RLS のおかげで,クライアントコードでは特別なフィルタリングを行わなくても,データベース側で自動的に「自分のタスクだけ」が返されます.
追記 (サーバーサイドとN8Nのみからアクセスを受け付けるようにする方法)
- Supabaseのダッシュボードから
SERVICE_ROLE_KEY
を取得する - SQL Editorで以下のコマンドを実行し,一般アクセスを制限する
REVOKE ALL ON public.customer_line_id_table FROM PUBLIC;
- サーバーサイドでSupabase Clientを以下のように実装する
class SupabaseClient:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
# 初期化済みならば、何もしない
if hasattr(self, 'client'):
return
dot = os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env.local")
load_dotenv(dot)
self.url = os.getenv("NEXT_PUBLIC_SUPABASE_URL")
self.key = os.getenv("NEXT_PUBLIC_SUPABASE_ANON_KEY")
self.service_key = os.getenv("SERVICE_ROLE_KEY")
if not self.url or not self.key or not self.service_key:
raise ValueError("Required environment variables not set")
try:
# 通常のクライアント(一般的なTable用)
self.client = create_client(self.url, self.key)
# 管理者権限クライアント(CustomerLineIdTable用)
self.admin_client = create_client(self.url, self.service_key)
except Exception as e:
raise Exception(f"Failed to initialize Supabase client: {e}")
def get_client(self, jwt_token: Optional[str] = None) -> Client:
try:
if jwt_token:
headers = {
"Authorization": f"Bearer {jwt_token}",
"apikey": self.key
}
return create_client(self.url, self.key, headers=headers)
return self.client
except Exception as e:
raise Exception(f"Error creating client with JWT token: {e}")
def get_admin_client(self) -> Client:
"""
CustomerLineIdTableなど特許が必要なテーブル用のSERVICE_ROLE_KEYを使用したクライアントを返す
"""
return self.admin_client
def __call__(self) -> Client:
return self.client
Discussion