prismaを利用してMySQLに仮想的な行レベルセキュリティ(RLS)を設定する

データベース

背景と目的

現時点でMySQLはRLS(Row Level Security)をサポートしていません。マルチテナントなどを単一DBで管理する場合、何らかのコーディングミスなどで他テナントのデータへアクセスしてしまうと大変です。RLSを利用することによってデータアクセス権限周りを堅固にできます。今回はRLSが利用できないMySQLを、Node.js向けORMであるprismaを使って仮想的にRLS(のようなもの)を設定していきます。

設定方法

RLSを再現するため、prismaの4.16.0以降で導入されたPrisma Client extensionsを利用します。extensionsではミドルウェアのような処理を挿入したり共通のクエリ操作などを設定することが可能です。

Prisma Client extensions: query component | Prisma Documentation
Extend the functionality of Prisma Client, query component

今回はマルチテナントのテナント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が実現できることを謳っているので自信をもって利用できるかと思います(上のコードは自信がないので参考程度に…)。

タイトルとURLをコピーしました