権限設計プラクティス

マルチテナントSaaSにおける安全なデータアクセス:権限設計で実現するテナント間隔離

Tags: マルチテナント, SaaS, データ隔離, 権限設計, セキュリティ, 開発

はじめに:マルチテナントSaaSにおけるデータ隔離の重要性

多くのSaaS(Software as a Service)アプリケーションは、単一のインスタンスで複数の顧客(テナント)にサービスを提供するマルチテナントアーキテクチャを採用しています。このアーキテクチャはリソースの効率的な利用や運用コストの削減に貢献しますが、最も重要な課題の一つとして「テナント間のデータ隔離」が挙げられます。あるテナントのデータが、意図せず他のテナントからアクセス可能になってしまうことは、セキュリティ上、信頼性上の重大な問題となります。

このデータ隔離を実現するための鍵となるのが、適切な権限設計です。アプリケーションのどの部分で、どのようにテナントを識別し、そのテナントに紐づくデータのみにアクセスを制限するかを設計し、実装する必要があります。

本記事では、マルチテナントシステムにおけるデータ隔離の基本的な考え方から、アプリケーション層やデータベース層でデータ隔離を実現するための具体的な権限管理の実践パターンについて解説します。経験数年程度のソフトウェアエンジニアの皆様が、自身の開発するSaaSにおいて、よりセキュアなデータアクセスを実現するための一助となれば幸いです。

マルチテナントシステムにおけるデータ隔離の基本

マルチテナントアーキテクチャにおけるデータ隔離の方法は、主にデータベースの構成によっていくつかのレベルに分けられます。それぞれのレベルで、データ隔離を保証するための責任範囲や権限管理の方法が異なります。

  1. 共有データベース・共有スキーマ (Shared Database, Shared Schema): 最もシンプルで、リソース効率が高い構成です。全てのテナントが同じデータベース、同じスキーマ内のテーブルを共有します。各テーブルには、どのデータがどのテナントに属するかを識別するための「テナントID」のようなカラムが追加されます。データ隔離はアプリケーション層での責任となります。アプリケーションが常にテナントIDでデータをフィルタリングすることにより、特定のテナントのユーザーは自身のテナントのデータのみにアクセスできるよう制御します。権限管理の複雑さがアプリケーションコードに集中します。

  2. 共有データベース・分離スキーマ (Shared Database, Separated Schema): 各テナントごとにデータベース内に独立したスキーマを作成し、その中にテーブルを作成する構成です。全てのテナントが同じデータベースサーバーを共有しますが、データはスキーマによって論理的に分離されます。データベースのユーザー権限をテナントごとに設定することで、特定のスキーマ(テナントのデータ)へのアクセスを制御することが可能です。データ隔離は主にデータベース層で行われますが、アプリケーションは接続先のスキーマを動的に切り替える必要があります。

  3. 分離データベース (Separated Database): テナントごとに独立したデータベースインスタンス(またはサーバー)を用意する構成です。物理的または論理的にデータが完全に分離されます。最も隔離レベルが高く、セキュリティリスクは低いですが、運用コストやリソース使用量は増加します。データ隔離はデータベース層およびインフラ層で保証されます。アプリケーションは、ユーザーが属するテナントに応じて適切なデータベースに接続する必要があります。

どの構成を選択するかは、要件、セキュリティニーズ、運用能力などによって異なりますが、特にスタートアップや中小規模のSaaSでは「共有データベース・共有スキーマ」が採用されるケースが多く見られます。本記事では、この共有データベース・共有スキーマ構成における、アプリケーション層でのデータ隔離を中心とした権限設計に焦点を当てて解説します。

アプリケーション層でのデータ隔離を実現する権限管理

共有データベース・共有スキーマ構成では、アプリケーションコードがデータ隔離の責任を負います。これは、全てのデータアクセス操作において、現在ログインしているユーザーが属するテナントのIDを用いてデータをフィルタリングする必要があるということです。

基本的な考え方:全てのデータアクセスにテナントIDによるフィルタリングを含める

ユーザーからのリクエストを受け付けたアプリケーションは、まずそのユーザーがどのテナントに属するかを認証・認可システムから取得します。このテナントIDを、データベースへのクエリやデータ操作を行う際に必ず条件として含めます。

例えば、ユーザー一覧を取得するAPIエンドポイントがあるとします。通常のクエリは SELECT * FROM users のようになるかもしれませんが、マルチテナント環境では SELECT * FROM users WHERE tenant_id = 'current_tenant_id' のように、必ず tenant_id でフィルタリングする必要があります。

-- ユーザー一覧を取得するクエリ(tenant_idによるフィルタリングなし - 非セキュア!)
SELECT id, name, email FROM users;

-- マルチテナント対応のユーザー一覧取得クエリ
SELECT id, name, email FROM users WHERE tenant_id = 'authenticated_user_tenant_id';

データ更新や削除についても同様です。

-- ユーザー情報を更新するクエリ(tenant_idによるフィルタリングなし - 非セキュア!)
UPDATE users SET email = 'new_email@example.com' WHERE id = 123;

-- マルチテナント対応のユーザー情報更新クエリ
UPDATE users SET email = 'new_email@example.com' WHERE id = 123 AND tenant_id = 'authenticated_user_tenant_id';

WHERE tenant_id = '...' の条件を含め忘れると、他のテナントのデータにアクセスしたり、更新・削除してしまったりするセキュリティ上の脆弱性となります。

実装パターン:データアクセスの共通化と自動化

全てのクエリに手動で WHERE tenant_id = ... を追加するのは、実装漏れのリスクが高く現実的ではありません。フレームワークやORM(Object-Relational Mapper)を活用し、データフィルタリングを共通化・自動化することが推奨されます。

多くのORMやデータアクセスライブラリには、グローバルなクエリフィルタリングや、特定の属性に基づいた自動的なクエリ条件追加をサポートする機能があります。

例:ORMを使った実装の考え方(擬似コード)

# アプリケーションコードの一部
current_tenant_id = get_current_authenticated_tenant_id() # 現在のテナントIDを取得

# ユーザーオブジェクトを取得する際に、ORMが自動的にテナントIDフィルタを適用する
# 内部的には WHERE tenant_id = '...' がクエリに追加される
user = session.query(User).filter_by(id=user_id).first()

# 複数のユーザーを取得する場合も同様
active_users = session.query(User).filter_by(status='active').all()

これを実現するためには、ORMの設定やカスタム機能を活用します。例えば、SQLAlchemy(Python)ではイベントリスナーやカスタムクエリビルダ、Hibernate(Java)ではInterceptorやFilterなどを用いて、各エンティティ(テーブル)に対するクエリが発行される際に自動的にテナントIDによるフィルタリング条件を追加するように実装できます。

例:データベースの行レベルセキュリティ (Row Level Security, RLS)

一部のデータベースシステム(PostgreSQL, SQL Server, Oracleなど)は、データベースレベルで特定の条件を満たす行のみにアクセスを許可する「行レベルセキュリティ(RLS)」機能を提供しています。これを利用すると、アプリケーションが発行するクエリの内容に関わらず、データベース側でテナントIDによるフィルタリングを強制できます。

PostgreSQLにおけるRLSの設定例:

-- user_idに基づいて現在のテナントIDを取得する関数(例)
CREATE OR REPLACE FUNCTION get_current_tenant_id()
RETURNS INTEGER AS $$
SELECT tenant_id FROM sessions WHERE session_id = current_setting('app.session_id');
$$ LANGUAGE SQL STABLE;

-- usersテーブルにRLSを有効化
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- テナントIDが一致する行のみアクセスを許可するポリシーを設定
CREATE POLICY tenant_isolation_policy ON users
FOR ALL
USING (tenant_id = get_current_tenant_id()); -- SELECT, UPDATE, DELETEに適用される条件

この設定により、users テーブルへのアクセスは、get_current_tenant_id() 関数が返す値(現在のセッションのテナントID)と行の tenant_id が一致する場合にのみ許可されます。アプリケーションコード側で WHERE tenant_id = ... を書き忘れても、データベースがアクセスをブロックするため、より堅牢なデータ隔離を実現できます。

RLSは強力な機能ですが、対応しているデータベースシステムが限定される点、パフォーマンスへの影響を考慮する必要がある点に注意が必要です。

認証・認可システムとの連携

データ隔離における権限管理は、認証・認可システムと密接に関連します。

  1. 認証: ユーザーがシステムにログインする際に、正当なユーザーであることを確認します。
  2. テナント識別のための情報取得: 認証後、そのユーザーがどのテナントに属するのか、認証システムやユーザー管理データベースから情報を取得します。これはユーザープロフィール情報や、認証トークン(例:JWT)のクレームに含まれることが多いです。
  3. 認可: ユーザーが要求する操作(例:データ閲覧、更新)が、そのユーザーのロールや権限、そしてテナントIDに基づいて許可されるかを判断します。データアクセスが必要な操作の場合は、ここで取得したテナントIDをデータフィルタリングに利用します。

ユーザーのセッション情報や認証コンテキストに現在のテナントIDを保持し、データアクセス層(ORMやDBコネクタ)からその情報にアクセスできるように設計することが一般的です。

権限設計上の考慮事項

マルチテナントシステムにおいて、データ隔離の観点から権限設計を考える際には、いくつかの特殊なケースや注意点があります。

権限ミスの防止とセキュアなコーディング

データ隔離における権限ミスは、情報漏洩に直結する重大な脆弱性です。これを防ぐためには、開発プロセス全体でセキュリティを考慮する必要があります。

まとめ

マルチテナントSaaSにおけるデータ隔離は、サービスの信頼性と顧客からの信頼を維持するために不可欠な要件です。適切な権限設計とそれを支える技術的な仕組みが、安全なデータアクセスを実現します。

本記事では、マルチテナント構成におけるデータ隔離のレベルを紹介し、特に共有データベース・共有スキーマ構成におけるアプリケーション層でのデータ隔離の実践方法として、テナントIDによるデータフィルタリング、ORMやRLSを活用した自動化について解説しました。また、認証・認可システムとの連携や、権限設計上の考慮事項、そして権限ミスを防ぐための開発プラクティスについても触れました。

データ隔離に関する権限管理は、システム設計の初期段階から考慮し、開発・テストを通じて継続的に検証していくことが重要です。本記事で紹介した内容が、皆様のSaaS開発におけるセキュアな権限設計の一助となれば幸いです。