背景と目的
現時点でMySQLはRLS(Row Level Security)をサポートしていません。マルチテナントなどを単一DBで管理する場合、何らかのコーディングミスなどで他テナントのデータへアクセスしてしまうと大変です。RLSを利用することによってデータアクセス権限周りを堅固にできます。今回はRLSが利用できないMySQLを、Node.js向けORMであるprismaを使って仮想的にRLS(のようなもの)を設定していきます。
設定方法
RLSを再現するため、prismaの4.16.0以降で導入されたPrisma Client extensions
を利用します。extensionsではミドルウェアのような処理を挿入したり共通のクエリ操作などを設定することが可能です。

今回はマルチテナントのテナントIDを基にデータのアクセス範囲を決めることを想定します。
まずはテナントを管理するOrganization
テーブルと、各テーブルにテナントを区別するためのtenantId
を設定します。
model Organization {
id Int @id @default(autoincrement())
tenantId String @unique
name String
user User[]
...
}
model User {
id Int @id @default(autoincrement())
tenantId String
email String
name String
...
organization Organization @relation(fields: [tenantId], references: [tenantId])
}
以下のコードではprismaクライアントの$extends
メソッドからクエリ操作でwhere句にtenantId
を設定し、どのようなクエリが呼ばれても必ずテナントが指定されるようにします。
prisma(tenantId: string) {
if (!tenantId) {
throw new Error("TenantId is not found");
}
const extPrisma = this.prismaClient.$extends({
query: {
$allOperations({ args, query, operation }) {
if (operation === "create") {
return query(args);
}
return query({ ...args, where: { ...args?.where, tenantId: tenantId } });
}
}
});
return extPrisma;
}
if (operation === "create")
と分岐させているのは、データ作成時はtenantId
は入力してもらうためwhere句を指定する必要がないからです。
システム管理者など、すべてのデータにアクセスする必要があるクエリに関しては、extensions
を利用していない通常のクライアントを利用することで可能です。
最後に
RLSが利用できないMySQLでもORMであるprismaを利用することで、ある程度のセキュリティが担保できる仕組みを構築できます。prisma公式ドキュメントにもRLSが実現できることを謳っているので自信をもって利用できるかと思います(上のコードは自信がないので参考程度に…)。