{
    "componentChunkName": "component---src-templates-post-js",
    "path": "/saas-ontology-2/",
    "result": {"data":{"ghostPost":{"id":"Ghost__Post__6a8ebf76d3fc9f00013c1f86","title":"SaaSを題材にしたオントロジーを作ってみようシリーズ #2 組織の単位など","slug":"saas-ontology-2","featured":false,"feature_image":"https://ghost.tech.anti-pattern.co.jp/content/images/2026/08/----------2026-08-26-19.43.18.png","excerpt":"\nこんにちは！ 第1回目はいかがでしたでしょうか？！難しかったですかね？\n\nだとしても、第2回を読もうとしていただいて、ありがとうございます！\n\n第2回は、さらにモリモリの内容になっちゃいましたが、テナントをベースとしたさらなる深掘りと、オントロジーの活用例の第一歩として実際にクエリをしてみたいと思います！\n\nクエリには、「SPARQL」というクエリ言語を使います。突然登場して混乱するかもなので、「SPARQL」を初めて聞いたという方は、\n\n * SPARQLって結局何なの？実際にRDFデータに問い合わせしてみて勉強しよう！\n   [https://tech.anti-pattern.co.jp/whatisspaeql/]\n\nという別のブログを書いておりますので、まずこちらをざっと眺めてみてください！\n\nそしてさらに！「SHACL」というものも登場してきます。これは、ざっくり言うと「RDFデータが期待する形になっているかを検証するための言語」なのですが、こちらについても\n\n * SHACLって結局何なの？実際にデータを検証してみて理解する [https://tech.anti-pat","custom_excerpt":null,"visibility":"public","created_at_pretty":"26 August, 2026","published_at_pretty":"26 August, 2026","updated_at_pretty":"26 August, 2026","created_at":"2026-08-26T19:27:02.000+09:00","published_at":"2026-08-26T19:44:00.000+09:00","updated_at":"2026-08-26T19:44:00.000+09:00","meta_title":null,"meta_description":null,"og_description":null,"og_image":null,"og_title":null,"twitter_description":null,"twitter_image":null,"twitter_title":null,"authors":[{"name":"Akihiro YAGASAKI","slug":"akihiro","bio":null,"profile_image":"https://ghost.tech.anti-pattern.co.jp/content/images/2022/04/yagasaki--2-.jpeg","twitter":null,"facebook":null,"website":null}],"primary_author":{"name":"Akihiro YAGASAKI","slug":"akihiro","bio":null,"profile_image":"https://ghost.tech.anti-pattern.co.jp/content/images/2022/04/yagasaki--2-.jpeg","twitter":null,"facebook":null,"website":null},"primary_tag":{"name":"Ontology","slug":"ontology","description":null,"feature_image":null,"meta_description":null,"meta_title":null,"visibility":"public"},"tags":[{"name":"Ontology","slug":"ontology","description":null,"feature_image":null,"meta_description":null,"meta_title":null,"visibility":"public"}],"plaintext":"\nこんにちは！ 第1回目はいかがでしたでしょうか？！難しかったですかね？\n\nだとしても、第2回を読もうとしていただいて、ありがとうございます！\n\n第2回は、さらにモリモリの内容になっちゃいましたが、テナントをベースとしたさらなる深掘りと、オントロジーの活用例の第一歩として実際にクエリをしてみたいと思います！\n\nクエリには、「SPARQL」というクエリ言語を使います。突然登場して混乱するかもなので、「SPARQL」を初めて聞いたという方は、\n\n * SPARQLって結局何なの？実際にRDFデータに問い合わせしてみて勉強しよう！\n   [https://tech.anti-pattern.co.jp/whatisspaeql/]\n\nという別のブログを書いておりますので、まずこちらをざっと眺めてみてください！\n\nそしてさらに！「SHACL」というものも登場してきます。これは、ざっくり言うと「RDFデータが期待する形になっているかを検証するための言語」なのですが、こちらについても\n\n * SHACLって結局何なの？実際にデータを検証してみて理解する [https://tech.anti-pattern.co.jp/whatisshacl/]\n\nを眺めてみてください！\n\nでは、さっそく第1回目の復習も兼ねて、テナントについて改めて考えてみたいと思います。\n\n0. Tenantは会社なのか？\nOrganization・Workspace・Teamを分けて考える\n前回は、B2B SaaSの最小構造として、次のモデルを作りました。\n\nTenant\n├── Membership\n│   ├── User\n│   └── Role\n│       └── Permission\n│\n├── Subscription\n│   └── Plan\n│\n└── Resource\n\n\nそして、Tenantを次のように定義しました。\n\nTenant\n＝ SaaS上でユーザー、権限、契約、データを分離する論理的な境界\n\n\nこの定義は、B2B SaaSを考えるうえで非常に重要ですが、実際のSaaSを設計し始めると、ほぼ確実に次の疑問が出てきます。\n\nTenantは会社なのか？\n\n\n最初は多くの場合、Tenantを顧客企業と同じものとして扱います。\n\n株式会社ABC\n＝ Tenant ABC\n\n\n小規模なSaaSであれば、これでも十分ですが、顧客企業が大きくなったり、複数部署や子会社で利用したりすると、会社とTenantを同一視することが難しくなります。\n\n\n--------------------------------------------------------------------------------\n\n1. 会社とTenantが一致しない例\n株式会社ABCという会社があるとします。\n\n株式会社ABC\n├── 営業本部\n│   ├── 東日本営業部\n│   └── 西日本営業部\n├── 開発本部\n│   ├── 製品開発部\n│   └── 基盤開発部\n└── 管理本部\n    ├── 経理部\n    └── 人事部\n\n\nこの会社が、あるプロジェクト管理SaaSを導入したとします。会社全体で1つの利用環境を使うのであれば、構造は単純です。\n\nTenant ABC\n\n\nしかし、次のような要求が発生するかもしれません。\n\n * 営業部と開発部でデータを分離したい\n * 子会社ごとに管理者を分けたい\n * 部門ごとに利用料金を集計したい\n * 外部委託先には一部のプロジェクトだけ見せたい\n * 海外法人と国内法人でデータ保存地域を分けたい\n * 会社全体では契約を一本化したい\n\nここで、すべての部署を別Tenantにすると、今度は会社全体としての管理が分断されてしまいます。\n\n営業本部Tenant\n開発本部Tenant\n管理本部Tenant\n\n\n反対に、すべてを1つのTenantに押し込むと、今度はデータや権限の分離が難しくなります。つまり、現実世界の組織構造と、SaaS上の利用構造は、似ているようで異なるのです。\n\n\n--------------------------------------------------------------------------------\n\n2. Tenantは現実世界の組織ではない\nTenantは、会社や部署そのものではなく、SaaS上で何かを分離するための境界です。\n\nTenant\n＝ SaaS上の管理・契約・セキュリティ・データ分離の境界\n\n\nSaaSにおいてTenantを分ける理由には、次のようなものがあります。\n\n * データを分離する\n * 管理者を分離する\n * 契約を分離する\n * 請求を分離する\n * 認証設定を分離する\n * データ保存地域を分離する\n * セキュリティポリシーを分離する\n * 監査ログを分離する\n\nしたがって、現実世界の会社が1つであっても、Tenantが複数になることがあります。\n\n株式会社ABC\n├── 国内事業Tenant\n└── 海外事業Tenant\n\n\n逆に、複数の会社が1つのTenantを共有することもあります。\n\nABCグループTenant\n├── 株式会社ABC\n├── ABC販売株式会社\n└── ABCシステムズ株式会社\n\n\nこの場合、会社は3つあってもTenantは1つです。\n\n\n--------------------------------------------------------------------------------\n\n3. Organizationを追加する\n現実世界の会社や部署を表すため、Organizationという概念を導入します。\n\nOrganization\n＝ 現実世界に存在する組織・団体\n\n\nOrganizationの例には、次のようなものがあります。\n\n * 法人\n * 会社\n * 官公庁\n * 自治体\n * 学校\n * 医療法人\n * 部署\n * 事業部\n * 支店\n * 子会社\n * プロジェクト組織\n\n最初のモデルでは、会社も部署もOrganizationの一種として扱います。\n\n株式会社ABC a Organization\n営業本部 a Organization\n開発本部 a Organization\n\n\nOrganization同士には、階層関係を持たせます。\n\n株式会社ABC\nhasSubOrganization\n営業本部\n\n\n全体としては、次のようになります。\n\n\ngraph TD\n    A[株式会社ABC\nOrganization]\n    B[営業本部\nOrganization]\n    C[開発本部\nOrganization]\n    D[管理本部\nOrganization]\n\n    A -->|hasSubOrganization| B\n    A -->|hasSubOrganization| C\n    A -->|hasSubOrganization| D\n\n\n\nここで重要なのは、OrganizationとTenantを分離することです。\n\nOrganization\n＝ 現実世界の組織\n\nTenant\n＝ SaaS上の論理的な利用境界\n\n\n\n--------------------------------------------------------------------------------\n\n4. OrganizationとTenantの関係\nOrganizationとTenantの間には、利用関係を置きます。\n\nOrganization usesTenant Tenant\n\n\n逆方向から表現するなら、次のようになります。\n\nTenant servesOrganization Organization\n\n\nたとえば株式会社ABCがTenant ABCを使う場合は、次のように表現できます。\n\n株式会社ABC\n→ usesTenant\n→ Tenant ABC\n\n\n\ngraph LR\n    O[株式会社ABC\nOrganization]\n    T[Tenant ABC\nTenant]\n\n    O -->|usesTenant| T\n\n\n\nただし、OrganizationとTenantは1対1とは限りません。\n\n * 1つのOrganizationが複数Tenantを利用する\n\n株式会社ABC\n├── 国内事業Tenant\n└── 海外事業Tenant\n\n\n * 複数Organizationが1つのTenantを利用する\n\nABCグループTenant\n├── 株式会社ABC\n├── ABC販売株式会社\n└── ABCシステムズ株式会社\n\n\nしたがって、一般化すればOrganizationとTenantの関係は多対多ですが、初期実装では複雑さを避けるため、\n\n1つのTenantは1つの主Organizationに対応する\n\n\nという制約を置いても構いません。\n\n\n--------------------------------------------------------------------------------\n\n5. Workspaceを追加する\n次に、Workspaceを導入します。\n\nWorkspace\n＝ Tenantの内部に作られる共同作業の論理的な空間\n\n\nTenantとWorkspaceは、どちらも境界に見えるため混同されやすい概念ですが、役割を分けると次のようになります。\n\nTenant\n＝ 契約・請求・セキュリティ・データ分離の境界\n\nWorkspace\n＝ 日常的な共同作業や業務データ整理の境界\n\n\nたとえば、株式会社ABCのTenant内に、次のWorkspaceを作ることができます。\n\nTenant ABC\n├── 営業Workspace\n├── 開発Workspace\n└── 管理Workspace\n\n\n\ngraph TD\n    T[Tenant ABC]\n\n    W1[営業Workspace]\n    W2[開発Workspace]\n    W3[管理Workspace]\n\n    T -->|containsWorkspace| W1\n    T -->|containsWorkspace| W2\n    T -->|containsWorkspace| W3\n\n\n\nこの構造であれば、契約やSSO設定はTenantで共通化しながら、業務データをWorkspaceごとに整理できます。\n\n\n--------------------------------------------------------------------------------\n\n6. TenantとWorkspaceの違い\nTenantを分けると、多くの場合、次のような設定も分離されます。\n\n * 契約\n * 請求\n * SSO\n * 管理者\n * セキュリティポリシー\n * APIキー\n * 監査ログ\n * データ保持期間\n * データ保存地域\n\n一方、Workspaceは、そこまで強い境界ではありません。\n\n契約は1つ\n請求も1つ\nSSO設定も共通\nしかし業務データや作業場所は分けたい\n\n\nこのような要求にWorkspaceが向いています。\n\nたとえるなら、\n\nTenant\n＝ 建物全体\n\nWorkspace\n＝ 建物内の用途別エリア\n\n\nです。\n\nただし、このたとえも完全ではありません。Workspace間で一部のデータを共有するSaaSもあるためです。オントロジーのたとえ話は理解を助ける一方で、使いすぎると逆に概念を固定しすぎます。たとえは地図であって、現実そのものではありませんが、ややこしくなるので、いまのところはそういうものとして次に行きましょう！\n\n\n--------------------------------------------------------------------------------\n\n7. ResourceとWorkspace\nWorkspaceを導入すると、ResourceはWorkspaceに格納されるようになります。\n\nTenant containsWorkspace Workspace\nWorkspace containsResource Resource\n\n\n\ngraph LR\n    T[Tenant]\n    W[Workspace]\n    R[Resource]\n\n    T -->|containsWorkspace| W\n    W -->|containsResource| R\n\n\nResourceからTenantをたどる場合は、次の経路になります。\n\nResource\n→ belongsToWorkspace\n→ Workspace\n→ belongsToTenant\n→ Tenant\n\n\nたとえば、CRMであれば次のようになります。\n\n顧客(データ)「株式会社XYZ」\n→ 営業Workspaceに属する\n→ Tenant ABCに属する\n\n\n実際のデータベースでは、Resourceにworkspace_idとtenant_idの両方を持たせることがあります。\n\ncustomer\n├── customer_id\n├── tenant_id\n├── workspace_id\n├── name\n└── created_at\n\n\n概念上はworkspace_idからTenantを特定できますが、物理設計では次の理由でtenant_idを重複して保持することがあります。\n\n * Tenant条件をすべてのクエリに適用するため\n * データ漏えいを防ぐため\n * パーティションキーに利用するため\n * 検索性能を高めるため\n * 不整合を検知するため\n\nこれは、概念モデルと物理データモデルが同一ではない例です。オントロジーは「何を意味するか」を表し、データベースは「どう安全かつ高速に保存するか」を考えます。\n\n\n--------------------------------------------------------------------------------\n\n8. Teamを追加する\n次に、Teamを追加します。\n\nTeam\n＝ Tenant内で共同作業や権限付与に使われるメンバーの集合\n\n\nTeamの例は次のとおりです。\n\n * 営業Team\n * 開発Team\n * 管理者Team\n * セキュリティTeam\n * プロジェクトATeam\n * 外部監査Team\n\nTeamはOrganizationと似ていますが、同じものではありません。Organizationが現実世界の組織構造を表すのに対し、Teamは、SaaS内で人をまとめるための集合です。\n\nOrganization\n＝ 現実世界の組織\n\nTeam\n＝ SaaS上のメンバー集合\n\n\nたとえば、プロジェクトATeamには、複数の部署からメンバーが参加できます。\n\nプロジェクトATeam\n├── 営業本部の山田さん\n├── 開発本部の佐藤さん\n├── 管理本部の鈴木さん\n└── 外部委託先の高橋さん\n\n\nこのTeamは正式な組織図には存在しないかもしれませんが、SaaS上のアクセス制御では重要です。\n\n\n--------------------------------------------------------------------------------\n\n9. TeamにはUserではなくMembershipを入れる\nTeamのメンバーとして、Userを直接追加する設計もできます。\n\nTeam hasMember User\n\n\nしかし、B2B SaaS共通モデルとしては、MembershipをTeamに所属させる方が自然です。\n\nTeam hasMember Membership\n\n\n理由は、同じUserが複数Tenantに所属できるためです。\n\nUser: 山田太郎\n\nMembership 1\n├── Tenant A\n└── Role: 管理者\n\nMembership 2\n├── Tenant B\n└── Role: 閲覧者\n\n\nTenant AのTeamに所属させたいのは、単なる山田太郎というUserではありません。\n\n正確には、\n\n山田太郎のTenant AにおけるMembership\n\n\nを所属させたいのです。\n\nしたがって、次の関係を使います。\n\nMembership memberOfTeam Team\n\n\n\n--------------------------------------------------------------------------------\n\n10. WorkspaceとTeamの違い\nWorkspaceとTeamも混同されやすい概念で、違いは何をまとめるかにあります。\n\nWorkspace\n＝ 作業場所やデータをまとめる\n\nTeam\n＝ メンバーをまとめる\n\n\n営業Workspaceには、次のようなデータがあります。\n\n営業Workspace\n├── 顧客\n├── 商談\n├── 営業資料\n└── 活動履歴\n\n\n営業Teamには、人が所属します。\n\n営業Team\n├── 山田さんのMembership\n├── 佐藤さんのMembership\n└── 鈴木さんのMembership\n\n\nTeamがWorkspaceにアクセスします。\n\nTeam canAccessWorkspace Workspace\n\n\nつまり、\n\nTeam\n＝ 誰が\n\nWorkspace\n＝ どこで\n\nResource\n＝ 何を\n\n\nと考えることができます。\n\n\n--------------------------------------------------------------------------------\n\n11. Organization・Tenant・Workspace・Teamの整理\n概念意味主に含むものOrganization現実世界の組織法人、部署、支店TenantSaaS上の管理・分離境界契約、セキュリティ、設定Workspace\nTenant内の共同作業空間Resource、作業対象TeamTenant内のメンバー集合Membership簡単にまとめると、次のようになります。\n\nOrganization\n＝ 現実世界では、どの組織なのか\n\nTenant\n＝ SaaS上では、どこまでを1つの顧客環境として扱うのか\n\nWorkspace\n＝ Tenant内で、どこを作業場所として使うのか\n\nTeam\n＝ 誰と誰をひとまとまりにするのか\n\n\n\n--------------------------------------------------------------------------------\n\n12. Roleの適用範囲という問題\nWorkspaceを導入すると、前回のRoleモデルに新しい問題が生まれます。前回は、MembershipにRoleを付与しました。\n\nMembership assignedRole Role\n\n\nしかし、Roleがどの範囲で有効なのか分かりません。\n\nたとえば、山田太郎さんが次の権限を持っているとします。\n\nTenant ABCでは一般メンバー\n\n営業Workspaceでは編集者\n\n開発Workspaceでは閲覧者\n\n\nこの場合、単純な\n\nMembership hasRole Editor\n\n\nでは不十分で、どのWorkspaceでEditorなのかを表現する必要があります。\n\n\n--------------------------------------------------------------------------------\n\n13. RoleAssignmentを追加する\nRoleの付与そのものを表す概念として、RoleAssignmentを追加します。\n\nRoleAssignment\n＝ ある主体に、ある適用範囲で、あるRoleを付与した事実\n\n\nRoleAssignmentは、最低限次の3つを結びます。\n\nassignee\n＝ 誰に付与するか\n\nrole\n＝ どのRoleを付与するか\n\nscope\n＝ どの範囲で有効か\n\n\n営業Workspaceで山田太郎さんをEditorにする場合は、次のようになります。\n\nRoleAssignment\n├── assignee: 山田太郎のMembership\n├── role: Editor\n└── scope: 営業Workspace\n\n\nTenant全体の管理者にする場合は、次のようになります。\n\nRoleAssignment\n├── assignee: 山田太郎のMembership\n├── role: Tenant Administrator\n└── scope: Tenant ABC\n\n\n\ngraph TD\n    M[Membership]\n    RA[RoleAssignment]\n    R[Role]\n    S[Scope\nTenant or Workspace]\n\n    M -->|hasRoleAssignment| RA\n    RA -->|assignedRole| R\n    RA -->|appliesTo| S\n\n\nたとえばRoleAssignmentには、将来的に次の属性を持たせることができます。\n\n * 付与日時\n * 付与者\n * 有効期限\n * 付与理由\n * 手動付与か自動付与か\n * 一時的な権限か\n * 承認済みか\n\n関係に属性が必要になったら、その関係を独立した概念にする。これはオントロジー設計において、とてもよく使われる考え方のようです。（たぶん）\n\n\n--------------------------------------------------------------------------------\n\n14. 今回追加するクラス\n今回のオントロジーでは、前回のクラスに次のものを追加します。\n\nOrganization\nWorkspace\nTeam\nRoleAssignment\n\n\n前回から引き継ぐ主なクラスは次のとおりです。\n\nTenant\nUser\nMembership\nRole\nPermission\nSubscription\nPlan\nResource\n\n\n今回までの全体像は、次のようになります。\n\nOrganization\n└── usesTenant\n    └── Tenant\n        ├── Membership\n        │   └── User\n        │\n        ├── Workspace\n        │   └── Resource\n        │\n        ├── Team\n        │   └── Membership\n        │\n        ├── RoleAssignment\n        │   ├── Membership\n        │   ├── Role\n        │   │   └── Permission\n        │   └── Scope\n        │       ├── Tenant\n        │       └── Workspace\n        │\n        └── Subscription\n            └── Plan\n\n\n\n--------------------------------------------------------------------------------\n\n15. オントロジー定義のTurtle\nここからは、今回追加した概念をTurtleで定義します。\n\n最初は、できるだけ単純なOWL/RDFSモデルにします。\n\n@prefix saas: <https://example.com/ontology/saas#> .\n@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix owl:  <http://www.w3.org/2002/07/owl#> .\n@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .\n\nsaas:B2BSaaSOntology a owl:Ontology ;\n    rdfs:label \"B2B SaaS Core Ontology\"@en ,\n               \"B2B SaaS共通オントロジー\"@ja .\n\n#\n# Classes\n#\n\nsaas:Organization a owl:Class ;\n    rdfs:label \"Organization\"@en ,\n               \"組織\"@ja ;\n    rdfs:comment\n        \"現実世界に存在する法人、部署、支店、団体などを表す。\"@ja .\n\nsaas:Tenant a owl:Class ;\n    rdfs:label \"Tenant\"@en ,\n               \"テナント\"@ja ;\n    rdfs:comment\n        \"SaaS上で契約、管理、セキュリティ、データを分離する論理的な境界。\"@ja .\n\nsaas:Workspace a owl:Class ;\n    rdfs:label \"Workspace\"@en ,\n               \"ワークスペース\"@ja ;\n    rdfs:comment\n        \"Tenant内部に作られる共同作業の論理的な空間。\"@ja .\n\nsaas:Team a owl:Class ;\n    rdfs:label \"Team\"@en ,\n               \"チーム\"@ja ;\n    rdfs:comment\n        \"Tenant内で共同作業やアクセス制御に使用するMembershipの集合。\"@ja .\n\nsaas:User a owl:Class ;\n    rdfs:label \"User\"@en ,\n               \"ユーザー\"@ja .\n\nsaas:Membership a owl:Class ;\n    rdfs:label \"Membership\"@en ,\n               \"所属関係\"@ja ;\n    rdfs:comment\n        \"Userが特定のTenantに参加しているという関係を表す。\"@ja .\n\nsaas:RoleAssignment a owl:Class ;\n    rdfs:label \"Role Assignment\"@en ,\n               \"ロール割当て\"@ja ;\n    rdfs:comment\n        \"あるMembershipに、特定の適用範囲でRoleを付与した事実。\"@ja .\n\nsaas:Role a owl:Class ;\n    rdfs:label \"Role\"@en ,\n               \"ロール\"@ja .\n\nsaas:Permission a owl:Class ;\n    rdfs:label \"Permission\"@en ,\n               \"権限\"@ja .\n\nsaas:Resource a owl:Class ;\n    rdfs:label \"Resource\"@en ,\n               \"リソース\"@ja ;\n    rdfs:comment\n        \"SaaS上でTenantまたはWorkspaceが管理する業務オブジェクト。\"@ja .\n\nsaas:Subscription a owl:Class .\nsaas:Plan a owl:Class .\n\n#\n# Organization properties\n#\n\nsaas:hasSubOrganization a owl:ObjectProperty ;\n    rdfs:domain saas:Organization ;\n    rdfs:range saas:Organization ;\n    rdfs:label \"has sub-organization\"@en ,\n               \"下位組織を持つ\"@ja .\n\nsaas:parentOrganization a owl:ObjectProperty ;\n    owl:inverseOf saas:hasSubOrganization ;\n    rdfs:domain saas:Organization ;\n    rdfs:range saas:Organization ;\n    rdfs:label \"parent organization\"@en ,\n               \"上位組織\"@ja .\n\nsaas:usesTenant a owl:ObjectProperty ;\n    rdfs:domain saas:Organization ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"uses tenant\"@en ,\n               \"テナントを利用する\"@ja .\n\nsaas:servesOrganization a owl:ObjectProperty ;\n    owl:inverseOf saas:usesTenant ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Organization ;\n    rdfs:label \"serves organization\"@en ,\n               \"組織に提供される\"@ja .\n\n#\n# Workspace properties\n#\n\nsaas:containsWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"contains workspace\"@en ,\n               \"ワークスペースを含む\"@ja .\n\nsaas:belongsToTenant a owl:ObjectProperty ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"belongs to tenant\"@en ,\n               \"テナントに属する\"@ja .\n\nsaas:workspaceBelongsToTenant a owl:ObjectProperty ;\n    rdfs:subPropertyOf saas:belongsToTenant ;\n    owl:inverseOf saas:containsWorkspace ;\n    rdfs:domain saas:Workspace ;\n    rdfs:range saas:Tenant .\n\nsaas:containsResource a owl:ObjectProperty ;\n    rdfs:domain saas:Workspace ;\n    rdfs:range saas:Resource ;\n    rdfs:label \"contains resource\"@en ,\n               \"リソースを含む\"@ja .\n\nsaas:belongsToWorkspace a owl:ObjectProperty ;\n    owl:inverseOf saas:containsResource ;\n    rdfs:domain saas:Resource ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"belongs to workspace\"@en ,\n               \"ワークスペースに属する\"@ja .\n\n#\n# Membership and Team properties\n#\n\nsaas:belongsToUser a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:User .\n\nsaas:membershipBelongsToTenant a owl:ObjectProperty ;\n    rdfs:subPropertyOf saas:belongsToTenant ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:Tenant .\n\nsaas:containsTeam a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Team ;\n    rdfs:label \"contains team\"@en ,\n               \"チームを含む\"@ja .\n\nsaas:teamBelongsToTenant a owl:ObjectProperty ;\n    rdfs:subPropertyOf saas:belongsToTenant ;\n    owl:inverseOf saas:containsTeam ;\n    rdfs:domain saas:Team ;\n    rdfs:range saas:Tenant .\n\nsaas:memberOfTeam a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:Team ;\n    rdfs:label \"member of team\"@en ,\n               \"チームに所属する\"@ja .\n\nsaas:hasTeamMember a owl:ObjectProperty ;\n    owl:inverseOf saas:memberOfTeam ;\n    rdfs:domain saas:Team ;\n    rdfs:range saas:Membership .\n\nsaas:canAccessWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:Team ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"can access workspace\"@en ,\n               \"ワークスペースにアクセスできる\"@ja .\n\n#\n# Role assignment properties\n#\n\nsaas:hasRoleAssignment a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:RoleAssignment .\n\nsaas:assignedTo a owl:ObjectProperty ;\n    owl:inverseOf saas:hasRoleAssignment ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Membership ;\n    rdfs:label \"assigned to\"@en ,\n               \"割当て対象\"@ja .\n\nsaas:assignedRole a owl:ObjectProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Role ;\n    rdfs:label \"assigned role\"@en ,\n               \"割り当てるロール\"@ja .\n\nsaas:appliesToTenant a owl:ObjectProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"applies to tenant\"@en ,\n               \"テナントに適用される\"@ja .\n\nsaas:appliesToWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"applies to workspace\"@en ,\n               \"ワークスペースに適用される\"@ja .\n\nsaas:grantsPermission a owl:ObjectProperty ;\n    rdfs:domain saas:Role ;\n    rdfs:range saas:Permission .\n\n#\n# Datatype properties\n#\n\nsaas:name a owl:DatatypeProperty ;\n    rdfs:range xsd:string .\n\nsaas:status a owl:DatatypeProperty ;\n    rdfs:range xsd:string .\n\nsaas:createdAt a owl:DatatypeProperty ;\n    rdfs:range xsd:dateTime .\n\nsaas:assignedAt a owl:DatatypeProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range xsd:dateTime .\n\nsaas:expiresAt a owl:DatatypeProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range xsd:dateTime .\n\n\n\n\n第一回に比べて、いきなりでっかくなりましたね。。。\n\n\n--------------------------------------------------------------------------------\n\n16. belongsToTenantを共通化する理由\n上のTurtleでは、次の3つのプロパティを用意しました。\n\nworkspaceBelongsToTenant\nmembershipBelongsToTenant\nteamBelongsToTenant\n\n\nこれらは、すべて上位プロパティであるbelongsToTenantのサブプロパティです。\n\nsaas:workspaceBelongsToTenant\n    rdfs:subPropertyOf saas:belongsToTenant .\n\nsaas:membershipBelongsToTenant\n    rdfs:subPropertyOf saas:belongsToTenant .\n\nsaas:teamBelongsToTenant\n    rdfs:subPropertyOf saas:belongsToTenant .\n\n\nこれにより、Workspace、Membership、Teamをまとめて、\n\n?entity saas:belongsToTenant ?tenant .\n\n\nという形で検索できます。\n\n一方で、詳細を知りたい場合は、\n\n?workspace saas:workspaceBelongsToTenant ?tenant .\n\n\nのように、具体的な関係を指定できます。\n\nこのような上位プロパティは、検索や推論を簡単にするために役立ちます。\n\n\n--------------------------------------------------------------------------------\n\n17. 株式会社ABCの具体的なインスタンス\nここからは、実際のデータをTurtleで表現します。\n\n17.1 組織・Tenant・Workspace・Team\n@prefix saas: <https://example.com/ontology/saas#> .\n@prefix ex:   <https://example.com/data/> .\n@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .\n\n#\n# Organizations\n#\n\nex:organization-abc a saas:Organization ;\n    saas:name \"株式会社ABC\" ;\n    saas:hasSubOrganization\n        ex:organization-abc-sales,\n        ex:organization-abc-development,\n        ex:organization-abc-administration ;\n    saas:usesTenant ex:tenant-abc .\n\nex:organization-abc-sales a saas:Organization ;\n    saas:name \"営業本部\" ;\n    saas:parentOrganization ex:organization-abc .\n\nex:organization-abc-development a saas:Organization ;\n    saas:name \"開発本部\" ;\n    saas:parentOrganization ex:organization-abc .\n\nex:organization-abc-administration a saas:Organization ;\n    saas:name \"管理本部\" ;\n    saas:parentOrganization ex:organization-abc .\n\n#\n# Tenant\n#\n\nex:tenant-abc a saas:Tenant ;\n    saas:name \"株式会社ABCテナント\" ;\n    saas:status \"active\" ;\n    saas:containsWorkspace\n        ex:workspace-sales,\n        ex:workspace-development,\n        ex:workspace-administration ;\n    saas:containsTeam\n        ex:team-sales,\n        ex:team-development,\n        ex:team-project-alpha .\n\n#\n# Workspaces\n#\n\nex:workspace-sales a saas:Workspace ;\n    saas:name \"営業Workspace\" ;\n    saas:workspaceBelongsToTenant ex:tenant-abc .\n\nex:workspace-development a saas:Workspace ;\n    saas:name \"開発Workspace\" ;\n    saas:workspaceBelongsToTenant ex:tenant-abc .\n\nex:workspace-administration a saas:Workspace ;\n    saas:name \"管理Workspace\" ;\n    saas:workspaceBelongsToTenant ex:tenant-abc .\n\n#\n# Teams\n#\n\nex:team-sales a saas:Team ;\n    saas:name \"営業Team\" ;\n    saas:teamBelongsToTenant ex:tenant-abc ;\n    saas:canAccessWorkspace ex:workspace-sales .\n\nex:team-development a saas:Team ;\n    saas:name \"開発Team\" ;\n    saas:teamBelongsToTenant ex:tenant-abc ;\n    saas:canAccessWorkspace ex:workspace-development .\n\nex:team-project-alpha a saas:Team ;\n    saas:name \"プロジェクトAlpha Team\" ;\n    saas:teamBelongsToTenant ex:tenant-abc ;\n    saas:canAccessWorkspace\n        ex:workspace-sales,\n        ex:workspace-development .\n\n\n\n\n\n--------------------------------------------------------------------------------\n\n18. UserとMembershipの具体例\n#\n# Users\n#\n\nex:user-yamada a saas:User ;\n    saas:name \"山田太郎\" ;\n    saas:status \"active\" .\n\nex:user-sato a saas:User ;\n    saas:name \"佐藤花子\" ;\n    saas:status \"active\" .\n\nex:user-suzuki a saas:User ;\n    saas:name \"鈴木一郎\" ;\n    saas:status \"active\" .\n\n#\n# Memberships\n#\n\nex:membership-yamada-abc a saas:Membership ;\n    saas:belongsToUser ex:user-yamada ;\n    saas:membershipBelongsToTenant ex:tenant-abc ;\n    saas:status \"active\" ;\n    saas:memberOfTeam\n        ex:team-sales,\n        ex:team-project-alpha .\n\nex:membership-sato-abc a saas:Membership ;\n    saas:belongsToUser ex:user-sato ;\n    saas:membershipBelongsToTenant ex:tenant-abc ;\n    saas:status \"active\" ;\n    saas:memberOfTeam\n        ex:team-development,\n        ex:team-project-alpha .\n\nex:membership-suzuki-abc a saas:Membership ;\n    saas:belongsToUser ex:user-suzuki ;\n    saas:membershipBelongsToTenant ex:tenant-abc ;\n    saas:status \"active\" ;\n    saas:memberOfTeam ex:team-sales .\n\n\nここで、UserではなくMembershipがTeamに所属していることが重要です。\n\n山田太郎という人\n\n\nではなく、\n\n山田太郎のTenant ABCにおける所属\n\n\nが営業Teamに参加しています。\n\n\n--------------------------------------------------------------------------------\n\n19. RoleとPermissionの具体例\n#\n# Permissions\n#\n\nex:permission-tenant-manage a saas:Permission ;\n    saas:name \"tenant.manage\" .\n\nex:permission-member-invite a saas:Permission ;\n    saas:name \"member.invite\" .\n\nex:permission-workspace-read a saas:Permission ;\n    saas:name \"workspace.read\" .\n\nex:permission-resource-read a saas:Permission ;\n    saas:name \"resource.read\" .\n\nex:permission-resource-create a saas:Permission ;\n    saas:name \"resource.create\" .\n\nex:permission-resource-update a saas:Permission ;\n    saas:name \"resource.update\" .\n\nex:permission-resource-delete a saas:Permission ;\n    saas:name \"resource.delete\" .\n\n#\n# Roles\n#\n\nex:role-tenant-admin a saas:Role ;\n    saas:name \"Tenant Administrator\" ;\n    saas:grantsPermission\n        ex:permission-tenant-manage,\n        ex:permission-member-invite,\n        ex:permission-workspace-read,\n        ex:permission-resource-read,\n        ex:permission-resource-create,\n        ex:permission-resource-update,\n        ex:permission-resource-delete .\n\nex:role-workspace-editor a saas:Role ;\n    saas:name \"Workspace Editor\" ;\n    saas:grantsPermission\n        ex:permission-workspace-read,\n        ex:permission-resource-read,\n        ex:permission-resource-create,\n        ex:permission-resource-update .\n\nex:role-workspace-viewer a saas:Role ;\n    saas:name \"Workspace Viewer\" ;\n    saas:grantsPermission\n        ex:permission-workspace-read,\n        ex:permission-resource-read .\n\n\n\n--------------------------------------------------------------------------------\n\n20. RoleAssignmentの具体例\n山田太郎さんには、次の権限を付与します。\n\nTenant ABC全体\n→ Tenant Administrator\n\n営業Workspace\n→ Workspace Editor\n\n開発Workspace\n→ Workspace Viewer\n\n\nこれをTurtleで表現すると、次のようになります。\n\n#\n# Tenant-level role assignment\n#\n\nex:role-assignment-yamada-tenant-admin a saas:RoleAssignment ;\n    saas:assignedTo ex:membership-yamada-abc ;\n    saas:assignedRole ex:role-tenant-admin ;\n    saas:appliesToTenant ex:tenant-abc ;\n    saas:assignedAt \"2026-08-01T09:00:00+09:00\"^^xsd:dateTime .\n\n#\n# Workspace-level role assignments\n#\n\nex:role-assignment-yamada-sales-editor a saas:RoleAssignment ;\n    saas:assignedTo ex:membership-yamada-abc ;\n    saas:assignedRole ex:role-workspace-editor ;\n    saas:appliesToWorkspace ex:workspace-sales ;\n    saas:assignedAt \"2026-08-01T09:05:00+09:00\"^^xsd:dateTime .\n\nex:role-assignment-yamada-development-viewer a saas:RoleAssignment ;\n    saas:assignedTo ex:membership-yamada-abc ;\n    saas:assignedRole ex:role-workspace-viewer ;\n    saas:appliesToWorkspace ex:workspace-development ;\n    saas:assignedAt \"2026-08-01T09:10:00+09:00\"^^xsd:dateTime .\n\nex:role-assignment-sato-development-editor a saas:RoleAssignment ;\n    saas:assignedTo ex:membership-sato-abc ;\n    saas:assignedRole ex:role-workspace-editor ;\n    saas:appliesToWorkspace ex:workspace-development ;\n    saas:assignedAt \"2026-08-01T10:00:00+09:00\"^^xsd:dateTime .\n\n\n\n--------------------------------------------------------------------------------\n\n21. Resourceの具体例\n営業Workspaceには顧客と商談があり、開発Workspaceには設計書があるとします。\n\nex:customer-xyz a saas:Resource ;\n    saas:name \"株式会社XYZ\" ;\n    saas:belongsToWorkspace ex:workspace-sales .\n\nex:deal-core-system-renewal a saas:Resource ;\n    saas:name \"基幹システム刷新案件\" ;\n    saas:belongsToWorkspace ex:workspace-sales .\n\nex:document-system-architecture a saas:Resource ;\n    saas:name \"システム構成設計書\" ;\n    saas:belongsToWorkspace ex:workspace-development .\n\n\nこのデータから、\n\n株式会社XYZ\n→ 営業Workspace\n→ Tenant ABC\n\n\nという関係をたどれます。\n\n\n--------------------------------------------------------------------------------\n\n22. 全インスタンスの関係図\n\ngraph TD\n    O[株式会社ABC\nOrganization]\n    T[Tenant ABC]\n\n    W1[営業Workspace]\n    W2[開発Workspace]\n\n    TM1[営業Team]\n    TM2[開発Team]\n    TM3[プロジェクトAlpha Team]\n\n    U1[山田太郎\nUser]\n    U2[佐藤花子\nUser]\n\n    M1[山田Membership]\n    M2[佐藤Membership]\n\n    RA1[Tenant Admin Assignment]\n    RA2[Sales Editor Assignment]\n    RA3[Development Viewer Assignment]\n\n    R1[Tenant Administrator]\n    R2[Workspace Editor]\n    R3[Workspace Viewer]\n\n    C[株式会社XYZ\nResource]\n    D[システム構成設計書\nResource]\n\n    O -->|usesTenant| T\n\n    T -->|containsWorkspace| W1\n    T -->|containsWorkspace| W2\n\n    T -->|containsTeam| TM1\n    T -->|containsTeam| TM2\n    T -->|containsTeam| TM3\n\n    U1 --> M1\n    U2 --> M2\n\n    M1 --> T\n    M2 --> T\n\n    M1 --> TM1\n    M1 --> TM3\n    M2 --> TM2\n    M2 --> TM3\n\n    TM1 -->|canAccess| W1\n    TM2 -->|canAccess| W2\n    TM3 -->|canAccess| W1\n    TM3 -->|canAccess| W2\n\n    M1 --> RA1\n    M1 --> RA2\n    M1 --> RA3\n\n    RA1 --> R1\n    RA1 --> T\n\n    RA2 --> R2\n    RA2 --> W1\n\n    RA3 --> R3\n    RA3 --> W2\n\n    W1 --> C\n    W2 --> D\n\n\n\n\n\n--------------------------------------------------------------------------------\n\n23. SPARQLで問い合わせる\nオントロジーができたら、どのような質問に答えられるかを確認します。\n\n23.1 Tenantが持つWorkspaceを取得する\nPREFIX saas: <https://example.com/ontology/saas#>\n\nSELECT ?workspace ?workspaceName\nWHERE {\n    <https://example.com/data/tenant-abc>\n        saas:containsWorkspace ?workspace .\n\n    ?workspace saas:name ?workspaceName .\n}\n\n\n結果のイメージは次のとおりです。\n\n営業Workspace\n開発Workspace\n管理Workspace\n\n\n\n--------------------------------------------------------------------------------\n\n23.2 山田太郎が所属するTeamを取得する\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nSELECT ?team ?teamName\nWHERE {\n    ex:membership-yamada-abc\n        saas:memberOfTeam ?team .\n\n    ?team saas:name ?teamName .\n}\n\n\n結果は次のようになります。\n\n営業Team\nプロジェクトAlpha Team\n\n\n\n--------------------------------------------------------------------------------\n\n23.3 山田太郎がTeam経由でアクセスできるWorkspace\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nSELECT DISTINCT ?workspace ?workspaceName\nWHERE {\n    ex:membership-yamada-abc\n        saas:memberOfTeam ?team .\n\n    ?team saas:canAccessWorkspace ?workspace .\n\n    ?workspace saas:name ?workspaceName .\n}\n\n\n結果のイメージは次のとおりです。\n\n営業Workspace\n開発Workspace\n\n\n営業Teamから営業Workspaceへアクセスでき、プロジェクトAlpha Teamから営業・開発の両Workspaceへアクセスできるためです。\n\n\n--------------------------------------------------------------------------------\n\n23.4 山田太郎が営業Workspaceで持つRole\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nSELECT ?role ?roleName\nWHERE {\n    ?assignment a saas:RoleAssignment ;\n        saas:assignedTo ex:membership-yamada-abc ;\n        saas:appliesToWorkspace ex:workspace-sales ;\n        saas:assignedRole ?role .\n\n    ?role saas:name ?roleName .\n}\n\n\n結果は次のようになります。\n\nWorkspace Editor\n\n\n\n--------------------------------------------------------------------------------\n\n23.5 山田太郎が営業Workspaceで利用できるPermission\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nSELECT DISTINCT ?permission ?permissionName\nWHERE {\n    ?assignment a saas:RoleAssignment ;\n        saas:assignedTo ex:membership-yamada-abc ;\n        saas:appliesToWorkspace ex:workspace-sales ;\n        saas:assignedRole ?role .\n\n    ?role saas:grantsPermission ?permission .\n\n    ?permission saas:name ?permissionName .\n}\nORDER BY ?permissionName\n\n\n結果のイメージは次のとおりです。\n\nresource.create\nresource.read\nresource.update\nworkspace.read\n\n\n\n--------------------------------------------------------------------------------\n\n23.6 Resourceが属するTenantを取得する\nResourceは直接Tenantを参照せず、Workspaceを経由しています。\n\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nSELECT ?tenant ?tenantName\nWHERE {\n    ex:customer-xyz\n        saas:belongsToWorkspace ?workspace .\n\n    ?workspace\n        saas:workspaceBelongsToTenant ?tenant .\n\n    ?tenant saas:name ?tenantName .\n}\n\n\n結果は次のようになります。\n\n株式会社ABCテナント\n\n\n\n--------------------------------------------------------------------------------\n\n24. プロパティチェーンによる推論\nOWLには、複数の関係をつなぎ、新しい関係を推論するプロパティチェーンがあります。\n\nたとえば、\n\nResource belongsToWorkspace Workspace\nWorkspace workspaceBelongsToTenant Tenant\n\n\nという2つの関係から、\n\nResource resourceBelongsToTenant Tenant\n\n\nを推論できます。\n\nTurtleでは、次のように定義できます。\n\nsaas:resourceBelongsToTenant a owl:ObjectProperty ;\n    rdfs:domain saas:Resource ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"resource belongs to tenant\"@en ,\n               \"リソースがテナントに属する\"@ja ;\n    owl:propertyChainAxiom (\n        saas:belongsToWorkspace\n        saas:workspaceBelongsToTenant\n    ) .\n\n\nこれにより、推論エンジンが動作する環境では、明示的に次を書かなくても、\n\nex:customer-xyz\n    saas:resourceBelongsToTenant ex:tenant-abc .\n\n\nという関係を導出できます。\n\n同じように、Membershipが所属するTeamから、そのMembershipがアクセス可能なWorkspaceを推論することもできます。\n\nsaas:membershipCanAccessWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:Workspace ;\n    owl:propertyChainAxiom (\n        saas:memberOfTeam\n        saas:canAccessWorkspace\n    ) .\n\n\nすると、\n\nMembership\n→ memberOfTeam\n→ Team\n→ canAccessWorkspace\n→ Workspace\n\n\nから、\n\nMembership\n→ membershipCanAccessWorkspace\n→ Workspace\n\n\nを導出できます。\n\n\n--------------------------------------------------------------------------------\n\n25. 推論できることと、許可してよいことは違う\nここで注意が必要です。オントロジー上、\n\n山田太郎のMembership\n→ Team経由で営業Workspaceにアクセス可能\n\n\nと推論できたとしても、それだけで実際のアクセスを許可してよいとは限りません。\n\n実際の認可では、さらに次の条件が必要になることがあります。\n\n * Membershipが有効である\n * Teamが有効である\n * Workspaceが停止されていない\n * RoleAssignmentの有効期限が切れていない\n * 必要なPermissionを持っている\n * 契約上その機能が利用可能である\n * Resource自体に追加制限がない\n * IPアドレス制限に違反していない\n * MFAを完了している\n\nつまり、\n\n関係が存在する\n\n\nことと、\n\nその操作を今この瞬間に許可する\n\n\nことは別です。\n\nオントロジーは認可判断の材料を整理できますが、認可エンジンそのものとは限りません。この境界を曖昧にすると、便利そうな知識グラフが、いつの間にか危険なアクセス制御システムになってしまいます。\n\n\n--------------------------------------------------------------------------------\n\n26. SHACLによる最低限の検証\nOWLは意味や推論を表現するのに適していますが、入力データの妥当性検証にはSHACLが向いています。\n\nたとえば、Workspaceには必ず1つのTenantが必要だとします。\n\n@prefix sh:   <http://www.w3.org/ns/shacl#> .\n@prefix saas: <https://example.com/ontology/saas#> .\n@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .\n\nsaas:WorkspaceShape a sh:NodeShape ;\n    sh:targetClass saas:Workspace ;\n\n    sh:property [\n        sh:path saas:workspaceBelongsToTenant ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Tenant ;\n        sh:message\n            \"Workspaceは必ず1つのTenantに所属しなければなりません。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:name ;\n        sh:minCount 1 ;\n        sh:datatype xsd:string ;\n        sh:message\n            \"Workspaceには名称が必要です。\"@ja\n    ] .\n\n\nMembershipにも、UserとTenantがそれぞれ1つ必要です。\n\nsaas:MembershipShape a sh:NodeShape ;\n    sh:targetClass saas:Membership ;\n\n    sh:property [\n        sh:path saas:belongsToUser ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:User ;\n        sh:message\n            \"Membershipは必ず1つのUserに属する必要があります。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:membershipBelongsToTenant ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Tenant ;\n        sh:message\n            \"Membershipは必ず1つのTenantに属する必要があります。\"@ja\n    ] .\n\n\nRoleAssignmentには、割当て対象とRoleが必要です。\n\nsaas:RoleAssignmentShape a sh:NodeShape ;\n    sh:targetClass saas:RoleAssignment ;\n\n    sh:property [\n        sh:path saas:assignedTo ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Membership\n    ] ;\n\n    sh:property [\n        sh:path saas:assignedRole ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Role\n    ] ;\n\n    sh:xone (\n        [\n            sh:property [\n                sh:path saas:appliesToTenant ;\n                sh:minCount 1 ;\n                sh:maxCount 1\n            ] ;\n            sh:property [\n                sh:path saas:appliesToWorkspace ;\n                sh:maxCount 0\n            ]\n        ]\n        [\n            sh:property [\n                sh:path saas:appliesToWorkspace ;\n                sh:minCount 1 ;\n                sh:maxCount 1\n            ] ;\n            sh:property [\n                sh:path saas:appliesToTenant ;\n                sh:maxCount 0\n            ]\n        ]\n    ) .\n\n\nこのShapeでは、RoleAssignmentの適用先を、\n\nTenantまたはWorkspaceのどちらか一方\n\n\nに限定しています。\n\n両方を同時に指定したり、どちらも指定しなかったりすると、検証エラーになります。\n\n\n--------------------------------------------------------------------------------\n\n27. TeamとWorkspaceが同じTenantに属するかを検証する\nTeamからWorkspaceへのアクセスを設定する場合、両者が同じTenantに所属していることを確認したくなります。たとえば、Tenant\nAのTeamが、誤ってTenant BのWorkspaceへアクセスできてはいけません。\n\nTenant A\n└── Team A\n\nTenant B\n└── Workspace B\n\nTeam A canAccess Workspace B\n\n\nこれは、典型的なクロステナント不整合であり、単純なRDFSだけでは十分に検出できません。SHACL-SPARQLを使うと、次のような制約を書けます。\n\nsaas:TeamWorkspaceTenantConsistencyShape\n    a sh:NodeShape ;\n    sh:targetClass saas:Team ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"TeamがアクセスするWorkspaceは、Teamと同じTenantに所属する必要があります。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas: <https://example.com/ontology/saas#>\n\n            SELECT $this\n            WHERE {\n                $this\n                    saas:teamBelongsToTenant ?teamTenant ;\n                    saas:canAccessWorkspace ?workspace .\n\n                ?workspace\n                    saas:workspaceBelongsToTenant ?workspaceTenant .\n\n                FILTER (?teamTenant != ?workspaceTenant)\n            }\n        \"\"\"\n    ] .\n\n\nこの制約は、B2B\nSaaSでは非常に重要です。マルチテナントシステムで最も避けるべき問題の一つが、他TenantのデータやWorkspaceを誤って参照することだからです。\n\n\n--------------------------------------------------------------------------------\n\n28. MembershipとTeamのTenant整合性\nMembershipが所属するTeamも、同じTenantに属している必要があります。\n\nsaas:MembershipTeamTenantConsistencyShape\n    a sh:NodeShape ;\n    sh:targetClass saas:Membership ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"Membershipが所属するTeamは、Membershipと同じTenantに属する必要があります。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas: <https://example.com/ontology/saas#>\n\n            SELECT $this\n            WHERE {\n                $this\n                    saas:membershipBelongsToTenant ?membershipTenant ;\n                    saas:memberOfTeam ?team .\n\n                ?team\n                    saas:teamBelongsToTenant ?teamTenant .\n\n                FILTER (?membershipTenant != ?teamTenant)\n            }\n        \"\"\"\n    ] .\n\n\nこのようなTenant整合性制約は、今後追加するほぼすべての概念で必要になります。\n\n * Membership\n * Team\n * Workspace\n * Resource\n * RoleAssignment\n * API Client\n * Audit Event\n * Usage Record\n * Entitlement\n\nB2B SaaSのオントロジーでは、各概念の意味だけでなく、\n\nそのインスタンスがどのTenant文脈に属するか\n\n\nを常に意識する必要があります。\n\n\n--------------------------------------------------------------------------------\n\n29. 今回の最小関係一覧\n主語関係目的語意味OrganizationhasSubOrganizationOrganization下位組織を持つOrganizationusesTenant\nTenant組織がTenantを利用するTenantcontainsWorkspaceWorkspaceTenantがWorkspaceを含むWorkspace\nworkspaceBelongsToTenantTenantWorkspaceの所属TenantTenantcontainsTeamTeam\nTenantがTeamを含むTeamteamBelongsToTenantTenantTeamの所属TenantMembershipmemberOfTeam\nTeamMembershipがTeamに参加するTeamcanAccessWorkspaceWorkspaceTeamがWorkspaceにアクセスする\nWorkspacecontainsResourceResourceWorkspaceがResourceを含むResourcebelongsToWorkspace\nWorkspaceResourceの所属WorkspaceMembershiphasRoleAssignmentRoleAssignment\nMembershipにRole付与があるRoleAssignmentassignedRoleRole付与されるRoleRoleAssignment\nappliesToTenantTenantRoleのTenant適用範囲RoleAssignmentappliesToWorkspaceWorkspace\nRoleのWorkspace適用範囲RolegrantsPermissionPermissionRoleがPermissionを付与する\n最小関係なのに、すごい量になっちゃいましたね。。。\n\n\n--------------------------------------------------------------------------------\n\n30. 今回のモデルで答えられる質問\nOrganization、Workspace、Team、RoleAssignmentを追加したことで、次のような質問に答えられます。\n\nOrganizationに関する質問\n株式会社ABCは、どのTenantを利用しているか。\n\nこのTenantを利用するOrganizationはどれか。\n\n営業本部の上位Organizationは何か。\n\n株式会社ABCの下位Organizationは何か。\n\n\nWorkspaceに関する質問\nTenant ABCには、どのWorkspaceがあるか。\n\n営業Workspaceには、どのResourceがあるか。\n\nこのResourceは、最終的にどのTenantに属するか。\n\n\nTeamに関する質問\n山田太郎は、どのTeamに参加しているか。\n\nプロジェクトAlpha Teamには、誰が所属しているか。\n\nこのTeamは、どのWorkspaceへアクセスできるか。\n\n\nRoleに関する質問\n山田太郎は、営業WorkspaceでどのRoleを持つか。\n\n山田太郎は、開発WorkspaceでどのPermissionを持つか。\n\nTenant全体の管理者は誰か。\n\nこのRoleAssignmentは、いつ付与されたか。\n\n\nこの、「質問に答えられるようになる」というところが、オントロジーのすごいところですね！\n\n\n--------------------------------------------------------------------------------\n\n31. 今回のモデルのまとめ\n今回の内容を最も単純にまとめると、次のようになります。\n\nOrganizationは、現実世界の組織である。\n\nTenantは、SaaS上の契約・管理・分離境界である。\n\nWorkspaceは、Tenant内の共同作業空間である。\n\nTeamは、Tenant内のMembershipの集合である。\n\nResourceは、Workspaceに属する。\n\nRoleAssignmentは、誰に、どのRoleを、\nどの範囲で付与したかを表す。\n\n\n図にすると、次の形です。\n\n\ngraph LR\n    Organization --> Tenant\n    Tenant --> Workspace\n    Workspace --> Resource\n\n    User --> Membership\n    Membership --> Tenant\n    Membership --> Team\n    Team --> Workspace\n\n    Membership --> RoleAssignment\n    RoleAssignment --> Role\n    RoleAssignment --> Tenant\n    RoleAssignment --> Workspace\n    Role --> Permission\n\n\n\n\n--------------------------------------------------------------------------------\n\n次回予告\nここまでで、\n\n誰がTenantに所属しているか\n\n誰がどのWorkspaceへアクセスできるか\n\n誰がどのRoleとPermissionを持つか\n\n\nを表現できるようになりました。\n\nしかし、まだ別の問題が残っています。\n\nたとえば、山田太郎さんがanalytics.execute\nというPermissionを持っているとしても、契約しているPlanがAnalytics機能を含んでいなければ、その機能は使えません。\n\nPermissionがある\nしかし\n契約上、その機能が提供されていない\n\n\n反対に、Tenantの契約上は機能が提供されていても、山田太郎さんにPermissionがなければ利用できません。\n\n契約上は利用可能\nしかし\nそのUserには操作権限がない\n\n\nここには、異なる2種類の「できる」があります。\n\nPermission\n＝ そのUserが操作してよいか\n\nEntitlement\n＝ そのTenantに機能が提供されているか\n\n\n次回は、次の概念を追加します。\n\nFeature\nEntitlement\nLimit\nQuota\nUsage\n\n\n第3回テーマは、\n\n権限があっても使えない？ FeatureとEntitlementで「契約上使える機能」を表現する\n\nにしようかなと思っております。\n\nRoleとPermissionが人を中心とした認可を表すのに対し、FeatureとEntitlementはTenantを中心とした提供条件を表します。この2つを分けることで、料金プラン、オプション機能、利用上限、無料トライアル、β機能、個別契約をきれいに表現できるようになります。\n\nいきなり第2回から、モリモリの内容ですが、ぜひこの後も一緒に勉強していきましょう！\n\nまだまだ、第10回くらいまで続く予定ですので、お楽しみに！\n\nアディオス！","html":"<h1></h1><p>こんにちは！ 第1回目はいかがでしたでしょうか？！難しかったですかね？</p><p>だとしても、第2回を読もうとしていただいて、ありがとうございます！</p><p>第2回は、さらにモリモリの内容になっちゃいましたが、テナントをベースとしたさらなる深掘りと、オントロジーの活用例の第一歩として実際にクエリをしてみたいと思います！</p><p>クエリには、「SPARQL」というクエリ言語を使います。突然登場して混乱するかもなので、「SPARQL」を初めて聞いたという方は、</p><ul><li><a href=\"https://tech.anti-pattern.co.jp/whatisspaeql/\">SPARQLって結局何なの？実際にRDFデータに問い合わせしてみて勉強しよう！</a></li></ul><p>という別のブログを書いておりますので、まずこちらをざっと眺めてみてください！</p><p>そしてさらに！「SHACL」というものも登場してきます。これは、ざっくり言うと「RDFデータが期待する形になっているかを検証するための言語」なのですが、こちらについても</p><ul><li><a href=\"https://tech.anti-pattern.co.jp/whatisshacl/\">SHACLって結局何なの？実際にデータを検証してみて理解する</a></li></ul><p>を眺めてみてください！</p><p>では、さっそく第1回目の復習も兼ねて、テナントについて改めて考えてみたいと思います。</p><h1 id=\"0-tenant%E3%81%AF%E4%BC%9A%E7%A4%BE%E3%81%AA%E3%81%AE%E3%81%8B%EF%BC%9F\">0. Tenantは会社なのか？</h1><h2 id=\"organization%E3%83%BBworkspace%E3%83%BBteam%E3%82%92%E5%88%86%E3%81%91%E3%81%A6%E8%80%83%E3%81%88%E3%82%8B\">Organization・Workspace・Teamを分けて考える</h2><p>前回は、B2B SaaSの最小構造として、次のモデルを作りました。</p><pre><code class=\"language-text\">Tenant\n├── Membership\n│   ├── User\n│   └── Role\n│       └── Permission\n│\n├── Subscription\n│   └── Plan\n│\n└── Resource\n</code></pre><p>そして、Tenantを次のように定義しました。</p><pre><code class=\"language-text\">Tenant\n＝ SaaS上でユーザー、権限、契約、データを分離する論理的な境界\n</code></pre><p>この定義は、B2B SaaSを考えるうえで非常に重要ですが、実際のSaaSを設計し始めると、ほぼ確実に次の疑問が出てきます。</p><pre><code class=\"language-text\">Tenantは会社なのか？\n</code></pre><p>最初は多くの場合、Tenantを顧客企業と同じものとして扱います。</p><pre><code class=\"language-text\">株式会社ABC\n＝ Tenant ABC\n</code></pre><p>小規模なSaaSであれば、これでも十分ですが、顧客企業が大きくなったり、複数部署や子会社で利用したりすると、会社とTenantを同一視することが難しくなります。</p><hr><h1 id=\"1-%E4%BC%9A%E7%A4%BE%E3%81%A8tenant%E3%81%8C%E4%B8%80%E8%87%B4%E3%81%97%E3%81%AA%E3%81%84%E4%BE%8B\">1. 会社とTenantが一致しない例</h1><p>株式会社ABCという会社があるとします。</p><pre><code class=\"language-text\">株式会社ABC\n├── 営業本部\n│   ├── 東日本営業部\n│   └── 西日本営業部\n├── 開発本部\n│   ├── 製品開発部\n│   └── 基盤開発部\n└── 管理本部\n    ├── 経理部\n    └── 人事部\n</code></pre><p>この会社が、あるプロジェクト管理SaaSを導入したとします。会社全体で1つの利用環境を使うのであれば、構造は単純です。</p><pre><code class=\"language-text\">Tenant ABC\n</code></pre><p>しかし、次のような要求が発生するかもしれません。</p><ul><li>営業部と開発部でデータを分離したい</li><li>子会社ごとに管理者を分けたい</li><li>部門ごとに利用料金を集計したい</li><li>外部委託先には一部のプロジェクトだけ見せたい</li><li>海外法人と国内法人でデータ保存地域を分けたい</li><li>会社全体では契約を一本化したい</li></ul><p>ここで、すべての部署を別Tenantにすると、今度は会社全体としての管理が分断されてしまいます。</p><pre><code class=\"language-text\">営業本部Tenant\n開発本部Tenant\n管理本部Tenant\n</code></pre><p>反対に、すべてを1つのTenantに押し込むと、今度はデータや権限の分離が難しくなります。つまり、現実世界の組織構造と、SaaS上の利用構造は、似ているようで異なるのです。</p><hr><h1 id=\"2-tenant%E3%81%AF%E7%8F%BE%E5%AE%9F%E4%B8%96%E7%95%8C%E3%81%AE%E7%B5%84%E7%B9%94%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%84\">2. Tenantは現実世界の組織ではない</h1><p>Tenantは、会社や部署そのものではなく、SaaS上で何かを分離するための境界です。</p><pre><code class=\"language-text\">Tenant\n＝ SaaS上の管理・契約・セキュリティ・データ分離の境界\n</code></pre><p>SaaSにおいてTenantを分ける理由には、次のようなものがあります。</p><ul><li>データを分離する</li><li>管理者を分離する</li><li>契約を分離する</li><li>請求を分離する</li><li>認証設定を分離する</li><li>データ保存地域を分離する</li><li>セキュリティポリシーを分離する</li><li>監査ログを分離する</li></ul><p>したがって、現実世界の会社が1つであっても、Tenantが複数になることがあります。</p><pre><code class=\"language-text\">株式会社ABC\n├── 国内事業Tenant\n└── 海外事業Tenant\n</code></pre><p>逆に、複数の会社が1つのTenantを共有することもあります。</p><pre><code class=\"language-text\">ABCグループTenant\n├── 株式会社ABC\n├── ABC販売株式会社\n└── ABCシステムズ株式会社\n</code></pre><p>この場合、会社は3つあってもTenantは1つです。</p><hr><h1 id=\"3-organization%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B\">3. Organizationを追加する</h1><p>現実世界の会社や部署を表すため、Organizationという概念を導入します。</p><pre><code class=\"language-text\">Organization\n＝ 現実世界に存在する組織・団体\n</code></pre><p>Organizationの例には、次のようなものがあります。</p><ul><li>法人</li><li>会社</li><li>官公庁</li><li>自治体</li><li>学校</li><li>医療法人</li><li>部署</li><li>事業部</li><li>支店</li><li>子会社</li><li>プロジェクト組織</li></ul><p>最初のモデルでは、会社も部署もOrganizationの一種として扱います。</p><pre><code class=\"language-text\">株式会社ABC a Organization\n営業本部 a Organization\n開発本部 a Organization\n</code></pre><p>Organization同士には、階層関係を持たせます。</p><pre><code class=\"language-text\">株式会社ABC\nhasSubOrganization\n営業本部\n</code></pre><p>全体としては、次のようになります。</p><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph TD\n    A[株式会社ABC<br>Organization]\n    B[営業本部<br>Organization]\n    C[開発本部<br>Organization]\n    D[管理本部<br>Organization]\n\n    A -->|hasSubOrganization| B\n    A -->|hasSubOrganization| C\n    A -->|hasSubOrganization| D\n\n</pre><!--kg-card-end: html--><p>ここで重要なのは、OrganizationとTenantを分離することです。</p><pre><code class=\"language-text\">Organization\n＝ 現実世界の組織\n\nTenant\n＝ SaaS上の論理的な利用境界\n</code></pre><hr><h1 id=\"4-organization%E3%81%A8tenant%E3%81%AE%E9%96%A2%E4%BF%82\">4. OrganizationとTenantの関係</h1><p>OrganizationとTenantの間には、利用関係を置きます。</p><pre><code class=\"language-text\">Organization usesTenant Tenant\n</code></pre><p>逆方向から表現するなら、次のようになります。</p><pre><code class=\"language-text\">Tenant servesOrganization Organization\n</code></pre><p>たとえば株式会社ABCがTenant ABCを使う場合は、次のように表現できます。</p><pre><code class=\"language-text\">株式会社ABC\n→ usesTenant\n→ Tenant ABC\n</code></pre><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph LR\n    O[株式会社ABC<br>Organization]\n    T[Tenant ABC<br>Tenant]\n\n    O -->|usesTenant| T\n\n</pre><!--kg-card-end: html--><p>ただし、OrganizationとTenantは1対1とは限りません。</p><ul><li>1つのOrganizationが複数Tenantを利用する</li></ul><pre><code class=\"language-text\">株式会社ABC\n├── 国内事業Tenant\n└── 海外事業Tenant\n</code></pre><ul><li>複数Organizationが1つのTenantを利用する</li></ul><pre><code class=\"language-text\">ABCグループTenant\n├── 株式会社ABC\n├── ABC販売株式会社\n└── ABCシステムズ株式会社\n</code></pre><p>したがって、一般化すればOrganizationとTenantの関係は多対多ですが、初期実装では複雑さを避けるため、</p><pre><code class=\"language-text\">1つのTenantは1つの主Organizationに対応する\n</code></pre><p>という制約を置いても構いません。</p><hr><h1 id=\"5-workspace%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B\">5. Workspaceを追加する</h1><p>次に、Workspaceを導入します。</p><pre><code class=\"language-text\">Workspace\n＝ Tenantの内部に作られる共同作業の論理的な空間\n</code></pre><p>TenantとWorkspaceは、どちらも境界に見えるため混同されやすい概念ですが、役割を分けると次のようになります。</p><pre><code class=\"language-text\">Tenant\n＝ 契約・請求・セキュリティ・データ分離の境界\n\nWorkspace\n＝ 日常的な共同作業や業務データ整理の境界\n</code></pre><p>たとえば、株式会社ABCのTenant内に、次のWorkspaceを作ることができます。</p><pre><code class=\"language-text\">Tenant ABC\n├── 営業Workspace\n├── 開発Workspace\n└── 管理Workspace\n</code></pre><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph TD\n    T[Tenant ABC]\n\n    W1[営業Workspace]\n    W2[開発Workspace]\n    W3[管理Workspace]\n\n    T -->|containsWorkspace| W1\n    T -->|containsWorkspace| W2\n    T -->|containsWorkspace| W3\n\n</pre><!--kg-card-end: html--><p>この構造であれば、契約やSSO設定はTenantで共通化しながら、業務データをWorkspaceごとに整理できます。</p><hr><h1 id=\"6-tenant%E3%81%A8workspace%E3%81%AE%E9%81%95%E3%81%84\">6. TenantとWorkspaceの違い</h1><p>Tenantを分けると、多くの場合、次のような設定も分離されます。</p><ul><li>契約</li><li>請求</li><li>SSO</li><li>管理者</li><li>セキュリティポリシー</li><li>APIキー</li><li>監査ログ</li><li>データ保持期間</li><li>データ保存地域</li></ul><p>一方、Workspaceは、そこまで強い境界ではありません。</p><pre><code class=\"language-text\">契約は1つ\n請求も1つ\nSSO設定も共通\nしかし業務データや作業場所は分けたい\n</code></pre><p>このような要求にWorkspaceが向いています。</p><p>たとえるなら、</p><pre><code class=\"language-text\">Tenant\n＝ 建物全体\n\nWorkspace\n＝ 建物内の用途別エリア\n</code></pre><p>です。</p><p>ただし、このたとえも完全ではありません。Workspace間で一部のデータを共有するSaaSもあるためです。オントロジーのたとえ話は理解を助ける一方で、使いすぎると逆に概念を固定しすぎます。たとえは地図であって、現実そのものではありませんが、ややこしくなるので、いまのところはそういうものとして次に行きましょう！</p><hr><h1 id=\"7-resource%E3%81%A8workspace\">7. ResourceとWorkspace</h1><p>Workspaceを導入すると、ResourceはWorkspaceに格納されるようになります。</p><pre><code class=\"language-text\">Tenant containsWorkspace Workspace\nWorkspace containsResource Resource\n</code></pre><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph LR\n    T[Tenant]\n    W[Workspace]\n    R[Resource]\n\n    T -->|containsWorkspace| W\n    W -->|containsResource| R\n</pre><!--kg-card-end: html--><p>ResourceからTenantをたどる場合は、次の経路になります。</p><pre><code class=\"language-text\">Resource\n→ belongsToWorkspace\n→ Workspace\n→ belongsToTenant\n→ Tenant\n</code></pre><p>たとえば、CRMであれば次のようになります。</p><pre><code class=\"language-text\">顧客(データ)「株式会社XYZ」\n→ 営業Workspaceに属する\n→ Tenant ABCに属する\n</code></pre><p>実際のデータベースでは、Resourceに<code>workspace_id</code>と<code>tenant_id</code>の両方を持たせることがあります。</p><pre><code class=\"language-text\">customer\n├── customer_id\n├── tenant_id\n├── workspace_id\n├── name\n└── created_at\n</code></pre><p>概念上は<code>workspace_id</code>からTenantを特定できますが、物理設計では次の理由で<code>tenant_id</code>を重複して保持することがあります。</p><ul><li>Tenant条件をすべてのクエリに適用するため</li><li>データ漏えいを防ぐため</li><li>パーティションキーに利用するため</li><li>検索性能を高めるため</li><li>不整合を検知するため</li></ul><p>これは、概念モデルと物理データモデルが同一ではない例です。オントロジーは「何を意味するか」を表し、データベースは「どう安全かつ高速に保存するか」を考えます。</p><hr><h1 id=\"8-team%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B\">8. Teamを追加する</h1><p>次に、Teamを追加します。</p><pre><code class=\"language-text\">Team\n＝ Tenant内で共同作業や権限付与に使われるメンバーの集合\n</code></pre><p>Teamの例は次のとおりです。</p><ul><li>営業Team</li><li>開発Team</li><li>管理者Team</li><li>セキュリティTeam</li><li>プロジェクトATeam</li><li>外部監査Team</li></ul><p>TeamはOrganizationと似ていますが、同じものではありません。Organizationが現実世界の組織構造を表すのに対し、Teamは、SaaS内で人をまとめるための集合です。</p><pre><code class=\"language-text\">Organization\n＝ 現実世界の組織\n\nTeam\n＝ SaaS上のメンバー集合\n</code></pre><p>たとえば、プロジェクトATeamには、複数の部署からメンバーが参加できます。</p><pre><code class=\"language-text\">プロジェクトATeam\n├── 営業本部の山田さん\n├── 開発本部の佐藤さん\n├── 管理本部の鈴木さん\n└── 外部委託先の高橋さん\n</code></pre><p>このTeamは正式な組織図には存在しないかもしれませんが、SaaS上のアクセス制御では重要です。</p><hr><h1 id=\"9-team%E3%81%AB%E3%81%AFuser%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8Fmembership%E3%82%92%E5%85%A5%E3%82%8C%E3%82%8B\">9. TeamにはUserではなくMembershipを入れる</h1><p>Teamのメンバーとして、Userを直接追加する設計もできます。</p><pre><code class=\"language-text\">Team hasMember User\n</code></pre><p>しかし、B2B SaaS共通モデルとしては、MembershipをTeamに所属させる方が自然です。</p><pre><code class=\"language-text\">Team hasMember Membership\n</code></pre><p>理由は、同じUserが複数Tenantに所属できるためです。</p><pre><code class=\"language-text\">User: 山田太郎\n\nMembership 1\n├── Tenant A\n└── Role: 管理者\n\nMembership 2\n├── Tenant B\n└── Role: 閲覧者\n</code></pre><p>Tenant AのTeamに所属させたいのは、単なる山田太郎というUserではありません。</p><p>正確には、</p><pre><code class=\"language-text\">山田太郎のTenant AにおけるMembership\n</code></pre><p>を所属させたいのです。</p><p>したがって、次の関係を使います。</p><pre><code class=\"language-text\">Membership memberOfTeam Team\n</code></pre><hr><h1 id=\"10-workspace%E3%81%A8team%E3%81%AE%E9%81%95%E3%81%84\">10. WorkspaceとTeamの違い</h1><p>WorkspaceとTeamも混同されやすい概念で、違いは何をまとめるかにあります。</p><pre><code class=\"language-text\">Workspace\n＝ 作業場所やデータをまとめる\n\nTeam\n＝ メンバーをまとめる\n</code></pre><p>営業Workspaceには、次のようなデータがあります。</p><pre><code class=\"language-text\">営業Workspace\n├── 顧客\n├── 商談\n├── 営業資料\n└── 活動履歴\n</code></pre><p>営業Teamには、人が所属します。</p><pre><code class=\"language-text\">営業Team\n├── 山田さんのMembership\n├── 佐藤さんのMembership\n└── 鈴木さんのMembership\n</code></pre><p>TeamがWorkspaceにアクセスします。</p><pre><code class=\"language-text\">Team canAccessWorkspace Workspace\n</code></pre><p>つまり、</p><pre><code class=\"language-text\">Team\n＝ 誰が\n\nWorkspace\n＝ どこで\n\nResource\n＝ 何を\n</code></pre><p>と考えることができます。</p><hr><h1 id=\"11-organization%E3%83%BBtenant%E3%83%BBworkspace%E3%83%BBteam%E3%81%AE%E6%95%B4%E7%90%86\">11. Organization・Tenant・Workspace・Teamの整理</h1><!--kg-card-begin: html--><table data-line=\"566\" class=\"code-line\" dir=\"auto\" style=\"border-collapse: collapse; margin-bottom: 0.7em;\"><thead data-line=\"566\" class=\"code-line\" dir=\"auto\"><tr data-line=\"566\" class=\"code-line\" dir=\"auto\"><th style=\"text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.69); padding: 5px 10px; border-top-color: rgba(255, 255, 255, 0.69); border-right-color: rgba(255, 255, 255, 0.69); border-left-color: rgba(255, 255, 255, 0.69);\">概念</th><th style=\"text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.69); padding: 5px 10px; border-top-color: rgba(255, 255, 255, 0.69); border-right-color: rgba(255, 255, 255, 0.69); border-left-color: rgba(255, 255, 255, 0.69);\">意味</th><th style=\"text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.69); padding: 5px 10px; border-top-color: rgba(255, 255, 255, 0.69); border-right-color: rgba(255, 255, 255, 0.69); border-left-color: rgba(255, 255, 255, 0.69);\">主に含むもの</th></tr></thead><tbody data-line=\"568\" class=\"code-line\" dir=\"auto\"><tr data-line=\"568\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Organization</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">現実世界の組織</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">法人、部署、支店</td></tr><tr data-line=\"569\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Tenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">SaaS上の管理・分離境界</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">契約、セキュリティ、設定</td></tr><tr data-line=\"570\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Workspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Tenant内の共同作業空間</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Resource、作業対象</td></tr><tr data-line=\"571\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Team</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Tenant内のメンバー集合</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Membership</td></tr></tbody></table><!--kg-card-end: html--><p>簡単にまとめると、次のようになります。</p><pre><code class=\"language-text\">Organization\n＝ 現実世界では、どの組織なのか\n\nTenant\n＝ SaaS上では、どこまでを1つの顧客環境として扱うのか\n\nWorkspace\n＝ Tenant内で、どこを作業場所として使うのか\n\nTeam\n＝ 誰と誰をひとまとまりにするのか\n</code></pre><hr><h1 id=\"12-role%E3%81%AE%E9%81%A9%E7%94%A8%E7%AF%84%E5%9B%B2%E3%81%A8%E3%81%84%E3%81%86%E5%95%8F%E9%A1%8C\">12. Roleの適用範囲という問題</h1><p>Workspaceを導入すると、前回のRoleモデルに新しい問題が生まれます。前回は、MembershipにRoleを付与しました。</p><pre><code class=\"language-text\">Membership assignedRole Role\n</code></pre><p>しかし、Roleがどの範囲で有効なのか分かりません。</p><p>たとえば、山田太郎さんが次の権限を持っているとします。</p><pre><code class=\"language-text\">Tenant ABCでは一般メンバー\n\n営業Workspaceでは編集者\n\n開発Workspaceでは閲覧者\n</code></pre><p>この場合、単純な</p><pre><code class=\"language-text\">Membership hasRole Editor\n</code></pre><p>では不十分で、どのWorkspaceでEditorなのかを表現する必要があります。</p><hr><h1 id=\"13-roleassignment%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B\">13. RoleAssignmentを追加する</h1><p>Roleの付与そのものを表す概念として、RoleAssignmentを追加します。</p><pre><code class=\"language-text\">RoleAssignment\n＝ ある主体に、ある適用範囲で、あるRoleを付与した事実\n</code></pre><p>RoleAssignmentは、最低限次の3つを結びます。</p><pre><code class=\"language-text\">assignee\n＝ 誰に付与するか\n\nrole\n＝ どのRoleを付与するか\n\nscope\n＝ どの範囲で有効か\n</code></pre><p>営業Workspaceで山田太郎さんをEditorにする場合は、次のようになります。</p><pre><code class=\"language-text\">RoleAssignment\n├── assignee: 山田太郎のMembership\n├── role: Editor\n└── scope: 営業Workspace\n</code></pre><p>Tenant全体の管理者にする場合は、次のようになります。</p><pre><code class=\"language-text\">RoleAssignment\n├── assignee: 山田太郎のMembership\n├── role: Tenant Administrator\n└── scope: Tenant ABC\n</code></pre><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph TD\n    M[Membership]\n    RA[RoleAssignment]\n    R[Role]\n    S[Scope<br>Tenant or Workspace]\n\n    M -->|hasRoleAssignment| RA\n    RA -->|assignedRole| R\n    RA -->|appliesTo| S\n</pre><!--kg-card-end: html--><p>たとえばRoleAssignmentには、将来的に次の属性を持たせることができます。</p><ul><li>付与日時</li><li>付与者</li><li>有効期限</li><li>付与理由</li><li>手動付与か自動付与か</li><li>一時的な権限か</li><li>承認済みか</li></ul><p>関係に属性が必要になったら、その関係を独立した概念にする。これはオントロジー設計において、とてもよく使われる考え方のようです。（たぶん）</p><hr><h1 id=\"14-%E4%BB%8A%E5%9B%9E%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B%E3%82%AF%E3%83%A9%E3%82%B9\">14. 今回追加するクラス</h1><p>今回のオントロジーでは、前回のクラスに次のものを追加します。</p><pre><code class=\"language-text\">Organization\nWorkspace\nTeam\nRoleAssignment\n</code></pre><p>前回から引き継ぐ主なクラスは次のとおりです。</p><pre><code class=\"language-text\">Tenant\nUser\nMembership\nRole\nPermission\nSubscription\nPlan\nResource\n</code></pre><p>今回までの全体像は、次のようになります。</p><pre><code class=\"language-text\">Organization\n└── usesTenant\n    └── Tenant\n        ├── Membership\n        │   └── User\n        │\n        ├── Workspace\n        │   └── Resource\n        │\n        ├── Team\n        │   └── Membership\n        │\n        ├── RoleAssignment\n        │   ├── Membership\n        │   ├── Role\n        │   │   └── Permission\n        │   └── Scope\n        │       ├── Tenant\n        │       └── Workspace\n        │\n        └── Subscription\n            └── Plan\n</code></pre><hr><h1 id=\"15-%E3%82%AA%E3%83%B3%E3%83%88%E3%83%AD%E3%82%B8%E3%83%BC%E5%AE%9A%E7%BE%A9%E3%81%AEturtle\">15. オントロジー定義のTurtle</h1><p>ここからは、今回追加した概念をTurtleで定義します。</p><p>最初は、できるだけ単純なOWL/RDFSモデルにします。</p><pre><code class=\"language-turtle\">@prefix saas: &lt;https://example.com/ontology/saas#&gt; .\n@prefix rdf:  &lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&gt; .\n@prefix rdfs: &lt;http://www.w3.org/2000/01/rdf-schema#&gt; .\n@prefix owl:  &lt;http://www.w3.org/2002/07/owl#&gt; .\n@prefix xsd:  &lt;http://www.w3.org/2001/XMLSchema#&gt; .\n\nsaas:B2BSaaSOntology a owl:Ontology ;\n    rdfs:label \"B2B SaaS Core Ontology\"@en ,\n               \"B2B SaaS共通オントロジー\"@ja .\n\n#\n# Classes\n#\n\nsaas:Organization a owl:Class ;\n    rdfs:label \"Organization\"@en ,\n               \"組織\"@ja ;\n    rdfs:comment\n        \"現実世界に存在する法人、部署、支店、団体などを表す。\"@ja .\n\nsaas:Tenant a owl:Class ;\n    rdfs:label \"Tenant\"@en ,\n               \"テナント\"@ja ;\n    rdfs:comment\n        \"SaaS上で契約、管理、セキュリティ、データを分離する論理的な境界。\"@ja .\n\nsaas:Workspace a owl:Class ;\n    rdfs:label \"Workspace\"@en ,\n               \"ワークスペース\"@ja ;\n    rdfs:comment\n        \"Tenant内部に作られる共同作業の論理的な空間。\"@ja .\n\nsaas:Team a owl:Class ;\n    rdfs:label \"Team\"@en ,\n               \"チーム\"@ja ;\n    rdfs:comment\n        \"Tenant内で共同作業やアクセス制御に使用するMembershipの集合。\"@ja .\n\nsaas:User a owl:Class ;\n    rdfs:label \"User\"@en ,\n               \"ユーザー\"@ja .\n\nsaas:Membership a owl:Class ;\n    rdfs:label \"Membership\"@en ,\n               \"所属関係\"@ja ;\n    rdfs:comment\n        \"Userが特定のTenantに参加しているという関係を表す。\"@ja .\n\nsaas:RoleAssignment a owl:Class ;\n    rdfs:label \"Role Assignment\"@en ,\n               \"ロール割当て\"@ja ;\n    rdfs:comment\n        \"あるMembershipに、特定の適用範囲でRoleを付与した事実。\"@ja .\n\nsaas:Role a owl:Class ;\n    rdfs:label \"Role\"@en ,\n               \"ロール\"@ja .\n\nsaas:Permission a owl:Class ;\n    rdfs:label \"Permission\"@en ,\n               \"権限\"@ja .\n\nsaas:Resource a owl:Class ;\n    rdfs:label \"Resource\"@en ,\n               \"リソース\"@ja ;\n    rdfs:comment\n        \"SaaS上でTenantまたはWorkspaceが管理する業務オブジェクト。\"@ja .\n\nsaas:Subscription a owl:Class .\nsaas:Plan a owl:Class .\n\n#\n# Organization properties\n#\n\nsaas:hasSubOrganization a owl:ObjectProperty ;\n    rdfs:domain saas:Organization ;\n    rdfs:range saas:Organization ;\n    rdfs:label \"has sub-organization\"@en ,\n               \"下位組織を持つ\"@ja .\n\nsaas:parentOrganization a owl:ObjectProperty ;\n    owl:inverseOf saas:hasSubOrganization ;\n    rdfs:domain saas:Organization ;\n    rdfs:range saas:Organization ;\n    rdfs:label \"parent organization\"@en ,\n               \"上位組織\"@ja .\n\nsaas:usesTenant a owl:ObjectProperty ;\n    rdfs:domain saas:Organization ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"uses tenant\"@en ,\n               \"テナントを利用する\"@ja .\n\nsaas:servesOrganization a owl:ObjectProperty ;\n    owl:inverseOf saas:usesTenant ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Organization ;\n    rdfs:label \"serves organization\"@en ,\n               \"組織に提供される\"@ja .\n\n#\n# Workspace properties\n#\n\nsaas:containsWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"contains workspace\"@en ,\n               \"ワークスペースを含む\"@ja .\n\nsaas:belongsToTenant a owl:ObjectProperty ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"belongs to tenant\"@en ,\n               \"テナントに属する\"@ja .\n\nsaas:workspaceBelongsToTenant a owl:ObjectProperty ;\n    rdfs:subPropertyOf saas:belongsToTenant ;\n    owl:inverseOf saas:containsWorkspace ;\n    rdfs:domain saas:Workspace ;\n    rdfs:range saas:Tenant .\n\nsaas:containsResource a owl:ObjectProperty ;\n    rdfs:domain saas:Workspace ;\n    rdfs:range saas:Resource ;\n    rdfs:label \"contains resource\"@en ,\n               \"リソースを含む\"@ja .\n\nsaas:belongsToWorkspace a owl:ObjectProperty ;\n    owl:inverseOf saas:containsResource ;\n    rdfs:domain saas:Resource ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"belongs to workspace\"@en ,\n               \"ワークスペースに属する\"@ja .\n\n#\n# Membership and Team properties\n#\n\nsaas:belongsToUser a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:User .\n\nsaas:membershipBelongsToTenant a owl:ObjectProperty ;\n    rdfs:subPropertyOf saas:belongsToTenant ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:Tenant .\n\nsaas:containsTeam a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Team ;\n    rdfs:label \"contains team\"@en ,\n               \"チームを含む\"@ja .\n\nsaas:teamBelongsToTenant a owl:ObjectProperty ;\n    rdfs:subPropertyOf saas:belongsToTenant ;\n    owl:inverseOf saas:containsTeam ;\n    rdfs:domain saas:Team ;\n    rdfs:range saas:Tenant .\n\nsaas:memberOfTeam a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:Team ;\n    rdfs:label \"member of team\"@en ,\n               \"チームに所属する\"@ja .\n\nsaas:hasTeamMember a owl:ObjectProperty ;\n    owl:inverseOf saas:memberOfTeam ;\n    rdfs:domain saas:Team ;\n    rdfs:range saas:Membership .\n\nsaas:canAccessWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:Team ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"can access workspace\"@en ,\n               \"ワークスペースにアクセスできる\"@ja .\n\n#\n# Role assignment properties\n#\n\nsaas:hasRoleAssignment a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:RoleAssignment .\n\nsaas:assignedTo a owl:ObjectProperty ;\n    owl:inverseOf saas:hasRoleAssignment ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Membership ;\n    rdfs:label \"assigned to\"@en ,\n               \"割当て対象\"@ja .\n\nsaas:assignedRole a owl:ObjectProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Role ;\n    rdfs:label \"assigned role\"@en ,\n               \"割り当てるロール\"@ja .\n\nsaas:appliesToTenant a owl:ObjectProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"applies to tenant\"@en ,\n               \"テナントに適用される\"@ja .\n\nsaas:appliesToWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"applies to workspace\"@en ,\n               \"ワークスペースに適用される\"@ja .\n\nsaas:grantsPermission a owl:ObjectProperty ;\n    rdfs:domain saas:Role ;\n    rdfs:range saas:Permission .\n\n#\n# Datatype properties\n#\n\nsaas:name a owl:DatatypeProperty ;\n    rdfs:range xsd:string .\n\nsaas:status a owl:DatatypeProperty ;\n    rdfs:range xsd:string .\n\nsaas:createdAt a owl:DatatypeProperty ;\n    rdfs:range xsd:dateTime .\n\nsaas:assignedAt a owl:DatatypeProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range xsd:dateTime .\n\nsaas:expiresAt a owl:DatatypeProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range xsd:dateTime .\n</code></pre><p></p><!--kg-card-begin: html-->  <script type=\"text/turtle\" class=\"ontologyviewer\" data-height=\"600px\" data-layout=\"dagre\">\n\n@prefix saas: <https://example.com/ontology/saas#> .\n@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n@prefix owl:  <http://www.w3.org/2002/07/owl#> .\n@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .\n\nsaas:B2BSaaSOntology a owl:Ontology ;\n    rdfs:label \"B2B SaaS Core Ontology\"@en ,\n               \"B2B SaaS共通オントロジー\"@ja .\n\n#\n# Classes\n#\n\nsaas:Organization a owl:Class ;\n    rdfs:label \"Organization\"@en ,\n               \"組織\"@ja ;\n    rdfs:comment\n        \"現実世界に存在する法人、部署、支店、団体などを表す。\"@ja .\n\nsaas:Tenant a owl:Class ;\n    rdfs:label \"Tenant\"@en ,\n               \"テナント\"@ja ;\n    rdfs:comment\n        \"SaaS上で契約、管理、セキュリティ、データを分離する論理的な境界。\"@ja .\n\nsaas:Workspace a owl:Class ;\n    rdfs:label \"Workspace\"@en ,\n               \"ワークスペース\"@ja ;\n    rdfs:comment\n        \"Tenant内部に作られる共同作業の論理的な空間。\"@ja .\n\nsaas:Team a owl:Class ;\n    rdfs:label \"Team\"@en ,\n               \"チーム\"@ja ;\n    rdfs:comment\n        \"Tenant内で共同作業やアクセス制御に使用するMembershipの集合。\"@ja .\n\nsaas:User a owl:Class ;\n    rdfs:label \"User\"@en ,\n               \"ユーザー\"@ja .\n\nsaas:Membership a owl:Class ;\n    rdfs:label \"Membership\"@en ,\n               \"所属関係\"@ja ;\n    rdfs:comment\n        \"Userが特定のTenantに参加しているという関係を表す。\"@ja .\n\nsaas:RoleAssignment a owl:Class ;\n    rdfs:label \"Role Assignment\"@en ,\n               \"ロール割当て\"@ja ;\n    rdfs:comment\n        \"あるMembershipに、特定の適用範囲でRoleを付与した事実。\"@ja .\n\nsaas:Role a owl:Class ;\n    rdfs:label \"Role\"@en ,\n               \"ロール\"@ja .\n\nsaas:Permission a owl:Class ;\n    rdfs:label \"Permission\"@en ,\n               \"権限\"@ja .\n\nsaas:Resource a owl:Class ;\n    rdfs:label \"Resource\"@en ,\n               \"リソース\"@ja ;\n    rdfs:comment\n        \"SaaS上でTenantまたはWorkspaceが管理する業務オブジェクト。\"@ja .\n\nsaas:Subscription a owl:Class .\nsaas:Plan a owl:Class .\n\n#\n# Organization properties\n#\n\nsaas:hasSubOrganization a owl:ObjectProperty ;\n    rdfs:domain saas:Organization ;\n    rdfs:range saas:Organization ;\n    rdfs:label \"has sub-organization\"@en ,\n               \"下位組織を持つ\"@ja .\n\nsaas:parentOrganization a owl:ObjectProperty ;\n    owl:inverseOf saas:hasSubOrganization ;\n    rdfs:domain saas:Organization ;\n    rdfs:range saas:Organization ;\n    rdfs:label \"parent organization\"@en ,\n               \"上位組織\"@ja .\n\nsaas:usesTenant a owl:ObjectProperty ;\n    rdfs:domain saas:Organization ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"uses tenant\"@en ,\n               \"テナントを利用する\"@ja .\n\nsaas:servesOrganization a owl:ObjectProperty ;\n    owl:inverseOf saas:usesTenant ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Organization ;\n    rdfs:label \"serves organization\"@en ,\n               \"組織に提供される\"@ja .\n\n#\n# Workspace properties\n#\n\nsaas:containsWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"contains workspace\"@en ,\n               \"ワークスペースを含む\"@ja .\n\nsaas:belongsToTenant a owl:ObjectProperty ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"belongs to tenant\"@en ,\n               \"テナントに属する\"@ja .\n\nsaas:workspaceBelongsToTenant a owl:ObjectProperty ;\n    rdfs:subPropertyOf saas:belongsToTenant ;\n    owl:inverseOf saas:containsWorkspace ;\n    rdfs:domain saas:Workspace ;\n    rdfs:range saas:Tenant .\n\nsaas:containsResource a owl:ObjectProperty ;\n    rdfs:domain saas:Workspace ;\n    rdfs:range saas:Resource ;\n    rdfs:label \"contains resource\"@en ,\n               \"リソースを含む\"@ja .\n\nsaas:belongsToWorkspace a owl:ObjectProperty ;\n    owl:inverseOf saas:containsResource ;\n    rdfs:domain saas:Resource ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"belongs to workspace\"@en ,\n               \"ワークスペースに属する\"@ja .\n\n#\n# Membership and Team properties\n#\n\nsaas:belongsToUser a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:User .\n\nsaas:membershipBelongsToTenant a owl:ObjectProperty ;\n    rdfs:subPropertyOf saas:belongsToTenant ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:Tenant .\n\nsaas:containsTeam a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Team ;\n    rdfs:label \"contains team\"@en ,\n               \"チームを含む\"@ja .\n\nsaas:teamBelongsToTenant a owl:ObjectProperty ;\n    rdfs:subPropertyOf saas:belongsToTenant ;\n    owl:inverseOf saas:containsTeam ;\n    rdfs:domain saas:Team ;\n    rdfs:range saas:Tenant .\n\nsaas:memberOfTeam a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:Team ;\n    rdfs:label \"member of team\"@en ,\n               \"チームに所属する\"@ja .\n\nsaas:hasTeamMember a owl:ObjectProperty ;\n    owl:inverseOf saas:memberOfTeam ;\n    rdfs:domain saas:Team ;\n    rdfs:range saas:Membership .\n\nsaas:canAccessWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:Team ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"can access workspace\"@en ,\n               \"ワークスペースにアクセスできる\"@ja .\n\n#\n# Role assignment properties\n#\n\nsaas:hasRoleAssignment a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:RoleAssignment .\n\nsaas:assignedTo a owl:ObjectProperty ;\n    owl:inverseOf saas:hasRoleAssignment ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Membership ;\n    rdfs:label \"assigned to\"@en ,\n               \"割当て対象\"@ja .\n\nsaas:assignedRole a owl:ObjectProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Role ;\n    rdfs:label \"assigned role\"@en ,\n               \"割り当てるロール\"@ja .\n\nsaas:appliesToTenant a owl:ObjectProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"applies to tenant\"@en ,\n               \"テナントに適用される\"@ja .\n\nsaas:appliesToWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range saas:Workspace ;\n    rdfs:label \"applies to workspace\"@en ,\n               \"ワークスペースに適用される\"@ja .\n\nsaas:grantsPermission a owl:ObjectProperty ;\n    rdfs:domain saas:Role ;\n    rdfs:range saas:Permission .\n\n#\n# Datatype properties\n#\n\nsaas:name a owl:DatatypeProperty ;\n    rdfs:range xsd:string .\n\nsaas:status a owl:DatatypeProperty ;\n    rdfs:range xsd:string .\n\nsaas:createdAt a owl:DatatypeProperty ;\n    rdfs:range xsd:dateTime .\n\nsaas:assignedAt a owl:DatatypeProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range xsd:dateTime .\n\nsaas:expiresAt a owl:DatatypeProperty ;\n    rdfs:domain saas:RoleAssignment ;\n    rdfs:range xsd:dateTime .\n\n</script><!--kg-card-end: html--><p>第一回に比べて、いきなりでっかくなりましたね。。。</p><hr><h1 id=\"16-belongstotenant%E3%82%92%E5%85%B1%E9%80%9A%E5%8C%96%E3%81%99%E3%82%8B%E7%90%86%E7%94%B1\">16. <code>belongsToTenant</code>を共通化する理由</h1><p>上のTurtleでは、次の3つのプロパティを用意しました。</p><pre><code class=\"language-text\">workspaceBelongsToTenant\nmembershipBelongsToTenant\nteamBelongsToTenant\n</code></pre><p>これらは、すべて上位プロパティである<code>belongsToTenant</code>のサブプロパティです。</p><pre><code class=\"language-turtle\">saas:workspaceBelongsToTenant\n    rdfs:subPropertyOf saas:belongsToTenant .\n\nsaas:membershipBelongsToTenant\n    rdfs:subPropertyOf saas:belongsToTenant .\n\nsaas:teamBelongsToTenant\n    rdfs:subPropertyOf saas:belongsToTenant .\n</code></pre><p>これにより、Workspace、Membership、Teamをまとめて、</p><pre><code class=\"language-sparql\">?entity saas:belongsToTenant ?tenant .\n</code></pre><p>という形で検索できます。</p><p>一方で、詳細を知りたい場合は、</p><pre><code class=\"language-sparql\">?workspace saas:workspaceBelongsToTenant ?tenant .\n</code></pre><p>のように、具体的な関係を指定できます。</p><p>このような上位プロパティは、検索や推論を簡単にするために役立ちます。</p><hr><h1 id=\"17-%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BEabc%E3%81%AE%E5%85%B7%E4%BD%93%E7%9A%84%E3%81%AA%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9\">17. 株式会社ABCの具体的なインスタンス</h1><p>ここからは、実際のデータをTurtleで表現します。</p><h2 id=\"171-%E7%B5%84%E7%B9%94%E3%83%BBtenant%E3%83%BBworkspace%E3%83%BBteam\">17.1 組織・Tenant・Workspace・Team</h2><pre><code class=\"language-turtle\">@prefix saas: &lt;https://example.com/ontology/saas#&gt; .\n@prefix ex:   &lt;https://example.com/data/&gt; .\n@prefix xsd:  &lt;http://www.w3.org/2001/XMLSchema#&gt; .\n\n#\n# Organizations\n#\n\nex:organization-abc a saas:Organization ;\n    saas:name \"株式会社ABC\" ;\n    saas:hasSubOrganization\n        ex:organization-abc-sales,\n        ex:organization-abc-development,\n        ex:organization-abc-administration ;\n    saas:usesTenant ex:tenant-abc .\n\nex:organization-abc-sales a saas:Organization ;\n    saas:name \"営業本部\" ;\n    saas:parentOrganization ex:organization-abc .\n\nex:organization-abc-development a saas:Organization ;\n    saas:name \"開発本部\" ;\n    saas:parentOrganization ex:organization-abc .\n\nex:organization-abc-administration a saas:Organization ;\n    saas:name \"管理本部\" ;\n    saas:parentOrganization ex:organization-abc .\n\n#\n# Tenant\n#\n\nex:tenant-abc a saas:Tenant ;\n    saas:name \"株式会社ABCテナント\" ;\n    saas:status \"active\" ;\n    saas:containsWorkspace\n        ex:workspace-sales,\n        ex:workspace-development,\n        ex:workspace-administration ;\n    saas:containsTeam\n        ex:team-sales,\n        ex:team-development,\n        ex:team-project-alpha .\n\n#\n# Workspaces\n#\n\nex:workspace-sales a saas:Workspace ;\n    saas:name \"営業Workspace\" ;\n    saas:workspaceBelongsToTenant ex:tenant-abc .\n\nex:workspace-development a saas:Workspace ;\n    saas:name \"開発Workspace\" ;\n    saas:workspaceBelongsToTenant ex:tenant-abc .\n\nex:workspace-administration a saas:Workspace ;\n    saas:name \"管理Workspace\" ;\n    saas:workspaceBelongsToTenant ex:tenant-abc .\n\n#\n# Teams\n#\n\nex:team-sales a saas:Team ;\n    saas:name \"営業Team\" ;\n    saas:teamBelongsToTenant ex:tenant-abc ;\n    saas:canAccessWorkspace ex:workspace-sales .\n\nex:team-development a saas:Team ;\n    saas:name \"開発Team\" ;\n    saas:teamBelongsToTenant ex:tenant-abc ;\n    saas:canAccessWorkspace ex:workspace-development .\n\nex:team-project-alpha a saas:Team ;\n    saas:name \"プロジェクトAlpha Team\" ;\n    saas:teamBelongsToTenant ex:tenant-abc ;\n    saas:canAccessWorkspace\n        ex:workspace-sales,\n        ex:workspace-development .\n</code></pre><p></p><!--kg-card-begin: html-->  <script type=\"text/turtle\" class=\"ontologyviewer\" data-height=\"600px\" data-layout=\"dagre\">\n\n@prefix saas: <https://example.com/ontology/saas#> .\n@prefix ex:   <https://example.com/data/> .\n@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .\n\n#\n# Organizations\n#\n\nex:organization-abc a saas:Organization ;\n    saas:name \"株式会社ABC\" ;\n    saas:hasSubOrganization\n        ex:organization-abc-sales,\n        ex:organization-abc-development,\n        ex:organization-abc-administration ;\n    saas:usesTenant ex:tenant-abc .\n\nex:organization-abc-sales a saas:Organization ;\n    saas:name \"営業本部\" ;\n    saas:parentOrganization ex:organization-abc .\n\nex:organization-abc-development a saas:Organization ;\n    saas:name \"開発本部\" ;\n    saas:parentOrganization ex:organization-abc .\n\nex:organization-abc-administration a saas:Organization ;\n    saas:name \"管理本部\" ;\n    saas:parentOrganization ex:organization-abc .\n\n#\n# Tenant\n#\n\nex:tenant-abc a saas:Tenant ;\n    saas:name \"株式会社ABCテナント\" ;\n    saas:status \"active\" ;\n    saas:containsWorkspace\n        ex:workspace-sales,\n        ex:workspace-development,\n        ex:workspace-administration ;\n    saas:containsTeam\n        ex:team-sales,\n        ex:team-development,\n        ex:team-project-alpha .\n\n#\n# Workspaces\n#\n\nex:workspace-sales a saas:Workspace ;\n    saas:name \"営業Workspace\" ;\n    saas:workspaceBelongsToTenant ex:tenant-abc .\n\nex:workspace-development a saas:Workspace ;\n    saas:name \"開発Workspace\" ;\n    saas:workspaceBelongsToTenant ex:tenant-abc .\n\nex:workspace-administration a saas:Workspace ;\n    saas:name \"管理Workspace\" ;\n    saas:workspaceBelongsToTenant ex:tenant-abc .\n\n#\n# Teams\n#\n\nex:team-sales a saas:Team ;\n    saas:name \"営業Team\" ;\n    saas:teamBelongsToTenant ex:tenant-abc ;\n    saas:canAccessWorkspace ex:workspace-sales .\n\nex:team-development a saas:Team ;\n    saas:name \"開発Team\" ;\n    saas:teamBelongsToTenant ex:tenant-abc ;\n    saas:canAccessWorkspace ex:workspace-development .\n\nex:team-project-alpha a saas:Team ;\n    saas:name \"プロジェクトAlpha Team\" ;\n    saas:teamBelongsToTenant ex:tenant-abc ;\n    saas:canAccessWorkspace\n        ex:workspace-sales,\n        ex:workspace-development .\n\n\n</script><!--kg-card-end: html--><hr><h1 id=\"18-user%E3%81%A8membership%E3%81%AE%E5%85%B7%E4%BD%93%E4%BE%8B\">18. UserとMembershipの具体例</h1><pre><code class=\"language-turtle\">#\n# Users\n#\n\nex:user-yamada a saas:User ;\n    saas:name \"山田太郎\" ;\n    saas:status \"active\" .\n\nex:user-sato a saas:User ;\n    saas:name \"佐藤花子\" ;\n    saas:status \"active\" .\n\nex:user-suzuki a saas:User ;\n    saas:name \"鈴木一郎\" ;\n    saas:status \"active\" .\n\n#\n# Memberships\n#\n\nex:membership-yamada-abc a saas:Membership ;\n    saas:belongsToUser ex:user-yamada ;\n    saas:membershipBelongsToTenant ex:tenant-abc ;\n    saas:status \"active\" ;\n    saas:memberOfTeam\n        ex:team-sales,\n        ex:team-project-alpha .\n\nex:membership-sato-abc a saas:Membership ;\n    saas:belongsToUser ex:user-sato ;\n    saas:membershipBelongsToTenant ex:tenant-abc ;\n    saas:status \"active\" ;\n    saas:memberOfTeam\n        ex:team-development,\n        ex:team-project-alpha .\n\nex:membership-suzuki-abc a saas:Membership ;\n    saas:belongsToUser ex:user-suzuki ;\n    saas:membershipBelongsToTenant ex:tenant-abc ;\n    saas:status \"active\" ;\n    saas:memberOfTeam ex:team-sales .\n</code></pre><p>ここで、UserではなくMembershipがTeamに所属していることが重要です。</p><pre><code class=\"language-text\">山田太郎という人\n</code></pre><p>ではなく、</p><pre><code class=\"language-text\">山田太郎のTenant ABCにおける所属\n</code></pre><p>が営業Teamに参加しています。</p><hr><h1 id=\"19-role%E3%81%A8permission%E3%81%AE%E5%85%B7%E4%BD%93%E4%BE%8B\">19. RoleとPermissionの具体例</h1><pre><code class=\"language-turtle\">#\n# Permissions\n#\n\nex:permission-tenant-manage a saas:Permission ;\n    saas:name \"tenant.manage\" .\n\nex:permission-member-invite a saas:Permission ;\n    saas:name \"member.invite\" .\n\nex:permission-workspace-read a saas:Permission ;\n    saas:name \"workspace.read\" .\n\nex:permission-resource-read a saas:Permission ;\n    saas:name \"resource.read\" .\n\nex:permission-resource-create a saas:Permission ;\n    saas:name \"resource.create\" .\n\nex:permission-resource-update a saas:Permission ;\n    saas:name \"resource.update\" .\n\nex:permission-resource-delete a saas:Permission ;\n    saas:name \"resource.delete\" .\n\n#\n# Roles\n#\n\nex:role-tenant-admin a saas:Role ;\n    saas:name \"Tenant Administrator\" ;\n    saas:grantsPermission\n        ex:permission-tenant-manage,\n        ex:permission-member-invite,\n        ex:permission-workspace-read,\n        ex:permission-resource-read,\n        ex:permission-resource-create,\n        ex:permission-resource-update,\n        ex:permission-resource-delete .\n\nex:role-workspace-editor a saas:Role ;\n    saas:name \"Workspace Editor\" ;\n    saas:grantsPermission\n        ex:permission-workspace-read,\n        ex:permission-resource-read,\n        ex:permission-resource-create,\n        ex:permission-resource-update .\n\nex:role-workspace-viewer a saas:Role ;\n    saas:name \"Workspace Viewer\" ;\n    saas:grantsPermission\n        ex:permission-workspace-read,\n        ex:permission-resource-read .\n</code></pre><hr><h1 id=\"20-roleassignment%E3%81%AE%E5%85%B7%E4%BD%93%E4%BE%8B\">20. RoleAssignmentの具体例</h1><p>山田太郎さんには、次の権限を付与します。</p><pre><code class=\"language-text\">Tenant ABC全体\n→ Tenant Administrator\n\n営業Workspace\n→ Workspace Editor\n\n開発Workspace\n→ Workspace Viewer\n</code></pre><p>これをTurtleで表現すると、次のようになります。</p><pre><code class=\"language-turtle\">#\n# Tenant-level role assignment\n#\n\nex:role-assignment-yamada-tenant-admin a saas:RoleAssignment ;\n    saas:assignedTo ex:membership-yamada-abc ;\n    saas:assignedRole ex:role-tenant-admin ;\n    saas:appliesToTenant ex:tenant-abc ;\n    saas:assignedAt \"2026-08-01T09:00:00+09:00\"^^xsd:dateTime .\n\n#\n# Workspace-level role assignments\n#\n\nex:role-assignment-yamada-sales-editor a saas:RoleAssignment ;\n    saas:assignedTo ex:membership-yamada-abc ;\n    saas:assignedRole ex:role-workspace-editor ;\n    saas:appliesToWorkspace ex:workspace-sales ;\n    saas:assignedAt \"2026-08-01T09:05:00+09:00\"^^xsd:dateTime .\n\nex:role-assignment-yamada-development-viewer a saas:RoleAssignment ;\n    saas:assignedTo ex:membership-yamada-abc ;\n    saas:assignedRole ex:role-workspace-viewer ;\n    saas:appliesToWorkspace ex:workspace-development ;\n    saas:assignedAt \"2026-08-01T09:10:00+09:00\"^^xsd:dateTime .\n\nex:role-assignment-sato-development-editor a saas:RoleAssignment ;\n    saas:assignedTo ex:membership-sato-abc ;\n    saas:assignedRole ex:role-workspace-editor ;\n    saas:appliesToWorkspace ex:workspace-development ;\n    saas:assignedAt \"2026-08-01T10:00:00+09:00\"^^xsd:dateTime .\n</code></pre><hr><h1 id=\"21-resource%E3%81%AE%E5%85%B7%E4%BD%93%E4%BE%8B\">21. Resourceの具体例</h1><p>営業Workspaceには顧客と商談があり、開発Workspaceには設計書があるとします。</p><pre><code class=\"language-turtle\">ex:customer-xyz a saas:Resource ;\n    saas:name \"株式会社XYZ\" ;\n    saas:belongsToWorkspace ex:workspace-sales .\n\nex:deal-core-system-renewal a saas:Resource ;\n    saas:name \"基幹システム刷新案件\" ;\n    saas:belongsToWorkspace ex:workspace-sales .\n\nex:document-system-architecture a saas:Resource ;\n    saas:name \"システム構成設計書\" ;\n    saas:belongsToWorkspace ex:workspace-development .\n</code></pre><p>このデータから、</p><pre><code class=\"language-text\">株式会社XYZ\n→ 営業Workspace\n→ Tenant ABC\n</code></pre><p>という関係をたどれます。</p><hr><h1 id=\"22-%E5%85%A8%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%81%AE%E9%96%A2%E4%BF%82%E5%9B%B3\">22. 全インスタンスの関係図</h1><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph TD\n    O[株式会社ABC<br>Organization]\n    T[Tenant ABC]\n\n    W1[営業Workspace]\n    W2[開発Workspace]\n\n    TM1[営業Team]\n    TM2[開発Team]\n    TM3[プロジェクトAlpha Team]\n\n    U1[山田太郎<br>User]\n    U2[佐藤花子<br>User]\n\n    M1[山田Membership]\n    M2[佐藤Membership]\n\n    RA1[Tenant Admin Assignment]\n    RA2[Sales Editor Assignment]\n    RA3[Development Viewer Assignment]\n\n    R1[Tenant Administrator]\n    R2[Workspace Editor]\n    R3[Workspace Viewer]\n\n    C[株式会社XYZ<br>Resource]\n    D[システム構成設計書<br>Resource]\n\n    O -->|usesTenant| T\n\n    T -->|containsWorkspace| W1\n    T -->|containsWorkspace| W2\n\n    T -->|containsTeam| TM1\n    T -->|containsTeam| TM2\n    T -->|containsTeam| TM3\n\n    U1 --> M1\n    U2 --> M2\n\n    M1 --> T\n    M2 --> T\n\n    M1 --> TM1\n    M1 --> TM3\n    M2 --> TM2\n    M2 --> TM3\n\n    TM1 -->|canAccess| W1\n    TM2 -->|canAccess| W2\n    TM3 -->|canAccess| W1\n    TM3 -->|canAccess| W2\n\n    M1 --> RA1\n    M1 --> RA2\n    M1 --> RA3\n\n    RA1 --> R1\n    RA1 --> T\n\n    RA2 --> R2\n    RA2 --> W1\n\n    RA3 --> R3\n    RA3 --> W2\n\n    W1 --> C\n    W2 --> D\n</pre><!--kg-card-end: html--><p></p><hr><h1 id=\"23-sparql%E3%81%A7%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B\">23. SPARQLで問い合わせる</h1><p>オントロジーができたら、どのような質問に答えられるかを確認します。</p><h2 id=\"231-tenant%E3%81%8C%E6%8C%81%E3%81%A4workspace%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B\">23.1 Tenantが持つWorkspaceを取得する</h2><pre><code class=\"language-sparql\">PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\n\nSELECT ?workspace ?workspaceName\nWHERE {\n    &lt;https://example.com/data/tenant-abc&gt;\n        saas:containsWorkspace ?workspace .\n\n    ?workspace saas:name ?workspaceName .\n}\n</code></pre><p>結果のイメージは次のとおりです。</p><pre><code class=\"language-text\">営業Workspace\n開発Workspace\n管理Workspace\n</code></pre><hr><h2 id=\"232-%E5%B1%B1%E7%94%B0%E5%A4%AA%E9%83%8E%E3%81%8C%E6%89%80%E5%B1%9E%E3%81%99%E3%82%8Bteam%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B\">23.2 山田太郎が所属するTeamを取得する</h2><pre><code class=\"language-sparql\">PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\nPREFIX ex:   &lt;https://example.com/data/&gt;\n\nSELECT ?team ?teamName\nWHERE {\n    ex:membership-yamada-abc\n        saas:memberOfTeam ?team .\n\n    ?team saas:name ?teamName .\n}\n</code></pre><p>結果は次のようになります。</p><pre><code class=\"language-text\">営業Team\nプロジェクトAlpha Team\n</code></pre><hr><h2 id=\"233-%E5%B1%B1%E7%94%B0%E5%A4%AA%E9%83%8E%E3%81%8Cteam%E7%B5%8C%E7%94%B1%E3%81%A7%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%A7%E3%81%8D%E3%82%8Bworkspace\">23.3 山田太郎がTeam経由でアクセスできるWorkspace</h2><pre><code class=\"language-sparql\">PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\nPREFIX ex:   &lt;https://example.com/data/&gt;\n\nSELECT DISTINCT ?workspace ?workspaceName\nWHERE {\n    ex:membership-yamada-abc\n        saas:memberOfTeam ?team .\n\n    ?team saas:canAccessWorkspace ?workspace .\n\n    ?workspace saas:name ?workspaceName .\n}\n</code></pre><p>結果のイメージは次のとおりです。</p><pre><code class=\"language-text\">営業Workspace\n開発Workspace\n</code></pre><p>営業Teamから営業Workspaceへアクセスでき、プロジェクトAlpha Teamから営業・開発の両Workspaceへアクセスできるためです。</p><hr><h2 id=\"234-%E5%B1%B1%E7%94%B0%E5%A4%AA%E9%83%8E%E3%81%8C%E5%96%B6%E6%A5%ADworkspace%E3%81%A7%E6%8C%81%E3%81%A4role\">23.4 山田太郎が営業Workspaceで持つRole</h2><pre><code class=\"language-sparql\">PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\nPREFIX ex:   &lt;https://example.com/data/&gt;\n\nSELECT ?role ?roleName\nWHERE {\n    ?assignment a saas:RoleAssignment ;\n        saas:assignedTo ex:membership-yamada-abc ;\n        saas:appliesToWorkspace ex:workspace-sales ;\n        saas:assignedRole ?role .\n\n    ?role saas:name ?roleName .\n}\n</code></pre><p>結果は次のようになります。</p><pre><code class=\"language-text\">Workspace Editor\n</code></pre><hr><h2 id=\"235-%E5%B1%B1%E7%94%B0%E5%A4%AA%E9%83%8E%E3%81%8C%E5%96%B6%E6%A5%ADworkspace%E3%81%A7%E5%88%A9%E7%94%A8%E3%81%A7%E3%81%8D%E3%82%8Bpermission\">23.5 山田太郎が営業Workspaceで利用できるPermission</h2><pre><code class=\"language-sparql\">PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\nPREFIX ex:   &lt;https://example.com/data/&gt;\n\nSELECT DISTINCT ?permission ?permissionName\nWHERE {\n    ?assignment a saas:RoleAssignment ;\n        saas:assignedTo ex:membership-yamada-abc ;\n        saas:appliesToWorkspace ex:workspace-sales ;\n        saas:assignedRole ?role .\n\n    ?role saas:grantsPermission ?permission .\n\n    ?permission saas:name ?permissionName .\n}\nORDER BY ?permissionName\n</code></pre><p>結果のイメージは次のとおりです。</p><pre><code class=\"language-text\">resource.create\nresource.read\nresource.update\nworkspace.read\n</code></pre><hr><h2 id=\"236-resource%E3%81%8C%E5%B1%9E%E3%81%99%E3%82%8Btenant%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B\">23.6 Resourceが属するTenantを取得する</h2><p>Resourceは直接Tenantを参照せず、Workspaceを経由しています。</p><pre><code class=\"language-sparql\">PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\nPREFIX ex:   &lt;https://example.com/data/&gt;\n\nSELECT ?tenant ?tenantName\nWHERE {\n    ex:customer-xyz\n        saas:belongsToWorkspace ?workspace .\n\n    ?workspace\n        saas:workspaceBelongsToTenant ?tenant .\n\n    ?tenant saas:name ?tenantName .\n}\n</code></pre><p>結果は次のようになります。</p><pre><code class=\"language-text\">株式会社ABCテナント\n</code></pre><hr><h1 id=\"24-%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%83%81%E3%82%A7%E3%83%BC%E3%83%B3%E3%81%AB%E3%82%88%E3%82%8B%E6%8E%A8%E8%AB%96\">24. プロパティチェーンによる推論</h1><p>OWLには、複数の関係をつなぎ、新しい関係を推論するプロパティチェーンがあります。</p><p>たとえば、</p><pre><code class=\"language-text\">Resource belongsToWorkspace Workspace\nWorkspace workspaceBelongsToTenant Tenant\n</code></pre><p>という2つの関係から、</p><pre><code class=\"language-text\">Resource resourceBelongsToTenant Tenant\n</code></pre><p>を推論できます。</p><p>Turtleでは、次のように定義できます。</p><pre><code class=\"language-turtle\">saas:resourceBelongsToTenant a owl:ObjectProperty ;\n    rdfs:domain saas:Resource ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"resource belongs to tenant\"@en ,\n               \"リソースがテナントに属する\"@ja ;\n    owl:propertyChainAxiom (\n        saas:belongsToWorkspace\n        saas:workspaceBelongsToTenant\n    ) .\n</code></pre><p>これにより、推論エンジンが動作する環境では、明示的に次を書かなくても、</p><pre><code class=\"language-turtle\">ex:customer-xyz\n    saas:resourceBelongsToTenant ex:tenant-abc .\n</code></pre><p>という関係を導出できます。</p><p>同じように、Membershipが所属するTeamから、そのMembershipがアクセス可能なWorkspaceを推論することもできます。</p><pre><code class=\"language-turtle\">saas:membershipCanAccessWorkspace a owl:ObjectProperty ;\n    rdfs:domain saas:Membership ;\n    rdfs:range saas:Workspace ;\n    owl:propertyChainAxiom (\n        saas:memberOfTeam\n        saas:canAccessWorkspace\n    ) .\n</code></pre><p>すると、</p><pre><code class=\"language-text\">Membership\n→ memberOfTeam\n→ Team\n→ canAccessWorkspace\n→ Workspace\n</code></pre><p>から、</p><pre><code class=\"language-text\">Membership\n→ membershipCanAccessWorkspace\n→ Workspace\n</code></pre><p>を導出できます。</p><hr><h1 id=\"25-%E6%8E%A8%E8%AB%96%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%93%E3%81%A8%E3%81%A8%E3%80%81%E8%A8%B1%E5%8F%AF%E3%81%97%E3%81%A6%E3%82%88%E3%81%84%E3%81%93%E3%81%A8%E3%81%AF%E9%81%95%E3%81%86\">25. 推論できることと、許可してよいことは違う</h1><p>ここで注意が必要です。オントロジー上、</p><pre><code class=\"language-text\">山田太郎のMembership\n→ Team経由で営業Workspaceにアクセス可能\n</code></pre><p>と推論できたとしても、それだけで実際のアクセスを許可してよいとは限りません。</p><p>実際の認可では、さらに次の条件が必要になることがあります。</p><ul><li>Membershipが有効である</li><li>Teamが有効である</li><li>Workspaceが停止されていない</li><li>RoleAssignmentの有効期限が切れていない</li><li>必要なPermissionを持っている</li><li>契約上その機能が利用可能である</li><li>Resource自体に追加制限がない</li><li>IPアドレス制限に違反していない</li><li>MFAを完了している</li></ul><p>つまり、</p><pre><code class=\"language-text\">関係が存在する\n</code></pre><p>ことと、</p><pre><code class=\"language-text\">その操作を今この瞬間に許可する\n</code></pre><p>ことは別です。</p><p>オントロジーは認可判断の材料を整理できますが、認可エンジンそのものとは限りません。この境界を曖昧にすると、便利そうな知識グラフが、いつの間にか危険なアクセス制御システムになってしまいます。</p><hr><h1 id=\"26-shacl%E3%81%AB%E3%82%88%E3%82%8B%E6%9C%80%E4%BD%8E%E9%99%90%E3%81%AE%E6%A4%9C%E8%A8%BC\">26. SHACLによる最低限の検証</h1><p>OWLは意味や推論を表現するのに適していますが、入力データの妥当性検証にはSHACLが向いています。</p><p>たとえば、Workspaceには必ず1つのTenantが必要だとします。</p><pre><code class=\"language-turtle\">@prefix sh:   &lt;http://www.w3.org/ns/shacl#&gt; .\n@prefix saas: &lt;https://example.com/ontology/saas#&gt; .\n@prefix xsd:  &lt;http://www.w3.org/2001/XMLSchema#&gt; .\n\nsaas:WorkspaceShape a sh:NodeShape ;\n    sh:targetClass saas:Workspace ;\n\n    sh:property [\n        sh:path saas:workspaceBelongsToTenant ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Tenant ;\n        sh:message\n            \"Workspaceは必ず1つのTenantに所属しなければなりません。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:name ;\n        sh:minCount 1 ;\n        sh:datatype xsd:string ;\n        sh:message\n            \"Workspaceには名称が必要です。\"@ja\n    ] .\n</code></pre><p>Membershipにも、UserとTenantがそれぞれ1つ必要です。</p><pre><code class=\"language-turtle\">saas:MembershipShape a sh:NodeShape ;\n    sh:targetClass saas:Membership ;\n\n    sh:property [\n        sh:path saas:belongsToUser ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:User ;\n        sh:message\n            \"Membershipは必ず1つのUserに属する必要があります。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:membershipBelongsToTenant ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Tenant ;\n        sh:message\n            \"Membershipは必ず1つのTenantに属する必要があります。\"@ja\n    ] .\n</code></pre><p>RoleAssignmentには、割当て対象とRoleが必要です。</p><pre><code class=\"language-turtle\">saas:RoleAssignmentShape a sh:NodeShape ;\n    sh:targetClass saas:RoleAssignment ;\n\n    sh:property [\n        sh:path saas:assignedTo ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Membership\n    ] ;\n\n    sh:property [\n        sh:path saas:assignedRole ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Role\n    ] ;\n\n    sh:xone (\n        [\n            sh:property [\n                sh:path saas:appliesToTenant ;\n                sh:minCount 1 ;\n                sh:maxCount 1\n            ] ;\n            sh:property [\n                sh:path saas:appliesToWorkspace ;\n                sh:maxCount 0\n            ]\n        ]\n        [\n            sh:property [\n                sh:path saas:appliesToWorkspace ;\n                sh:minCount 1 ;\n                sh:maxCount 1\n            ] ;\n            sh:property [\n                sh:path saas:appliesToTenant ;\n                sh:maxCount 0\n            ]\n        ]\n    ) .\n</code></pre><p>このShapeでは、RoleAssignmentの適用先を、</p><pre><code class=\"language-text\">TenantまたはWorkspaceのどちらか一方\n</code></pre><p>に限定しています。</p><p>両方を同時に指定したり、どちらも指定しなかったりすると、検証エラーになります。</p><hr><h1 id=\"27-team%E3%81%A8workspace%E3%81%8C%E5%90%8C%E3%81%98tenant%E3%81%AB%E5%B1%9E%E3%81%99%E3%82%8B%E3%81%8B%E3%82%92%E6%A4%9C%E8%A8%BC%E3%81%99%E3%82%8B\">27. TeamとWorkspaceが同じTenantに属するかを検証する</h1><p>TeamからWorkspaceへのアクセスを設定する場合、両者が同じTenantに所属していることを確認したくなります。たとえば、Tenant AのTeamが、誤ってTenant BのWorkspaceへアクセスできてはいけません。</p><pre><code class=\"language-text\">Tenant A\n└── Team A\n\nTenant B\n└── Workspace B\n\nTeam A canAccess Workspace B\n</code></pre><p>これは、典型的なクロステナント不整合であり、単純なRDFSだけでは十分に検出できません。SHACL-SPARQLを使うと、次のような制約を書けます。</p><pre><code class=\"language-turtle\">saas:TeamWorkspaceTenantConsistencyShape\n    a sh:NodeShape ;\n    sh:targetClass saas:Team ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"TeamがアクセスするWorkspaceは、Teamと同じTenantに所属する必要があります。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\n\n            SELECT $this\n            WHERE {\n                $this\n                    saas:teamBelongsToTenant ?teamTenant ;\n                    saas:canAccessWorkspace ?workspace .\n\n                ?workspace\n                    saas:workspaceBelongsToTenant ?workspaceTenant .\n\n                FILTER (?teamTenant != ?workspaceTenant)\n            }\n        \"\"\"\n    ] .\n</code></pre><p>この制約は、B2B SaaSでは非常に重要です。マルチテナントシステムで最も避けるべき問題の一つが、他TenantのデータやWorkspaceを誤って参照することだからです。</p><hr><h1 id=\"28-membership%E3%81%A8team%E3%81%AEtenant%E6%95%B4%E5%90%88%E6%80%A7\">28. MembershipとTeamのTenant整合性</h1><p>Membershipが所属するTeamも、同じTenantに属している必要があります。</p><pre><code class=\"language-turtle\">saas:MembershipTeamTenantConsistencyShape\n    a sh:NodeShape ;\n    sh:targetClass saas:Membership ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"Membershipが所属するTeamは、Membershipと同じTenantに属する必要があります。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\n\n            SELECT $this\n            WHERE {\n                $this\n                    saas:membershipBelongsToTenant ?membershipTenant ;\n                    saas:memberOfTeam ?team .\n\n                ?team\n                    saas:teamBelongsToTenant ?teamTenant .\n\n                FILTER (?membershipTenant != ?teamTenant)\n            }\n        \"\"\"\n    ] .\n</code></pre><p>このようなTenant整合性制約は、今後追加するほぼすべての概念で必要になります。</p><ul><li>Membership</li><li>Team</li><li>Workspace</li><li>Resource</li><li>RoleAssignment</li><li>API Client</li><li>Audit Event</li><li>Usage Record</li><li>Entitlement</li></ul><p>B2B SaaSのオントロジーでは、各概念の意味だけでなく、</p><pre><code class=\"language-text\">そのインスタンスがどのTenant文脈に属するか\n</code></pre><p>を常に意識する必要があります。</p><hr><h1 id=\"29-%E4%BB%8A%E5%9B%9E%E3%81%AE%E6%9C%80%E5%B0%8F%E9%96%A2%E4%BF%82%E4%B8%80%E8%A6%A7\">29. 今回の最小関係一覧</h1><!--kg-card-begin: html--><table data-line=\"1903\" class=\"code-line\" dir=\"auto\" style=\"border-collapse: collapse; margin-bottom: 0.7em;\"><thead data-line=\"1903\" class=\"code-line\" dir=\"auto\"><tr data-line=\"1903\" class=\"code-line\" dir=\"auto\"><th style=\"text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.69); padding: 5px 10px; border-top-color: rgba(255, 255, 255, 0.69); border-right-color: rgba(255, 255, 255, 0.69); border-left-color: rgba(255, 255, 255, 0.69);\">主語</th><th style=\"text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.69); padding: 5px 10px; border-top-color: rgba(255, 255, 255, 0.69); border-right-color: rgba(255, 255, 255, 0.69); border-left-color: rgba(255, 255, 255, 0.69);\">関係</th><th style=\"text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.69); padding: 5px 10px; border-top-color: rgba(255, 255, 255, 0.69); border-right-color: rgba(255, 255, 255, 0.69); border-left-color: rgba(255, 255, 255, 0.69);\">目的語</th><th style=\"text-align: left; border-bottom: 1px solid rgba(255, 255, 255, 0.69); padding: 5px 10px; border-top-color: rgba(255, 255, 255, 0.69); border-right-color: rgba(255, 255, 255, 0.69); border-left-color: rgba(255, 255, 255, 0.69);\">意味</th></tr></thead><tbody data-line=\"1905\" class=\"code-line\" dir=\"auto\"><tr data-line=\"1905\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Organization</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">hasSubOrganization</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Organization</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">下位組織を持つ</td></tr><tr data-line=\"1906\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Organization</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">usesTenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Tenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">組織がTenantを利用する</td></tr><tr data-line=\"1907\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Tenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">containsWorkspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Workspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">TenantがWorkspaceを含む</td></tr><tr data-line=\"1908\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Workspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">workspaceBelongsToTenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Tenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Workspaceの所属Tenant</td></tr><tr data-line=\"1909\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Tenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">containsTeam</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Team</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">TenantがTeamを含む</td></tr><tr data-line=\"1910\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Team</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">teamBelongsToTenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Tenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Teamの所属Tenant</td></tr><tr data-line=\"1911\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Membership</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">memberOfTeam</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Team</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">MembershipがTeamに参加する</td></tr><tr data-line=\"1912\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Team</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">canAccessWorkspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Workspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">TeamがWorkspaceにアクセスする</td></tr><tr data-line=\"1913\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Workspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">containsResource</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Resource</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">WorkspaceがResourceを含む</td></tr><tr data-line=\"1914\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Resource</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">belongsToWorkspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Workspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Resourceの所属Workspace</td></tr><tr data-line=\"1915\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Membership</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">hasRoleAssignment</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">RoleAssignment</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">MembershipにRole付与がある</td></tr><tr data-line=\"1916\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">RoleAssignment</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">assignedRole</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Role</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">付与されるRole</td></tr><tr data-line=\"1917\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">RoleAssignment</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">appliesToTenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Tenant</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">RoleのTenant適用範囲</td></tr><tr data-line=\"1918\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">RoleAssignment</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">appliesToWorkspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Workspace</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">RoleのWorkspace適用範囲</td></tr><tr data-line=\"1919\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Role</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">grantsPermission</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">Permission</td><td style=\"padding: 5px 10px; border-top: 1px solid rgba(255, 255, 255, 0.18); border-right-color: rgba(255, 255, 255, 0.18); border-bottom-color: rgba(255, 255, 255, 0.18); border-left-color: rgba(255, 255, 255, 0.18);\">RoleがPermissionを付与する</td></tr></tbody></table><!--kg-card-end: html--><p>最小関係なのに、すごい量になっちゃいましたね。。。</p><hr><h1 id=\"30-%E4%BB%8A%E5%9B%9E%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E3%81%A7%E7%AD%94%E3%81%88%E3%82%89%E3%82%8C%E3%82%8B%E8%B3%AA%E5%95%8F\">30. 今回のモデルで答えられる質問</h1><p>Organization、Workspace、Team、RoleAssignmentを追加したことで、次のような質問に答えられます。</p><h2 id=\"organization%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%B3%AA%E5%95%8F\">Organizationに関する質問</h2><pre><code class=\"language-text\">株式会社ABCは、どのTenantを利用しているか。\n\nこのTenantを利用するOrganizationはどれか。\n\n営業本部の上位Organizationは何か。\n\n株式会社ABCの下位Organizationは何か。\n</code></pre><h2 id=\"workspace%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%B3%AA%E5%95%8F\">Workspaceに関する質問</h2><pre><code class=\"language-text\">Tenant ABCには、どのWorkspaceがあるか。\n\n営業Workspaceには、どのResourceがあるか。\n\nこのResourceは、最終的にどのTenantに属するか。\n</code></pre><h2 id=\"team%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%B3%AA%E5%95%8F\">Teamに関する質問</h2><pre><code class=\"language-text\">山田太郎は、どのTeamに参加しているか。\n\nプロジェクトAlpha Teamには、誰が所属しているか。\n\nこのTeamは、どのWorkspaceへアクセスできるか。\n</code></pre><h2 id=\"role%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%B3%AA%E5%95%8F\">Roleに関する質問</h2><pre><code class=\"language-text\">山田太郎は、営業WorkspaceでどのRoleを持つか。\n\n山田太郎は、開発WorkspaceでどのPermissionを持つか。\n\nTenant全体の管理者は誰か。\n\nこのRoleAssignmentは、いつ付与されたか。\n</code></pre><p>この、「質問に答えられるようになる」というところが、オントロジーのすごいところですね！</p><hr><h1 id=\"31-%E4%BB%8A%E5%9B%9E%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E3%81%BE%E3%81%A8%E3%82%81\">31. 今回のモデルのまとめ</h1><p>今回の内容を最も単純にまとめると、次のようになります。</p><pre><code class=\"language-text\">Organizationは、現実世界の組織である。\n\nTenantは、SaaS上の契約・管理・分離境界である。\n\nWorkspaceは、Tenant内の共同作業空間である。\n\nTeamは、Tenant内のMembershipの集合である。\n\nResourceは、Workspaceに属する。\n\nRoleAssignmentは、誰に、どのRoleを、\nどの範囲で付与したかを表す。\n</code></pre><p>図にすると、次の形です。</p><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph LR\n    Organization --> Tenant\n    Tenant --> Workspace\n    Workspace --> Resource\n\n    User --> Membership\n    Membership --> Tenant\n    Membership --> Team\n    Team --> Workspace\n\n    Membership --> RoleAssignment\n    RoleAssignment --> Role\n    RoleAssignment --> Tenant\n    RoleAssignment --> Workspace\n    Role --> Permission\n\n</pre><!--kg-card-end: html--><hr><h1 id=\"%E6%AC%A1%E5%9B%9E%E4%BA%88%E5%91%8A\">次回予告</h1><p>ここまでで、</p><pre><code class=\"language-text\">誰がTenantに所属しているか\n\n誰がどのWorkspaceへアクセスできるか\n\n誰がどのRoleとPermissionを持つか\n</code></pre><p>を表現できるようになりました。</p><p>しかし、まだ別の問題が残っています。</p><p>たとえば、山田太郎さんが<code>analytics.execute</code>というPermissionを持っているとしても、契約しているPlanがAnalytics機能を含んでいなければ、その機能は使えません。</p><pre><code class=\"language-text\">Permissionがある\nしかし\n契約上、その機能が提供されていない\n</code></pre><p>反対に、Tenantの契約上は機能が提供されていても、山田太郎さんにPermissionがなければ利用できません。</p><pre><code class=\"language-text\">契約上は利用可能\nしかし\nそのUserには操作権限がない\n</code></pre><p>ここには、異なる2種類の「できる」があります。</p><pre><code class=\"language-text\">Permission\n＝ そのUserが操作してよいか\n\nEntitlement\n＝ そのTenantに機能が提供されているか\n</code></pre><p>次回は、次の概念を追加します。</p><pre><code class=\"language-text\">Feature\nEntitlement\nLimit\nQuota\nUsage\n</code></pre><p>第3回テーマは、</p><p><strong>権限があっても使えない？ FeatureとEntitlementで「契約上使える機能」を表現する</strong></p><p>にしようかなと思っております。</p><p>RoleとPermissionが人を中心とした認可を表すのに対し、FeatureとEntitlementはTenantを中心とした提供条件を表します。この2つを分けることで、料金プラン、オプション機能、利用上限、無料トライアル、β機能、個別契約をきれいに表現できるようになります。</p><p>いきなり第2回から、モリモリの内容ですが、ぜひこの後も一緒に勉強していきましょう！</p><p>まだまだ、第10回くらいまで続く予定ですので、お楽しみに！</p><p>アディオス！</p>","url":"https://ghost.tech.anti-pattern.co.jp/saas-ontology-2/","canonical_url":null,"uuid":"8f3ebabc-342d-4f8b-b60a-74319a19751e","page":null,"codeinjection_foot":null,"codeinjection_head":null,"codeinjection_styles":null,"comment_id":"6a8ebf76d3fc9f00013c1f86","reading_time":27}},"pageContext":{"slug":"saas-ontology-2"}},
    "staticQueryHashes": ["176528973","2358152166","2561578252","2731221146","4145280475"]}