1人のユーザーが複数の組織やワークスペースで作業できるSaaSでは、「このリソースを作成したのは誰か?」が、必ずしも「誰に帰属するのか?」の答えにはなりません。この違いは、誰がリソースを閲覧、変更、譲渡できるか、またチームを離れる人がいても誰が保持できるかに影響します。後回しにすると、ルールがクエリ、コントローラー、バックグラウンドタスクに分散しがちです。
マルチユーザーSaaSにおけるリソースの所有権は、ドメイン上の決定です。どのエンティティがリソースと主要な関係を持つのか、そしてその関係にどのような結果が伴うのかを定めます。認証、メンバーシップ、権限と同じものではありません。明示的にモデル化すれば、プロダクト、データ、コードで一貫したルールを共有できます。
アイデンティティ、所属、所有権は別々の関係

ユーザーはログインできるアイデンティティを表します。所属は、そのアイデンティティが組織やワークスペースに参加していることを示し、ロールや状態を伴う場合もあります。所有権は、プロダクトのルールに基づき、どのエンティティがリソースに対して責任を持つかを示します。1人のユーザーが複数のワークスペースに所属しながら、そのうちの1つに属するドキュメントを作成することもあります。
作成者も分けて考えるとよいでしょう。created_byフィールドには最初の操作を行った人を記録できますが、それだけで所有権を決めるべきではありません。レコードを作成した人が組織を離れても、リソースは組織に属し続ける場合があります。反対に、別の人が最初に作成したリソースを、後からある人が取得することもあります。
次の3つの問いへの答えを文書化しておくと役立ちます。所有者は誰か、誰がそのリソースを操作できるか、重要な各操作を誰が行ったか。同じフィールドや関係ですべてに答えようとしているなら、モデルで概念が混在している可能性があります。
個人所有、組織所有、共有所有から選ぶ
- 個人所有:リソースがユーザーに属します。プライベート設定のように、チームではなく個人にライフサイクルが紐づく場合に適しています。アカウントが無効化または削除されたときの扱いを決めておく必要があります。
- 組織所有:組織またはワークスペースに属します。メンバーが入れ替わってもチームがリソースを利用し続ける必要がある場合に適しています。ユーザーの所属はアクセスを可能にしますが、そのユーザー個人を所有者にするものではありません。
- 共有所有:複数のエンティティが所有または責任の関係を持てます。ドメインで本当に共同所有が必要な場合に限って使いましょう。複数人に閲覧や編集を許可する要件であれば、通常は複数の所有者ではなく、メンバーシップや権限によるアクセスの問題です。
この判断は、テーブルを簡略化したいという好みではなく、リソースのライフサイクルとビジネスルールに沿って行うべきです。チームが変わったとき、誰がリソースを保持できるのか、削除を決められるのか、責任を負うのかを確認してください。また、「ワークスペース」と「組織」も区別します。前者が独自のライフサイクルと境界を持つ実質的な下位区分なら、適切な所有者になり得ます。単なるビューやナビゲーション上のコンテキストなら、そうでないかもしれません。
所有者を推測せずにドメインを表現する
シンプルなモデルでは、リソーステーブルにowner_typeとowner_idなどの明示的な所有者を設けるか、user_idとorganization_idのように別々の参照を持たせます。前者は複数の所有者タイプに対応しやすい一方、参照の検証が必要で、ポリモーフィックな関連における参照整合性の制約も考慮しなければなりません。別々の参照は関連を明確にできますが、無効な組み合わせを防ぐ必要があります。
別の方法として、ドメイン上、個人アカウントとチームの両方を本当にその配下に表すのであれば、workspaceのような共通エンティティを設けられます。違いを隠すためだけに導入してはいけません。明確な意味と安定したルールが必要です。どの設計でも、リソースが互いに両立しない複数の所有者に同時に紐づいたり、所有権が必須なのに所有者がなかったりしないよう制約を設けます。
メンバーシップは、たとえばユーザーと組織の関係に状態やロールを含めて、別にモデル化します。UI上のアクティブなコンテキストは、その人が現在どこで作業しているかを示せますが、それだけでは権限の根拠になりません。各操作で検証する必要があります。同様に、所有者を「リソースを作成した人」や「現在選択されている組織」として算出してはいけません。
読み取り、書き込み、自動処理に所有権を適用する
クエリには所有権のスコープと適切なアクセスチェックを組み込む必要があります。識別子だけでリソースを取得し、該当しないものをUIで隠せばよいわけではありません。PHPアプリケーションでは、コンテキストの解決と認可ルールを再利用可能な層に集約し、不完全な条件を各コントローラーに重複して記述しないようにします。
書き込み時には、許可された所有者と、操作するユーザーの有効な所属を検証します。また、関連するリソース、たとえば移動先フォルダーが互換性のあるスコープに属することも確認します。そうしないと、更新によってスコープをまたぐ関連が作られる可能性があります。管理者向け操作では、例外的な認可を通常のルールと明確に分け、誰がなぜ実行したかを記録します。
キュー、スケジュール実行コマンド、webhook、インポート処理には、対話中のユーザーが常に存在するとは限りません。暗黙のセッションに依存せず、検証済みの所有者またはコンテキストの識別子を渡す必要があります。処理にはWebアプリケーションと同じスコープルールを適用します。拡張された権限で動作する場合は、その権限を制限し、監査します。
譲渡をドメイン操作として扱う
所有者の変更を、汎用的なフィールド編集として扱うべきではありません。誰が譲渡を開始できるか、譲受人がそのワークスペースとどのような関係にある必要があるか、招待が保留中の場合やアカウントが無効な場合にどうするか、といった事前条件を定義します。複数の関連や付随する処理を更新する場合、操作はアトミックであるべきです。
ファイル、インテグレーション、請求、自動化などの依存関係をどう扱うかも決めます。すべてをリソースと一緒に移すべきだと考えてはいけません。その方針はプロダクトによって異なります。すべての変更を1つのトランザクションで完了できない場合は、明確な状態と再試行時の冪等性を備えた、復旧可能なプロセスを設計します。
該当する場合は、実行者、以前の所有者、新しい所有者、日時、理由を監査ログに残します。これにより、変更を承認した人と技術的な操作を実行した人を区別できます。所有権が変わっても、元の作成者の記録は変更せずに保持できます。
モデルの問題を明らかにする境界条件をテストする
許可されるアクセスと拒否されるアクセスの両方を確認するため、サービスと統合のテストを自動化します。有用なテストマトリクスには、2つの組織に所属するユーザー、両方の組織における自分のリソースと他人のリソース、停止または取り消されたメンバーシップ、適格でない譲渡先への譲渡、既知の識別子を使ったアクセスが含まれます。応答とデータへの影響を確認してください。ボタンを隠すことは認可のテストにはなりません。
有効な所有者がいないリソース、重複した関係、譲渡中の失敗もテストします。スキーマで所有者を必須とするなら、データベース制約でその要件を支える必要があります。また制約を追加する前に、マイグレーションで一貫性のない既存レコードを検出します。バックグラウンドタスクでは、コンテキストが維持されることと、再試行で変更が重複しないことを確認します。
SaaSを拡張する前のチェックリスト

- リソースの種類ごとに、個人、組織、ワークスペースのいずれに属するか定義されていますか?
- 所有権は、作成者、所属、ロール、アクティブなコンテキストと区別されていますか?
- 読み取りと書き込みのクエリで、所有者と現在のメンバーシップを検証していますか?
- 自動処理に明示的なスコープを渡し、同等のアクセス制御を適用していますか?
- 譲渡に事前条件、定義済みの処理、監査、復旧方針がありますか?
- 複数のワークスペースを持つユーザー、退会、スコープをまたぐアクセス、不整合データのテストがありますか?
文書化されていない慣例に依存する答えが1つでもあれば、リソースの種類やコラボレーションのフローを増やす前に明確にしておくとよいでしょう。明示的なモデルですべての複雑なケースをなくせるわけではありませんが、判断を可視化し、スキーマ、クエリ、テストで検証できるようになります。



