Lino における安全な multi-tenancy
Multi-tenancy により、1 つのアプリケーションで複数の顧客、企業、組織に対応しながら、データと権限を分離できます。Lino では、Tenant はアプリケーションフローの一部として扱われます。この機能は tenancy の基盤を生成し、ITenantContext によって subdomain、host、または slug から Tenant を解決し、IUserContext によって JWT で認証されたユーザーを公開します。
JWT には、そのユーザーが認証された対象の TenantId も含まれます。Lino はその Tenant とリクエストに対して解決された Tenant を照合します。そのため、このページでは、その基盤が存在した後に決めるべきこと、つまりエンティティモデリング、組織ごとの権限、生成された抽象化の正しい使い方に焦点を当てます。
Tenant サポートの追加
システムが SaaS である場合、または異なる顧客のデータを同じアプリケーション内で扱いながらコンテキスト間の漏えいを防ぐ必要がある場合は、Tenant 機能を使用します。
lino feature tenant add
アシスタントは、Tenants の登録、ユーザー関連付け、現在の Tenant の解決、アプリケーションコンテキストへの伝播、認証連携の基盤を生成します。ITenantContext は subdomain、host、または slug によって解決された Tenant を表し、IUserContext は JWT によって認証されたユーザーを表します。そこには、そのユーザーが認証された対象の TenantId も含まれます。
生成後、どのエンティティが本当に Tenant に属し、どれが global なのかを確認してください。この確認はプロダクトモデリングの一部です。multi-tenancy は Tenants テーブルを作成するだけではなく、どのルール、画面、権限、データ、操作が Lino によって検証された現在の Tenant に依存するかを決めることです。
- Tenant-scoped: 注文、顧客の商品、会社設定、CRM の顧客、または特定の組織に属する任意のデータ。
- Global: プラットフォームの公開カタログ、プラン、内部運用リソース、管理設定、または Tenant を選択する前に使われるデータ。
- Hybrid: Tenant ごとのカスタマイズを持つ global データ。誤った重複やルール漏れを避けるため、明示的なモデリングが必要です。
モジュラーモノリスでは、通常 tenancy モジュールが元の Tenant エンティティの所有者です。catalog や CRM などの他モジュールは、このエンティティへ直接アクセスすべきではありません。Tenant の情報が必要な場合は、Tenant の作成や更新などの integration events によって供給される、必要最小限のフィールドを持つ shadow entities を保持できます。
Tenant context
Tenant 機能により、Lino は現在の Tenant の解決を一元化し、その値を ITenantContext で公開します。このコンテキストは、リクエストの subdomain、host、または slug から解決された Tenant を表し、Tenant が見つかった場合は TenantId や slug などの情報を含みます。
認証済みフローでは、IUserContext が JWT からユーザーを読み取ります。その JWT には、ユーザーが認証された対象の TenantId も含まれます。Lino は IUserContext の Tenant と ITenantContext の Tenant を照合し、ビジネスルールが食い違うコンテキストを信頼することを防ぎます。
生成された機能や後から追加した機能では、claims を手動で読み直したり、各 handler で任意の TenantId を受け取ったりするのではなく、常にこれらの抽象化を優先してください。これにより commands、queries、ページ、連携が同じ source of truth を使い、Lino が既に実装しているセキュリティ機構を維持できます。
Background Jobs、イベントコンシューマー、連携にもコンテキストが必要です。tenant-scoped の商品、顧客、ユーザーを処理する job は、処理対象の Tenant を明示的に受け取る必要があります。これは、ITenantContext と IUserContext を自動的に推論できる active HTTP request が存在しないためです。
Tenant-scoped エンティティ
Tenant-scoped エンティティは Tenant identifier を保持し、query filters に参加します。これにより、通常のフローはすべての queries で TenantId を手動で適用する必要がなくなります。機能は、検証済みコンテキストと Lino によって生成された filters を使用する必要があります。
- 特定の組織に属する aggregates には Tenant を含めます。
- Tenant と頻繁に検索されるフィールドを考慮して indexes を設定します。
- commands と queries では、Tenant を単独の権威として受け取るのではなく、
ITenantContextとIUserContextを使用します。 - imports や jobs では、どの Tenant を処理しているかを明示的に定義します。
tenant-scoped エンティティは 1 つの Tenant に属します。あるエンティティが複数の Tenants に属するように見える場合、それはモデリング判断として扱ってください。global なのか、Tenant ごとのコピーが必要なのか、関係を表す中間エンティティが不足しているのかもしれません。商品、顧客、ユーザーはモジュールごとに異なる意味を持つこともあります。その場合、元のエンティティを共有するよりも、少数のフィールドを持つ shadow entity の方が正しいことがあります。
参照と ownership も区別してください。catalog の商品は Tenant を参照できますが、それは catalog が Tenant の lifecycle を管理するという意味ではありません。tenancy モジュールは作成、有効化、ドメイン、branding、分離モードの所有者であり続けます。catalog は自分のフローをフィルタリングし検証するために必要なデータだけを保持します。
- Indexes: 検索、並び替え、Tenant ごとの一意性に使うフィールドと
TenantIdを組み合わせます。 - Commands: エンティティが tenant-scoped の場合、検証された現在の Tenant で操作を適用します。
- Queries: ページング、filters、カウンターを Lino によって検証された同じコンテキストに保ちます。
- Events: consumers が producer を不適切に照会せずに projections を更新できるよう、十分なコンテキストを含めます。
データ分離
分離は列、schema、または個別データベースで実現できます。選択はコスト、量、契約要件、運用能力によって決まります。
| 戦略 | 利点 | 使用する場面 |
|---|---|---|
| TenantId 列 | より単純で低コスト | 多くの SaaS にとって、filters と Lino context を組み合わせた良い初期選択。 |
| Tenant ごとの schema | 中間的な分離 | Tenant ごとのデータベースまでは必要ないが、より強い分離要件がある場合に有用。 |
| Tenant ごとのデータベース | 強い分離 | 契約、規模、運用がより高いコストと自動化を正当化する場合に適している。 |
初期段階の多くの SaaS では、global filters と一元化されたコンテキストを伴う Tenant ごとの列が実用的な出発点です。Lino はこの基盤を支援します。確認作業では、tenant-scoped aggregates が正しい Tenant を保持し、新しい queries が生成された抽象化によって検証された現在の Tenant を使い続けていることを確認してください。
モジュラーサービスでは、Tenant の分離とモジュールの分離は別の関心事です。Catalog モジュールは TenantId を持つ商品テーブルを持てます。CRM モジュールは TenantId を持つ顧客を持てます。そして Tenancy モジュールは元の Tenants 登録の所有者であり続けます。同じ identifier が存在することは、モジュール間の直接アクセスを許可するものではありません。
Tenant ごとの roles と権限
SaaS では、あるユーザーが 1 つの Tenant では管理者で、別の Tenant ではオペレーターであることがあります。Lino は user-Tenant の関連付けを扱う基盤を生成し、機能の権限は framework によって検証された active context の中で評価される必要があります。
Tenant の外に管理権限が存在する場合もあります。プラットフォーム運用画面は system context でのみアクセス可能であり、商品や顧客のようなページは active Tenant を要求することがあります。この分離により、アクションが global なのか、ITenantContext で解決された Tenant と IUserContext で認証されたユーザーに依存するのかが明確になります。
- 権限が組織ごとに異なる場合、Tenant ごとに roles をモデル化します。
- ページ、commands、queries の認可チェックでは現在のコンテキストを使用します。
- claims はコンパクトに保ち、認証済みフローに必要なものだけを token に含めます。
- 重要な権限が変わったら tokens を失効または更新します。
| コンテキスト | 例 | 扱い方 |
|---|---|---|
| System | プラットフォーム管理、プラン、Tenants、global 設定。 | 特定の Tenant の scope 外で実行する。 |
| Tenant | その Tenant の商品、顧客、注文、ユーザー、roles。 | Lino によって検証された現在の Tenant を使用する。 |
| Both | system と Tenant の両方に存在する一部の identity または管理機能。 | use case の中で、どのコンテキストが使われるかを宣言する。 |
JWT と Tenant セキュリティ
Tenant 機能が有効な場合、Lino は JWT を認証済みユーザーコンテキストの情報源として使用します。IUserContext は、JWT をデータベースのコピーにすることなく、ユーザーの claims と token 内の TenantId を読み取ります。
- issuer、audience、署名、有効期限を正しく構成しておきます。
- header、route、または画面 payload だけに基づく手動切り替えを受け入れるのではなく、Lino によって検証された Tenant を使用します。
- 大きすぎる claims を避けます。肥大化した tokens は運用と更新を難しくします。
- フローが監査を必要とする場合は、ユーザー、Tenant、操作を含むアクセス logs を記録します。
acme.dev.localhost や globex.dev.localhost のような subdomains を使うアプリケーションでは、ITenantContext はリクエストに対して解決された Tenant を表します。IUserContext は認証済みユーザーと JWT に含まれる Tenant を表します。Lino はこの 2 つのコンテキストを照合し、ある Tenant 向けに発行された token が別の Tenant コンテキストで使われることを防ぎます。
認可も引き続きコンテキスト依存です。たとえば商品を表示する権限は、その権限が付与され、ユーザーが認証された Tenant の中で有効です。新しい機能を作成するときは、単独の TenantId を受け取って認証済みフローを無視する近道を作るのではなく、既に利用できる権限とコンテキストを使用してください。
