どうも、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 | 神奈川は最高だね |
このような設定をすれば「誰でも閲覧はできるが、投稿者しかデータを更新・削除できない
」という制限をかけることができる。
-- 投稿は誰でも閲覧できる
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さんもこちらで語っています。
例えばこのようなセキュリティルールがあったときに
match /privatePosts/{postId} {
allow read: if request.auth.uid == resource.data.userId;
}
下記のようにprivatePosts
コレクションから全データを引っ張ってこようとするクエリーを投げます。
const q = query(collection(db, "privatePosts"));
const querySnapshot = await getDocs(q);
そうするとこのクエリーはセキュリティルールがアクセスを弾いてエラーになっちゃうんですね。エラーが出る理由は、上記のようにコレクション全体に対して取得リクエストを投げるとuserId
が自分と一致していないものもあるからです。これが「Firebaseのセキュリティルールはフィルターとしては使えない
」というFirebaseのセキュリティルールの性質です。今回はFirestoreのセキュリティルールを例に出しましたが、Realtime Databaseも同じ性質を持っています。
対してSupabaseの方はどうでしょう?
例えばprivate_posts
というテーブルがあり、そのテーブルにこのようなRow Level Securityルールがかけられていたとしましょう:
-- 投稿主でないと閲覧できない
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
だったとしましょう。
const { data, error } = await supabase.from('private_posts').select()
するとこんな感じのデータが返ってきます。
[
{
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はセキュリティルールさえ組んでおけば後は深く気にすることなくデータをクエリーするだけでユーザーが閲覧権限を持つデータを引っ張ってくることができます。地味なことかもですが、クライアント側で実装ミスをしても想定通り挙動してくれたりとメリットはちょいちょいあるのかなと思っています!