Developer&Designer Advent Calendar 2024 22日目の記事です🎄
はじめに
こんにちは。
HR forecasterというプロダクトの開発をしている伊藤です。
業務やプライベートの開発でPrisma ORMを利用しています。 PostgreSQLのRow-Level Security(RLS)を利用する際に、課題を感じることがあり、その改善に取り組んだので紹介します。
Prisma ORMは、TypeScript/JavaScriptで利用できるObject-Relational Mapping(ORM)ツールです。 宣言的なスキーマ定義や型安全性、マイグレーション機能を提供しており利用しています。
記事公開時点では、Prisma ORMは公式にはRLSに必要なマイグレーション機能をまだ提供していません。 なお、現在もGitHubリポジトリの下記のIssueでRLSサポートに関する議論が進んでいます。
なぜRLSが必要になるのか?
RLSはデータベース側でレコード(行)毎のアクセス制御ポリシーを定義できる強力な機能であり、マルチテナントアプリケーションやユーザ単位のアクセス制御が求められる場面で非常に有用です。
データベース側でアクセス制御を行うことで、アプリケーション側で細かいフィルタリングロジックを書く手間を減らすと同時に、アクセス制御ポリシーで許可されたデータしか操作できないように強制します。
例えば、企業Aのユーザーは企業Aのデータのみ、企業Bのユーザーは企業BのデータのみをWHERE句でフィルタリングするのではなく、RLSでアクセス制御を行うことで、アプリケーション側のロジックをシンプルに保つことができます。
Prisma ORM × RLSで抱えていた課題
Prismaはスキーマ定義ファイル( schema.prisma
)から自動的にマイグレーションSQLとTypeScript/JavaScriptのクライアントコードを生成できるため、開発効率が高まります。
しかし、現在はPrismaのマイグレーション機能ではRLSをサポートしておらず、RLSを有効にするには以下のような手作業が必要でした。
prisma migrate dev --create-only
でマイグレーションファイルを生成- 生成された
migration.sql
に手動でALTER TABLE ... ENABLE ROW LEVEL SECURITY
やCREATE POLICY
などのRLS関連のマイグレーション用SQL(以降、RLS関連SQL)を追記 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 というコマンドラインツールです。
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定義、コメントなどを読み込めます。
なお、schema.prismaでは2種類のコメントが利用できますが、//
はAbstract Syntax Tree(AST)に含まれないため、///
コメントを利用して @RLS
アノテーションを記述しています。
処理の流れとしては、以下のようになります。
- schema.prismaの
@RLS
アノテーションを解析し、RLSポリシーを適用するテーブル名とカラム名を取得 - コマンドラインオプション(
--migrations
)で指定されたマイグレーションディレクトリから最新のマイグレーションファイルを取得 - マイグレーションファイルから
CREATE TABLE
文のテーブル名を取得 @RLS
アノテーションのテーブル名とマイグレーションファイルのテーブル名が一致する場合、コマンドラインオプション(--currentSettingIsolation
,--currentSettingBypass
)と合わせてRLS関連SQLを生成してマイグレーションファイルに挿入
prismarlsの利用手順
- マイグレーションファイルを生成
npx prisma migrate dev --create-only
- 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", }, ... }
現時点での制約
直近のデータベースへ未適用のマイグレーションのみ対象
既にデータベースへ適用済みのマイグレーションファイルを書き換えると、Prismaが管理する_prisma_migrations
テーブルとの整合性が崩れてしまいます。 そのため、 prismarls はprisma migrate dev
の--create-only
オプションで生成された、まだ適用していないマイグレーションファイルに対してのみ動作します。汎用性は限定的
prismarlsは私が関わってきたプロジェクトでの特定シーンを目的に作ったツールであり、まだ成熟していません。 特定のアノテーションやルールに依存しているため、他プロジェクトでそのまま動く保証はありません。
Prisma公式リポジトリでのRLSサポート議論
Prisma公式リポジトリのIssue #12735 では、RLSサポートについての議論が進んでいます。 コミュニティからの要望や実装戦略が検討されており、将来的には公式機能が提供される可能性があります。
最後に
今回、Prisma ORMでRLSを利用する際の課題を解決するために作成したprismarlsというツールを紹介しました。 RLSを利用する際には手作業が必要でしたが、prismarlsを利用することで、RLS関連SQLの追記漏れを防ぎ、開発効率を向上させることができました。 今後は、Prisma公式のRLSサポートが進展して、prismarlsのようなサードパーティツールが不要になることを期待しています。
参考
- Prisma ORM
- 行セキュリティポリシー | PostgreSQL 16.4文書
- Support for row-level security (RLS) #12735
- prisma-schema-parser - npm
- Prisma Schema Overview | Prisma Documentation
- About migration histories | Prisma Documentation
- prismarls - npm
伊藤 祥 Sho Ito
クライアントP&M本部 プロダクト統括部 クライアントサービス開発部 HR_forecasterエンジニアリンググループ シニアエンジニア
2023年6月にパーソルキャリアに入社。現在はHR forecasterの開発に従事。
※2024年12月現在の情報です。