真北科技Zhenbei
UIAM · アーキテクチャ

テナント分離はドキュメントではなくクエリ層に書くべき理由

マルチテナントシステムで最も危険な脆弱性は「書き間違い」ではなく「書き忘れ」です。私たちはテナント述語を開発者の記憶から jOOQ の訪問リスナーへ移しました。

マルチテナントシステムは、公開してしばらく経つと必ず同じ問題に遭遇します。あるテーブルのクエリから tenant_id 条件が漏れていた、という問題です。コードレビューではまったく正常に見えます —— SQL の構文に問題はなく、業務ロジックも筋が通っている。ただ、本来は見えるはずのない行が余分に返ってくるだけです。

この種の問題の根源は、セキュリティ境界を「開発者が覚えて書く」という前提に置いていることにあります。人は忘れるし、新しい人は知らないし、リファクタリングで失われます。だから私たちのやり方は、テナント述語の強制チェックをデータアクセス層へ降ろすことです。

散らばった記憶ではなく、一箇所での宣言

最初のステップは、「どのテーブルがテナントスコープに属するか」を一つの集中レジストリに変えることです。45 のテーブルを TenantScopedTables で一度だけ宣言します。数十の Mapper に散らばらせて慣習に頼る、というやり方ではありません。

uiam-core — テナントスコープテーブルの登録
01// 集中注册:租户作用域表是一份清单,不是隐式约定
02object TenantScopedTables {
03 val ALL: Set<String> = setOf(
04 "sys_user", "sys_role", "sys_permission",
05 "sys_dept", "sys_space", "sys_audit_event",
06 // …共 45 张
07 )
08}

欠落した述語をクエリ層で捕捉する

次のステップは、jOOQ に毎回のクエリでチェックさせることです。この SQL はテナントスコープのテーブルに触れているか? 触れているなら、WHERE にテナント述語は付いているか? 付いていなければ、設定に従ってセキュリティログを記録するか、その場でエラーにします。

uiam-core — TenantScopeVisitListener
01override fun visit(ctx: VisitContext): Queries? {
02 if (!touchesTenantScopedTable(ctx.query())) return null
03
04 if (!hasTenantPredicate(ctx.query())) {
05 // DETECT_LOG:只记录,用于灰度观察
06 // DETECT_THROW:直接失败,生产环境默认
07 when (mode) {
08 DETECT_LOG -> audit.missingTenantPredicate(ctx)
09 DETECT_THROW -> throw MissingTenantPredicateException()
10 }
11 }
12 return null
13}

唯一の信頼できる情報源はトークンであって、リクエストヘッダではない

tenant_id は JWT だけから取ります。リクエストヘッダにもテナントが宣言されていて、両者が一致しない場合はその場で拒否します —— リクエストヘッダは呼び出し側が好きに書けるものであり、トークンはそうではないからです。

  • TenantContextFilter が JWT から tenant_id をパースし、リクエストコンテキストへ書き込む
  • ヘッダの宣言とトークンが不一致 → TenantMismatchException(403)
  • テナントを跨ぐ必要が本当にあるシステムレベル操作は runAsSystem による明示的なバイパスを通し、バイパス自体も監査に入る
  • リクエスト終了時に UserContext をクリアし、スレッドプールの再利用によるリクエスト間リークを防ぐ

代償は何か

この仕組みは無料ではありません。すべてのテナントスコープテーブルに統一されたテナント列名の規約を求めますし、テナントを跨ぐ必要が本当にあるクエリを書く開発者には、バイパス経路を明示的に呼び出すことを求めます —— 条件なしのクエリを何気なく書くより、ずっと面倒です。

私たちはこの面倒を受け入れます。マルチテナントシステムにおいては、「少しの面倒」と「データ漏洩」は同じ桁の問題ではないからです。

More Notes
Get in touch

アイデンティティ・エージェント・プライベートドメインの複雑さを、統治可能なひとつのカーネルへ

既存の IAM の置き換え、エージェント基盤の構築、あるいはプライベートドメイン運用の本格化——まずは 30 分のアーキテクチャ相談から始めましょう。私たちが得意とする種類の問題かを先に判断し、適さない場合は率直にお伝えします。