SupabaseのRow Level SecurityとFirebaseのセキュリティルール比較

どうも、Supabase DevRelのタイラーです！

SupabaseはFirebaseと同じような感覚でサクッとアプリ開発入れる便利なツールとして親しまれていますが、皆さんはそんなRow Level Securityについてきちんと理解できていますか？Supabaseを使っている人の多くがFirebaseを過去に使っていた人たちということもあるので、今回はSupabaseのRow Level SecurityとFirebaseのセキュリティルールの違いを一つ紹介できたらなと思います。

## その前にRow Level Securityのおさらい

Row Level SecurityとはSupabaseに組み込まれた（正確にはPostgreSQLに組み込まれた）アクセス制限の仕組みで、特定のrowにどのユーザーがアクセスすることができるのかをSQLを使って設定することができる形になっています。Firebaseでいうセキュリティルールにあたるものですね。

例えば、`posts`というテーブルがあったときに、「`誰でも閲覧はできるが、投稿者しかデータを更新・削除できない`」といった制限をSQLを使ってかけることができる代物になっています。

こんな感じの`posts`テーブルがあった場合に:

| id | user_id | content          |
|----|---------|------------------|
| 1  | aaa     | 夏は暑いね       |
| 2  | bbb     | 猫はかわいいね   |
| 3  | ccc     | 神奈川は最高だね |

このような設定をすれば「`誰でも閲覧はできるが、投稿者しかデータを更新・削除できない`」という制限をかけることができる。

```sql
-- 投稿は誰でも閲覧できる
create policy "Posts are viewable by everyone."
  on posts for select using (
    true
  );

-- 他人になりすまして投稿することはできない
create policy "Posts can be created under your user_id"
  on posts for insert with check (
    auth.uid() = user_id
  );

-- 投稿主しか投稿の更新ができない
create policy "Posts can be updated by the creator"
  on posts update using (
    auth.uid() = id
  ) with check (
    auth.uid() = id
  );

-- 投稿主しか投稿の削除ができない
create policy "Posts can be deleted by the creator"
  on posts delete using (
    auth.uid() = id
  );
```

この際`with check`と`using`というキーワードが出てきましたね。`with check`は新しく作成されるデータを指していて、`insert`や`update`をする際に更新後のデータを使って制限をかけることができます。`using`は今テーブルに既に保存されているデータを参照し権限設定ができる形になっております。

## Firebaseとの違いはフィルターとして使えるか使えないか

SupabaseとFirebaseのデータベースは両方ユーザーからのアクセスを制限し、セキュアなアプリケーションを作るための機能が備わっています。ただ、両者の違いはそのセキュリティ機能がフィルターとして機能してくれるか否かです。

Firebaseのセキュリティルールはフィルターになりません。このことについてはFirebaseチームのDoug Stevenさんも[こちら](https://medium.com/firebase-developers/what-does-it-mean-that-firestore-security-rules-are-not-filters-68ec14f3d003)で語っています。

例えばこのようなセキュリティルールがあったときに

```
match /privatePosts/{postId} {
  allow read: if request.auth.uid == resource.data.userId;
}
```

下記のように`privatePosts`コレクションから全データを引っ張ってこようとするクエリーを投げます。

```typescript
const q = query(collection(db, "privatePosts"));

const querySnapshot = await getDocs(q);
```

そうするとこのクエリーはセキュリティルールがアクセスを弾いてエラーになっちゃうんですね。エラーが出る理由は、上記のようにコレクション全体に対して取得リクエストを投げると`userId`が自分と一致していないものもあるからです。これが「`Firebaseのセキュリティルールはフィルターとしては使えない`」というFirebaseのセキュリティルールの性質です。今回はFirestoreのセキュリティルールを例に出しましたが、Realtime Databaseも同じ性質を持っています。

対してSupabaseの方はどうでしょう？

例えば`private_posts`というテーブルがあり、そのテーブルにこのようなRow Level Securityルールがかけられていたとしましょう：

```sql
-- 投稿主でないと閲覧できない
create policy "Private posts are viewable by everyone."
  on private_posts for select using (
    auth.uid() = user_id
  );
```

このような制限がかかっている`private_posts`テーブルに対してSupabaseを使ってこのような`private_posts`のデータ全てを取得するクエリーを投げてみます。その際仮に今SupabaseにログインしているユーザーのユーザーIDが`my_id`だったとしましょう。

```typescript
const { data, error } = await supabase.from('private_posts').select()
```

するとこんな感じのデータが返ってきます。

```typescript
[
  {
    id: 3,
    user_id: 'my_id',
    content: '秘密のコンテンツ'
  },
  {
    id: 5,
    user_id: 'my_id',
    content: '見られたくない秘密'
  },
  {
    id: 9,
    user_id: 'my_id',
    content: '隠したい過去'
  }
]
```

見ていただきたいのは、返ってくるデータの`user_id`が全て`my_id`になっている点です。クエリー事態は全データを取得しに行ったのに、Row Level SecurityがFilterの役割を果たし、自動で`user_id`に対して`where`がかかったような形でデータを取得することができました。これが**Supabase(PostgreSQL)のRow Level SecurityのFilterとして使える特性**です！

### まとめ

Firebaseはクライアント側でセキュリティルールとマッチした`where`文を書かないとクエリーできないのに対してSupabaseはセキュリティルールさえ組んでおけば後は深く気にすることなくデータをクエリーするだけでユーザーが閲覧権限を持つデータを引っ張ってくることができます。地味なことかもですが、クライアント側で実装ミスをしても想定通り挙動してくれたりとメリットはちょいちょいあるのかなと思っています！