Prisma ORM × RLSの悩みを解消。マイグレーションSQLの記述漏れを改善した話 #Developer&Designer Advent Calendar 2024

Developer&Designer Advent Calendar 2024

Developer&Designer Advent Calendar 2024 22日目の記事です🎄

はじめに

こんにちは。
HR forecasterというプロダクトの開発をしている伊藤です。

hr-forecaster.jp

業務やプライベートの開発でPrisma ORMを利用しています。 PostgreSQLのRow-Level Security(RLS)を利用する際に、課題を感じることがあり、その改善に取り組んだので紹介します。

Prisma ORMは、TypeScript/JavaScriptで利用できるObject-Relational Mapping(ORM)ツールです。 宣言的なスキーマ定義や型安全性、マイグレーション機能を提供しており利用しています。

www.prisma.io

記事公開時点では、Prisma ORMは公式にはRLSに必要なマイグレーション機能をまだ提供していません。 なお、現在もGitHubリポジトリの下記のIssueでRLSサポートに関する議論が進んでいます。

github.com

なぜRLSが必要になるのか?

RLSはデータベース側でレコード(行)毎のアクセス制御ポリシーを定義できる強力な機能であり、マルチテナントアプリケーションやユーザ単位のアクセス制御が求められる場面で非常に有用です。
データベース側でアクセス制御を行うことで、アプリケーション側で細かいフィルタリングロジックを書く手間を減らすと同時に、アクセス制御ポリシーで許可されたデータしか操作できないように強制します。
例えば、企業Aのユーザーは企業Aのデータのみ、企業Bのユーザーは企業BのデータのみをWHERE句でフィルタリングするのではなく、RLSでアクセス制御を行うことで、アプリケーション側のロジックをシンプルに保つことができます。

www.postgresql.jp

Prisma ORM × RLSで抱えていた課題

Prismaはスキーマ定義ファイル( schema.prisma )から自動的にマイグレーションSQLとTypeScript/JavaScriptのクライアントコードを生成できるため、開発効率が高まります。

www.prisma.io

しかし、現在はPrismaのマイグレーション機能ではRLSをサポートしておらず、RLSを有効にするには以下のような手作業が必要でした。

  1. prisma migrate dev --create-only でマイグレーションファイルを生成
  2. 生成された migration.sql に手動で ALTER TABLE ... ENABLE ROW LEVEL SECURITYCREATE POLICY などのRLS関連のマイグレーション用SQL(以降、RLS関連SQL)を追記
  3. prisma migrate dev でDBへ反映

補足として、上記の手順で登場するファイルの例にMessageテーブル関連の一部を示します。

schema.prismaの例

... // 他のモデル定義
model Message {
  messageId              String                  @id @default(cuid()) @map("message_id")
  message                String
  ...
  tenantId              String?                 @map("tenant_id")
  @@map("messages")
}

migration.sqlの例

CREATE TABLE "messages" (
    "message_id" TEXT NOT NULL,
    "message" TEXT NOT NULL,
    "tenant_id" TEXT,

    CONSTRAINT "messages_pkey" PRIMARY KEY ("message_id")
);

migration.sqlへRLS関連SQLの追加例 (手作業が必要だった)

ALTER TABLE "messages" ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "messages" USING("tenant_id" = current_setting('app.tenant_id'));
CREATE POLICY bypass_rls_policy ON "messages" USING (current_setting('app.bypass_rls', TRUE)::text = 'on');

この手順は煩雑で、RLS関連SQLの追記漏れが発生しやすい問題がありました。 Pull Requestをレビューする際にも、RLS関連SQLの追記漏れを見逃してしまうことがあり、リスクにつながる可能性を抱えていました。

課題解決を支援するツールの紹介

こうした状況を改善するために作ったのが prismarls というコマンドラインツールです。

www.npmjs.com

prismarlsは、PrismaでマイグレーションSQL生成後に、schema.prismaをもとに抽出したRLS設定に基づき、自動的にRLS関連SQLをマイグレーションファイルへ挿入し、追加漏れを防ぎます。

prismarlsの仕組み

prismarlsでは、RLS関連SQLに ALTER TABLE 文と CREATE POLICY 文を生成するために、schema.prismaに独自の @RLS アノテーションを /// コメントとして記載します。 @RLS アノテーションには、RLSポリシーを適用するテーブル名とカラム名を指定します。

以下は、Messageテーブルに対して、tenantId フィールドにRLSポリシーを適用する例です。

model Message {
  messageId              String                  @id @default(cuid()) @map("message_id")
  message                String
  ...
  tenantId              String?                 @map("tenant_id") /// @RLS(table: "messages", column: "tenant_id")
  @@map("messages")
}

prismarlsを実行すると、この @RLS アノテーションを解析し、RLS関連SQLを生成して直近のマイグレーションファイルに挿入します。 @RLS アノテーションを解析するために、外部ライブラリ @loancrate/prisma-schema-parser を利用しています。

このパーサーライブラリは、schema.prismaを解析して、Prismaのスキーマ定義情報を抽出するためのものです。スキーマ定義情報としては、model名やフィールドの名前/型/@map定義、コメントなどを読み込めます。

www.npmjs.com

なお、schema.prismaでは2種類のコメントが利用できますが、// はAbstract Syntax Tree(AST)に含まれないため、/// コメントを利用して @RLS アノテーションを記述しています。

www.prisma.io

処理の流れとしては、以下のようになります。

  1. schema.prismaの @RLS アノテーションを解析し、RLSポリシーを適用するテーブル名とカラム名を取得
  2. コマンドラインオプション( --migrations )で指定されたマイグレーションディレクトリから最新のマイグレーションファイルを取得
  3. マイグレーションファイルから CREATE TABLE 文のテーブル名を取得
  4. @RLS アノテーションのテーブル名とマイグレーションファイルのテーブル名が一致する場合、コマンドラインオプション( --currentSettingIsolation, --currentSettingBypass )と合わせてRLS関連SQLを生成してマイグレーションファイルに挿入

prismarlsの利用手順

  1. マイグレーションファイルを生成
   npx prisma migrate dev --create-only
  1. prismarlsでRLS設定を挿入
   npx @shoito/prismarls --schema=./prisma/schema.prisma --migrations=./prisma/migrations --currentSettingIsolation=app.tenant_id --currentSettingBypass=app.bypass_rls

これにより、最新のマイグレーションファイルにRLS設定用SQLが挿入されます。 さらに、上記の手順を簡略化するために、package.json のスクリプトに prisma migrate dev の実行とは別に、以下のような migrate:dev:create を追加しておくと、マイグレーションファイルの生成とRLS設定用SQLの追加が同時にできて便利です。

{
...
  "scripts": {
    "migrate:dev": "prisma migrate dev",
    "migrate:dev:create": "prisma migrate dev --create-only && pnpm run migrate:dev:append-rls",
    "migrate:dev:append-rls": "npx @shoito/prismarls --schema=./prisma/schema.prisma --migrations=./prisma/migrations --currentSettingIsolation=app.tenant_id --currentSettingBypass=app.bypass_rls",
  },
...
}

現時点での制約

  1. 直近のデータベースへ未適用のマイグレーションのみ対象
    既にデータベースへ適用済みのマイグレーションファイルを書き換えると、Prismaが管理する _prisma_migrations テーブルとの整合性が崩れてしまいます。 そのため、 prismarls は prisma migrate dev--create-only オプションで生成された、まだ適用していないマイグレーションファイルに対してのみ動作します。

    www.prisma.io

  2. 汎用性は限定的
    prismarlsは私が関わってきたプロジェクトでの特定シーンを目的に作ったツールであり、まだ成熟していません。 特定のアノテーションやルールに依存しているため、他プロジェクトでそのまま動く保証はありません。

Prisma公式リポジトリでのRLSサポート議論

Prisma公式リポジトリのIssue #12735 では、RLSサポートについての議論が進んでいます。 コミュニティからの要望や実装戦略が検討されており、将来的には公式機能が提供される可能性があります。

最後に

今回、Prisma ORMでRLSを利用する際の課題を解決するために作成したprismarlsというツールを紹介しました。 RLSを利用する際には手作業が必要でしたが、prismarlsを利用することで、RLS関連SQLの追記漏れを防ぎ、開発効率を向上させることができました。 今後は、Prisma公式のRLSサポートが進展して、prismarlsのようなサードパーティツールが不要になることを期待しています。

参考

伊藤 祥 Sho Ito

クライアントP&M本部 プロダクト統括部 クライアントサービス開発部 HR_forecasterエンジニアリンググループ シニアエンジニア

2023年6月にパーソルキャリアに入社。現在はHR forecasterの開発に従事。

※2024年12月現在の情報です。