Supabase(PostgreSQL)の行レベルセキュリティはいいけど…🤔

アプリケーションを作る開発者のためのデータベースやバックエンド分野で大きな革新がありました。その結果、新しいデータベースクラウドサービスプロバイダーの台頭が目立つようになっています。以下のような企業が出ています。

これらの中で、私のお気に入りはSupabaseです。その理由は、Supabaseは単なる管理データベースプロバイダー以上のものだからです。全てが一体となったBackend as a Service(BaaS)ソリューションを提供しており、PostgreSQLデータベース、認証、リアルタイムのサブスクリプション、RESTful APIの生成、ファイルストレージが揃っているため、Webアプリケーション用の追加のバックエンドサービスは必要ありません。API生成のみでは通常これを実現できませんが、それは以下の簡単な理由からです。

フロントエンドに直接データベースを公開してはならない

しかし、Postgresの強力な行レベルセキュリティ(RLS)のおかげで、API生成を介してデータベースへの安全かつ制御されたアクセスを可能にすることができます。以下は、簡単な例です(PostgreSQLで):

-- source: https://www.2ndquadrant.com/en/blog/application-users-vs-row-level-security/

CREATE TABLE chat (
    message_uuid    UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    message_from    NAME      NOT NULL DEFAULT current_user,
    message_to      NAME      NOT NULL,
    message_subject VARCHAR(64) NOT NULL,
);

CREATE POLICY chat_policy ON chat
    USING ((message_to = current_user) OR (message_from = current_user))
    WITH CHECK (message_from = current_user)

人間の言葉で言うと、以下のような意味です。

  1. chat テーブルの行は、現在のユーザーが送信者または受信者のどちらかである場合にのみ表示されます。
  2. chat テーブルの行が挿入または更新されるとき、送信者は現在のユーザーでなければなりません。

細かいアクセスポリシーを書いた後、データベースは定義されたルールに基づいて特定のデータにアクセスできるのは認証されたユーザーのみであることを保証します。これで、フロントエンドにAPIを公開することが安全になり、データのプライバシーとセキュリティが保たれます。

2007年、Microsoftは欧州の独占禁止規制当局に対応するためにサーバープロトコルを公開しました。しかし、多くのプロトコルに技術文書がなかったため、チームは実装に基づいて文書を作成する必要がありました。正確性を確保するために、Microsoftはこれらのプロトコル仕様をテストする専用チームを作りました。テストプロセスを自動化するために、内部ツールチームはさまざまなツールスイートを開発しました。

私は新卒でツールチームに参加しましたが、彼らはこの問題を根本的に解決する新しいツールを開発していました。仕様にはミスが生じるかもしれませんが、それは実装と完全に切り離されているためです。両者間で単一の定義を共有することで一貫性を保証し、テストの必要性を排除します。

そして、Open Protocol Notation(OPN)という新しいDSL(ドメイン固有言語)が導入されました。これは、プロトコルのアーキテクチャ、動作、およびデータをモデル化するために開発者が使用することを目的として設計されています。プロトコルスキーマファイル(IDL、WSDLなど)やメッセージパーサー、シミュレーション、技術文書の生成に使用されます。RPCプロトコルに関するOPNを完成させ、公開された技術文書の生成とメッセージの解析/表示に使われたときのことを今でも覚えています。そしてそれが、「単一の情報元」と呼ばれることに初めて気づきました。

もちろん、これがデータベースポリシーでセキュリティを実装する全てのポイントですが、アプリケーションの包括的なビューを持ちたい場合、多くのロジックがソースコードに存在しないため困難です。具体的に問題になり得るのは次の点です。

  • 単純性と保守性: システムの理解が難しくなるだけでなく、デバッグやテストも難しくなります。
  • 移植性: すべてのデータベースベンダーで一貫してサポートされているわけではありません。
  • バージョン管理: データベースプロバイダーはしばしば独自のバージョニングメカニズムを持っていますが、Git内のアプリケーションコードと簡単に統合することはできません。

ストアドプロシージャが今日でも利用されていることをめったに見ないのは、それが提供するすべての利点にもかかわらず同じ理由だと思います。

私たちが答えるべき最初の質問は次のとおりです。

アクセスポリシーをデータベースからアプリケーションコードと一緒に移動したい場合、どこが最適な場所か?

アプリケーションコードとデータベースの間の架け橋としてのObject-Relational Mapping(ORM)を考えるのは直感的です。そこで、私たちが開発しているZenStack OSSプロジェクトでは、アクセス制御機能を追加することに焦点を当てています。以下は、以前に見た「チャット」シナリオと同じスキーマの例です。

// auth() 関数は現在のユーザーを返します。
// future() 関数は更新後のエンティティの値を返します。

model User {
  id Int @id @default(autoincrement())
  username String
  sent Chat[] @relation('sent')
  received Chat[] @relation('received')

  // ユーザーが自身のプロフィールを読むことを許可
  @@allow('read', auth() == this)
}

model Chat {
  id Int @id @default(autoincrement())
  subject String
  fromUser User @relation('sent', fields: [fromUserId], references: [id])
  fromUserId Int
  toUser User @relation('received', fields: [toUserId], references: [id])
  toUserId Int

  // ユーザーが自身のチャットを読むことを許可
  @@allow('read', auth() == fromUser || auth() == toUser)

  // ユーザーが送信者としてチャットを作成することを許可
  @@allow('create', auth() == fromUser)

  // 送信者がチャットを更新することを許可するが、送信者を変更することは許さない
  @@allow('update', auth() == fromUser && auth() == future().fromUser)
}

アプリケーションコードがORMを使用してデータベースにアクセスするとき、クエリと変更に適切なフィルタが挿入され、セキュリティルールが実施されます。たとえば:

  • db.chat.findMany()を行うと、現在のユーザーに関連するチャットのみが返されます。
  • db.chat.create({ fromUserId: 1, toUserId: 2, subject: 'hello' })を行うと、現在のユーザーがID 1でない場合、ORMはリクエストを拒否します。

RLSポリシールールがアプリケーションコードに成功して移されました。一部の人々は「ちょっと待って、新しいスキーマファイルが導入されたって、それって「単一の情報源」という原則を破ることにならないの?」と尋ねるかもしれません。

私の短い答えは、スキーマファイルもアプリケーションコードの一部です。考えてみれば、上記で述べた単純性、移植性、バージョン管理を損なうことなく、RLS機能を実現できるのです。さらに、スキーマファイルはビルドプロセス中にTypescriptコードにトランスパイルされます。これはORMの2つの異なるアプローチの1つに過ぎません。"コードファースト"のアプローチTypeoRMや"スキーマファースト"のアプローチPrismaといったものです。

このことは「コードファースト」アプローチを使っても実現可能ですが、開発者にとって求められるアクセスポリシーをスキーマなしで表現するのは難しく直感的ではありません。「スキーマファースト」アプローチは、コード生成を通じて追加の利点を提供します。興味がある場合は、このトピックに関する別の投稿をチェックしてください。

公平を期すために、RLSが私たちのアプローチよりも優れている利点をいくつか否定できません。例えば、ポリシーが複数のアプリケーションで機能し、言語中立性がある点です。しかし、私たちみんなが知っているように、ワンサイズフィットオールのソリューションはありません。それは常に取るべきトレードオフを伴います。それが正しい進路だと信じる人々がいる限り、現在ある不便さは私たちが解決すべき問題に過ぎません。

あなたもその一人ですか? 😉 そうであれば、詳細については私たちのGithubをチェックしてください:

https://github.com/zenstackhq/zenstack

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/zenstack/rls-of-supabasepostgresql-is-good-but--1394