マルチテナント
📍 このドキュメントの位置づけ
対象読者: idp-serverのマルチテナント実装を理解したい開発者
このドキュメントで学べること:
- マルチテナントアーキテクチャの実装詳細
- Tenant/Organization モデルの設計
- Repository層でのテナント分離パターン
- PostgreSQL RLSによるデータベースレベルの分離
- テナントコンテキスト管理の仕組み
前提知識:
- concept-01: マルチテナントの理解
- Hexagonal Architectureの基礎知識
- Repository パターンの理解
🏗️ マルチテナントアーキテクチャ概要
idp-serverは、1つのアプリケーションインスタンスで複数のテナントを完全分離するマルチテナント型IdPです。
設計原則
1. Tenant-First Design
すべてのデータアクセスでテナントを明示的に指定します。
// ✅ 正しい: Tenantを明示的に渡す
public interface UserCommandRepository {
void register(Tenant tenant, User user);
void update(Tenant tenant, User user);
void delete(Tenant tenant, UserIdentifier userIdentifier);
}
// ❌ 誤り: Tenantなしでデータアクセス(テナント漏洩リスク)
public interface UserCommandRepository {
void register(User user); // どのテナントのユーザー?
}
参考実装:
2. 二重防御(Defense in Depth)
アプリケーション層とデータベース層の両方でテナント分離を強制します。
┌─────────────────────────────────────────┐
│ Application Layer (アプリケーション層) │
│ - Repository第一引数でTenant強制 │
│ - TransactionManagerでRLS設定 │
└─────────────────────────────────────────┘
↓
┌─────────────────── ──────────────────────┐
│ Database Layer (データベース層) │
│ - Row Level Security (RLS)による強制分離│
│ - FORCE ROW LEVEL SECURITY │
└─────────────────────────────────────────┘
3. Organization-Tenant 階層構造
組織とテナントの2層構造をサポートします。
Organization (組織)
├── Tenant (ORGANIZER) - 組織管理用
├── Tenant (PUBLIC) - アプリケーション用①
└── Tenant (PUBLIC) - アプリケーション用②
📦 コアモデル
Tenant
テナントは、idp-server内での完全に独立した認証・認可ドメインを表します。
主要フィールド:
public class Tenant implements Configurable {
TenantIdentifier identifier; // UUID形式のテナントID
TenantName name; // テナント名
TenantType type; // ADMIN/ORGANIZER/PUBLIC
TenantDomain domain; // テナントドメイン(トークンissuerに使用)
AuthorizationProvider authorizationProvider;
TenantAttributes attributes; // カスタム属性
TenantFeatures features;
UIConfiguration uiConfiguration;
CorsConfiguration corsConfiguration;
SessionConfiguration sessionConfiguration;
SecurityEventLogConfiguration securityEventLogConfiguration;
SecurityEventUserAttributeConfiguration securityEventUserAttributeConfiguration;
TenantIdentityPolicy identityPolicyConfig;
OrganizationIdentifier mainOrganizationIdentifier; // 所属組織
boolean enabled;
}
参考実装: Tenant.java:34
TenantIdentifier
テナントIDを表す値オブジェクトです。
public class TenantIdentifier implements UuidConvertable {
String value; // UUID文字列
public UUID valueAsUuid() {
return convertUuid(value);
}
public boolean exists() {
return Objects.nonNull(value) && !value.isEmpty();
}
}
参考実装: TenantIdentifier.java:23
TenantType
テナントの種別を定義します。
public enum TenantType {
ADMIN, // システム管理用テナント(初期化時に自動作成)
ORGANIZER, // 組織管理用テナント(組織作成時に自動作成)
PUBLIC; // アプリケーション用テナント(API経由で作成)
}
参考実装: TenantType.java:19
使い分け:
- ADMIN: システム全体の初期設定・管理用(1つのみ)
- ORGANIZER: 組織内のテナント管理・組織メンバー管理用(組織ごとに1つ)
- PUBLIC: 実際のアプリケーション認証用(組織ごとに複数作成可能)
Organization
組織は、複数のテナントをグループ化する上位概念です。
public class Organization implements Configurable {
OrganizationIdentifier identifier; // UUID形式の組織ID
OrganizationName name; // 組織名
OrganizationDescription description; // 組織説明
AssignedTenants assignedTenants; // 所属テナント一覧
boolean enabled;
public AssignedTenant findOrgTenant() {
// type="ORGANIZER"のテナントを取得
for (AssignedTenant tenant : assignedTenants()) {
if ("ORGANIZER".equals(tenant.type())) {
return tenant;
}
}
throw new AdminTenantNotFoundException(...);
}
public boolean hasAssignedTenant(TenantIdentifier tenantIdentifier) {
return assignedTenants.contains(tenantIdentifier);
}
}
参考実装: Organization.java:23
OrganizationIdentifier
組織IDを表す値オブジェクトです。
public class OrganizationIdentifier implements UuidConvertable {
String value; // UUID文字列
public UUID valueAsUuid() {
return convertUuid(value);
}
public boolean exists() {
return value != null && !value.isEmpty();
}
}
参考実装: OrganizationIdentifier.java:24
🛠️ Repository層の実装パターン
Tenant第一引数パターン
すべて のRepository操作で、第一引数にTenantを渡すことでテナント分離を設計レベルで強制します。
Query Repository
public interface AuthenticationConfigurationQueryRepository {
// ✅ すべてのメソッドで第一引数がTenant
AuthenticationConfiguration get(Tenant tenant, String key);
AuthenticationConfiguration find(Tenant tenant, String key);
AuthenticationConfiguration find(Tenant tenant, AuthenticationConfigurationIdentifier identifier);
AuthenticationConfiguration findWithDisabled(Tenant tenant, AuthenticationConfigurationIdentifier identifier, boolean includeDisabled);
long findTotalCount(Tenant tenant);
List<AuthenticationConfiguration> findList(Tenant tenant, int limit, int offset);
}
参考実装: AuthenticationConfigurationQueryRepository.java:24
Command Repository
public interface UserCommandRepository {
// ✅ すべてのメソッドで第一引数がTenant
void register(Tenant tenant, User user);
void update(Tenant tenant, User user);
void updatePassword(Tenant tenant, User user);
void delete(Tenant tenant, UserIdentifier userIdentifier);
}
参考実装: UserCommandRepository.java:23
例外: OrganizationRepository
組織はテナントより上位概念のため、Tenantを第一引数に取りません。
public interface OrganizationRepository {
// ✅ 組織操作では、OrganizationIdentifierのみを使用
void register(Organization organization);
void update(Organization organization);
void delete(OrganizationIdentifier identifier);
Organization get(OrganizationIdentifier identifier);
List<Organization> findList(OrganizationQueries queries);
}
参考実装: OrganizationRepository.java:21
🔐 データベースレベルのテナント分離
PostgreSQL Row Level Security (RLS)
PostgreSQLを使用する場合、Row Level Security (RLS) によりデータベースレベルでテナント分離を強制します。
DDLでのRLS設定
-- テナントテーブルにRLS有効化
ALTER TABLE tenant ENABLE ROW LEVEL SECURITY;
-- ポリシー作成: app.tenant_idと一致する行のみアクセス可能
CREATE POLICY tenant_isolation_policy
ON tenant
USING (id = current_setting('app.tenant_id')::uuid);
-- 強制RLS: DB管理者も制限
ALTER TABLE tenant FORCE ROW LEVEL SECURITY;
参考実装: V0_9_0__init_lib.sql (RLS設定箇所)
全テーブルへのRLS適用
idp-serverでは、以下のテーブルにRLS が適用されています:
tenant- テナント情報organization_tenants- 組織-テナント関係authorization_server_configuration- 認可サーバー設定permission- 権限role- ロールidp_user- ユーザーclient_configuration- クライアント設定authentication_configuration- 認証設定- その他すべてのテナント依存テーブル
確認方法:
-- RLS設定されているテーブルとポリシーを確認
SELECT schemaname, tablename, policyname, qual as policy_condition
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename, policyname;
参考実装: select-rls-policy.sql
TransactionManagerによるテナントコンテキスト設定
TransactionManagerは、トランザクション開始 時にPostgreSQLセッション変数app.tenant_idを設定します。
public class TransactionManager {
public static void beginTransaction(DatabaseType databaseType, TenantIdentifier tenantIdentifier) {
if (connectionHolder.get() != null) {
throw new SqlRuntimeException("Transaction already started");
}
OperationContext.set(OperationType.WRITE);
Connection conn = dbConnectionProvider.getConnection(
databaseType, AdminTenantContext.isAdmin(tenantIdentifier));
// PostgreSQLの場合、RLS用にテナントIDを設定
if (databaseType == DatabaseType.POSTGRESQL) {
setTenantId(conn, tenantIdentifier);
}
connectionHolder.set(conn);
}
/**
* Sets the current tenant identifier for Row-Level Security (RLS).
*
* PostgreSQLのset_config()関数でapp.tenant_idを設定します。
* is_local=trueにより、トランザクション終了時に自動クリアされます。
*/
private static void setTenantId(Connection conn, TenantIdentifier tenantIdentifier) {
log.trace("[RLS] SET app.tenant_id: tenant={}", tenantIdentifier.value());
// PreparedStatementでSQLインジェクション対策
try (var stmt = conn.prepareStatement("SELECT set_config('app.tenant_id', ?, true)")) {
stmt.setString(1, tenantIdentifier.value());
stmt.execute();
} catch (SQLException e) {
throw new SqlRuntimeException("Failed to set tenant_id", e);
}
}
}
参考実装: TransactionManager.java:25
重要なポイント:
1. is_local = true の重要性
SELECT set_config('app.tenant_id', 'xxx', true)
↑
is_local=true
true: トランザクション終了時に自動クリア(推奨)false: セッション全体で保持(コネクションプール使用時に危険)
危険なシナリオ(falseの場合):
1. Tenant A のトランザクション開始 → app.tenant_id = "A"
2. トランザクション終了 → app.tenant_id = "A" のまま残る
3. コネクションがプールに戻る
4. Tenant B がそのコネクションを取得
5. app.tenant_id = "A" のまま(Tenant B のデータアクセスがTenant A として実行される!)
2. トランザクション開始後に設定
// ❌ 誤り: トランザクション開始前に設定
setTenantId(conn, tenantIdentifier);
conn.setAutoCommit(false); // この後だとset_configが無効化される
// ✅ 正しい: トランザクション開始後に設定
conn.setAutoCommit(false);
setTenantId(conn, tenantIdentifier);