🦔

RLS(Row-Level Security)を簡単解説

2025/03/06に公開

はじめに

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が一致しているタスクだけ読み込みめる」というポリシー

ex. ポリシー
create policy "Users can view their own tasks"
on tasks
for select
to authenticated
using ( user_id = auth.uid() );
  1. 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のみからアクセスを受け付けるようにする方法)

  1. SupabaseのダッシュボードからSERVICE_ROLE_KEYを取得する
  2. SQL Editorで以下のコマンドを実行し,一般アクセスを制限する
REVOKE ALL ON public.customer_line_id_table FROM PUBLIC;
  1. サーバーサイドで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