{
    "componentChunkName": "component---src-templates-post-js",
    "path": "/saas-ontology-3/",
    "result": {"data":{"ghostPost":{"id":"Ghost__Post__6aaa464fd3fc9f00013c1fda","title":"SaaSを題材にしたオントロジーを作ってみようシリーズ #3 どの機能が使えるのか？","slug":"saas-ontology-3","featured":false,"feature_image":"https://ghost.tech.anti-pattern.co.jp/content/images/2026/09/----------2026-09-16-18.47.30.png","excerpt":"\nオッス！おらやぎぃ！\n\n第3回目になりました！\n\nここまで読んでいただいている方は、きっとSaaSというものの構造についてかなり詳しくなっているのではないでしょうか？！\n\n第2回は、テナントをベースとしたさらなる深掘りをしてみました。\n\n第3回は、そのテナントがどんな機能を使えるのか？使っても良いのか？というのを考えていきたいと思います。\n\nよくあるSaaSは、料金プランやオプションなどがあって、契約しているプランによって、テナントごとに使える機能や量が決まっていたりすると思います。それをどのように表現していくのか？を考えていきたいと思います。\n\n0. 権限があっても使えない？\nFeatureとEntitlementで「契約上使える機能」を表現する\n前回までに、B2B SaaSの利用構造と権限構造を作りました。\n\nOrganization\n└── usesTenant\n    └── Tenant\n        ├── Membership\n        │   └── User\n        │\n        ├── Workspace\n        │   └── Re","custom_excerpt":null,"visibility":"public","created_at_pretty":"16 September, 2026","published_at_pretty":"16 September, 2026","updated_at_pretty":"16 September, 2026","created_at":"2026-09-16T16:33:35.000+09:00","published_at":"2026-09-16T18:47:53.000+09:00","updated_at":"2026-09-16T18:47:53.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オッス！おらやぎぃ！\n\n第3回目になりました！\n\nここまで読んでいただいている方は、きっとSaaSというものの構造についてかなり詳しくなっているのではないでしょうか？！\n\n第2回は、テナントをベースとしたさらなる深掘りをしてみました。\n\n第3回は、そのテナントがどんな機能を使えるのか？使っても良いのか？というのを考えていきたいと思います。\n\nよくあるSaaSは、料金プランやオプションなどがあって、契約しているプランによって、テナントごとに使える機能や量が決まっていたりすると思います。それをどのように表現していくのか？を考えていきたいと思います。\n\n0. 権限があっても使えない？\nFeatureとEntitlementで「契約上使える機能」を表現する\n前回までに、B2B SaaSの利用構造と権限構造を作りました。\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山田太郎は、株式会社ABCテナントに所属している。\n\n山田太郎は、営業WorkspaceのEditorである。\n\nEditor Roleは、resource.updateというPermissionを持つ。\n\n\nここまでを見ると、山田太郎さんがResourceを更新できるかどうかは、Permissionだけで判定できそうに見えますが、実際のB2B\nSaaSでは、それだけでは足りません。\n\nたとえば、SaaSに「AIによる文章生成機能」があるとします。\n\n山田太郎さんのRoleには、次のPermissionが含まれています。\n\nai.generate\n\n\nそれでも、株式会社ABCがAI機能を含まないStandard Planを契約していたら、AI文章生成は利用できないでしょう。\n\n山田太郎はAI生成を操作してよい。\n\nしかし、\n\n株式会社ABCにはAI生成機能が提供されていない。\n\n\n反対の状況もあります。\n\n株式会社ABCがAI機能を含むEnterprise Planを契約していたとしても、山田太郎さんにai.generate\nというPermissionがなければ、山田太郎さんはその機能を利用できません。\n\n株式会社ABCにはAI生成機能が提供されている。\n\nしかし、\n\n山田太郎にはAI生成を実行する権限がない。\n\n\nここには、異なる2種類の「できる」があります。\n\nPermission\n＝ その人が操作してよいか\n\nEntitlement\n＝ そのTenantに、その機能が提供されているか\n\n\n今回は、この違いを表現するために、次の2つの概念を追加します。\n\nFeature\nEntitlement\n\n\n\n--------------------------------------------------------------------------------\n\n1. まず、何が問題なのか\n多くのSaaSでは、料金プランによって利用できる機能が変わります。\n\nたとえば、次のような料金体系があるとします。\n\nFeatureFreeStandardEnterprise基本的なデータ管理○○○CSVエクスポート×○○APIアクセス×○○SAML SSO××○監査ログ××\n○AI文章生成×オプション○この表には、少なくとも次の3種類の概念が含まれています。\n\nPlan\nFeature\nTenantごとの利用可否\n\n\nPlanは、SaaS提供者が販売する標準商品です。\n\nStandard Plan\nEnterprise Plan\n\n\nFeatureは、SaaSが提供する機能です。\n\nCSVエクスポート\nAPIアクセス\nSAML SSO\n監査ログ\nAI文章生成\n\n\nそして、実際に特定のTenantがそのFeatureを利用できるかどうかを表すのがEntitlementです。\n\n株式会社ABCテナントは、\nAI文章生成Featureを利用できる。\n\n\nこの「利用できる」という関係を、単純なプロパティではなく、独立した概念として扱います。\n\n\n--------------------------------------------------------------------------------\n\n2. Featureとは何か\nFeatureを、次のように定義します。\n\nFeature\n＝ SaaSが提供可能な、識別可能な機能・能力\n\n\nFeatureの例には、次のようなものがあります。\n\n * CSVエクスポート\n * PDF出力\n * APIアクセス\n * SAML SSO\n * SCIM連携\n * 監査ログ\n * AI文章生成\n * 高度な検索\n * ワークフロー承認\n * カスタムRole\n * 外部共有\n * データ保持期間の設定\n * IPアドレス制限\n * 独自ドメイン\n * ホワイトラベル\n * 優先サポート\n\nFeatureは、「画面上のボタン」と同じとは限りません。たとえば、「SAML SSO」というFeatureには、複数の画面やAPIが関係するかもしれません。\n\nSAML SSO Feature\n├── IdPメタデータの登録\n├── SPメタデータの取得\n├── SSOテスト\n├── SSO強制設定\n└── 証明書の更新\n\n\n逆に、画面上に1つのボタンがあっても、それが独立したFeatureとして管理されるとは限りません。\n\nFeatureとは、単なるUI部品ではなく、\n\n商品・契約・提供制御の単位として識別したい能力\n\n\nです。\n\n\n--------------------------------------------------------------------------------\n\n3. FeatureはResourceなのか\nここで、少し立ち止まって考えてみます。\n\n前回までに、SaaS上の業務対象をResourceとして表現しました。\n\n顧客\n商談\nタスク\n設計書\n監査イベント\n\n\nでは、FeatureもResourceなのでしょうか。\n\nこの連載では、FeatureとResourceは別の概念として扱います。\n\nResource\n＝ TenantやWorkspaceが管理する業務データ\n\nFeature\n＝ SaaS提供者が提供する機能・能力\n\n\nたとえば、「顧客CSVエクスポート」というFeatureがあったとしても、CSVエクスポートそのものは顧客業務データではありません。\n\nCustomer\n＝ Resource\n\nCSV Export\n＝ Feature\n\n\nFeatureはResourceに対して何らかの操作を提供することがあります。\n\nCSV Export Feature\n→ Customer Resourceを出力する\n\n\nしかし、FeatureとResourceは役割が異なります。\n\n概念例主な所有・定義主体Resource顧客、商談、タスクTenant、WorkspaceFeatureCSV出力、SSO、AI生成SaaS提供者\n--------------------------------------------------------------------------------\n\n4. FeatureはPermissionなのか\nFeatureとPermissionも、よく混同されます。\n\nたとえば、\n\nFeature: CSVエクスポート\n\nPermission: customer.export\n\n\nは、とても似ています。\n\nしかし、意味は異なります。\n\nFeature\n＝ SaaSとして、その能力を提供できるか\n\nPermission\n＝ その利用者が、その操作をしてよいか\n\n\nFeatureは商品・契約側の概念であり、Permissionは認可・アクセス制御側の概念です。\n\nたとえば、株式会社ABCがCSVエクスポートFeatureを利用できるとしても、すべてのUserがCSVを出力できるとは限りません。\n\n株式会社ABCテナント\n→ CSVエクスポートFeatureが有効\n\n山田太郎\n→ customer.export Permissionを持つ\n\n佐藤花子\n→ customer.export Permissionを持たない\n\n\nこの場合、山田太郎さんはCSVを出力できますが、佐藤花子さんは出力できません。\n\nつまり、一般的な操作可否は次のように考えられます。\n\n操作可能\n＝\nTenantがFeatureを利用可能\nかつ\n利用者がPermissionを持つ\n\n\nより厳密には、MembershipやRoleAssignmentの有効状態、適用Scopeなども確認します。\n\n操作可能\n＝\n有効なMembershipがある\nかつ\n対象Scopeで必要なPermissionを持つ\nかつ\n対象TenantにFeatureの有効なEntitlementがある\n\n\n\n--------------------------------------------------------------------------------\n\n5. FeatureはPlanに直接結び付ければよいのか\n単純な料金体系であれば、PlanとFeatureを直接結び付けることもできます。\n\nEnterprise Plan\n→ includesFeature\n→ SAML SSO\n\n\nこの構造は自然です。\n\n\ngraph LR\n    P[Enterprise Plan]\n    F[SAML SSO Feature]\n\n    P -->|includesFeature| F\n\n\n\nしかし、これだけでは、実際のTenantごとの状態を十分に表現できません。\n\nたとえば、次のようなケースがあります。\n\n * Standard Planだが、AI機能を追加契約している\n * Enterprise Planだが、監査ログだけ一時的に停止されている\n * 特定Tenantだけβ機能を提供している\n * 障害対応として一時的に上位機能を開放した\n * 契約交渉により、通常のPlanにない機能を個別提供した\n * 無料トライアル期間中だけSAML SSOを使える\n * 旧料金プランのTenantだけ、廃止済み機能を継続利用できる\n * 契約は有効だが、法務確認が終わるまでAI機能を無効化している\n\nこうした状態は、\n\nPlan includesFeature Feature\n\n\nだけでは表現できません。\n\nPlanは標準商品ですが、実際にTenantへ何が提供されているかは、個別契約や運用状況によって変わります。\n\nそこで、Entitlementという概念を導入します。\n\n\n--------------------------------------------------------------------------------\n\n6. Entitlementとは何か\nEntitlementは、日本語に訳しにくい言葉です。直訳すると、\n\n * 権利\n * 資格\n * 利用権\n * 受給資格\n * 正当な権原\n\nなどになります。\n\nB2B SaaSの文脈では、次のように定義すると分かりやすいでしょう。\n\nEntitlement\n＝ 特定のTenantに、特定のFeatureを提供する権利・資格・状態\n\n\nもう少し平たく言えば、\n\nこのTenantは、このFeatureを使える\n\n\nという事実です。\n\nたとえば、\n\n株式会社ABCテナントは、\nCSVエクスポートFeatureを利用できる。\n\n\nという事実を、Entitlementとして表します。\n\nEntitlement\n├── beneficiary: 株式会社ABCテナント\n├── feature: CSVエクスポート\n└── status: active\n\n\nここではbeneficiaryを「権利を受ける主体」という意味で使っています。\n\nこの連載では、最初はEntitlementの対象をTenantに限定します。\n\nEntitlementの対象\n＝ Tenant\n\n\n将来的には、Organization、Workspace、User、API\nClientなどを対象にする可能性もありますが、最初から広げると意味が曖昧になります。\n\nまずは、\n\nTenantに対するFeature提供\n\n\nとして固定します。\n\n\n--------------------------------------------------------------------------------\n\n7. なぜEntitlementを中間概念にするのか\n単純に次の関係を使うこともできます。\n\nTenant canUseFeature Feature\n\n\nたとえば、\n\nex:tenant-abc\n    saas:canUseFeature ex:feature-csv-export .\n\n\nこれは簡単で、分かりやすい表現です。\n\nしかし、実際のFeature提供には、さまざまな付随情報があります。\n\n * 有効か無効か\n * いつから有効か\n * いつまで有効か\n * どの契約に基づくか\n * どのPlanから継承されたか\n * 追加オプションとして付与されたか\n * 手動で付与されたか\n * 無料トライアルなのか\n * β提供なのか\n * 誰が有効化したか\n * なぜ停止されたか\n\n単純な、\n\nTenant canUseFeature Feature\n\n\nという関係だけでは、これらを表現できません。\n\nそこで、関係そのものを独立した概念にします。\n\nTenant\n→ Entitlement\n→ Feature\n\n\nこれは、前回までに登場したMembershipやRoleAssignmentと同じ考え方です。\n\nUser\n→ Membership\n→ Tenant\n\nMembership\n→ RoleAssignment\n→ Role\n\nTenant\n→ Entitlement\n→ Feature\n\n\n関係に属性や履歴が必要になったら、関係を独立した概念にする。これは、B2B SaaSオントロジーでは何度も登場する重要なパターンです。\n\n\n--------------------------------------------------------------------------------\n\n8. FeatureとEntitlementの関係\nFeatureは、SaaS提供者が定義します。\n\nEntitlementは、Tenantごとに発生します。\n\nFeature\n＝ 提供可能な機能の定義\n\nEntitlement\n＝ 特定Tenantへの提供状態\n\n\nたとえば、SaaS全体には1つの「SAML SSO Feature」が存在します。\n\nFeature:\nSAML SSO\n\n\nこのFeatureに対して、Tenantごとに別々のEntitlementが作られます。\n\nEntitlement 1\n├── Tenant: 株式会社ABC\n├── Feature: SAML SSO\n└── status: active\n\nEntitlement 2\n├── Tenant: 株式会社XYZ\n├── Feature: SAML SSO\n└── status: inactive\n\nEntitlement 3\n├── Tenant: 株式会社DEF\n├── Feature: SAML SSO\n└── status: trial\n\n\nFeatureは1つですが、EntitlementはTenantごとに異なります。\n\n\ngraph TD\n    F[SAML SSO\nFeature]\n\n    E1[ABC SSO Entitlement\nactive]\n    E2[XYZ SSO Entitlement\ninactive]\n    E3[DEF SSO Entitlement\ntrial]\n\n    T1[株式会社ABC\nTenant]\n    T2[株式会社XYZ\nTenant]\n    T3[株式会社DEF\nTenant]\n\n    T1 --> E1\n    E1 --> F\n\n    T2 --> E2\n    E2 --> F\n\n    T3 --> E3\n    E3 --> F\n\n\n\n\n--------------------------------------------------------------------------------\n\n9. Plan・Subscription・Entitlementの違い\nここで、Plan、Subscription、Entitlementの違いを整理します。\n\n9.1 Plan\nPlan\n＝ SaaS提供者が定義する標準商品\n\n\n例：\n\nFree\nStandard\nProfessional\nEnterprise\n\n\n9.2 Subscription\nSubscription\n＝ TenantによるPlanの個別契約\n\n\n例：\n\n株式会社ABCのEnterprise Plan契約\n\n\n9.3 Entitlement\nEntitlement\n＝ Tenantに対する個別Featureの提供状態\n\n\n例：\n\n株式会社ABCテナントに、\nSAML SSO Featureが提供されている。\n\n\n図にすると、次のようになります。\n\n\ngraph LR\n    T[Tenant]\n    S[Subscription]\n    P[Plan]\n    E[Entitlement]\n    F[Feature]\n\n    T -->|hasSubscription| S\n    S -->|basedOnPlan| P\n    T -->|hasEntitlement| E\n    E -->|entitlesFeature| F\n    E -->|derivedFrom| S\n\n\n\nPlanとFeatureを商品定義として結び付け、SubscriptionとEntitlementをTenantごとの実態として結び付けます。\n\n商品定義側\nPlan → Feature\n\nTenant個別側\nTenant → Subscription → Plan\nTenant → Entitlement → Feature\n\n\n\n--------------------------------------------------------------------------------\n\n10. PlanからFeatureを直接推論しない理由\nここは少し重要な設計判断です。\n\nたとえば、Enterprise PlanがSAML SSOを含むなら、\n\n株式会社ABC\n→ Enterprise Planを契約\n→ SAML SSOを利用可能\n\n\nと自動的に推論したくなります。\n\n概念的には自然ですが、実運用では注意が必要です。\n\nSubscriptionがEnterprise Planに基づいていたとしても、次のようなケースがあります。\n\n * Subscriptionがまだ開始前である\n * Subscriptionが停止中である\n * 支払い遅延により一部機能が停止されている\n * SAML SSOの初期設定が未完了である\n * 特定地域では提供対象外である\n * セキュリティ審査が完了していない\n * 管理者が明示的に機能を無効化した\n * 一時的な障害対応で停止している\n\nしたがって、\n\nPlanに含まれる\n\n\nことと、\n\n現在そのTenantで利用可能\n\n\nであることは、完全には同じではありません。\n\nこの連載では、次のように分けます。\n\nPlan includesFeature Feature\n＝ 商品設計上、そのPlanにFeatureが含まれる\n\nEntitlement status active\n＝ 特定Tenantに、現在そのFeatureが提供されている\n\n\n最終的な利用可否では、Entitlementを参照します。PlanはEntitlementを生成する根拠にはなりますが、Entitlementそのものではありません。\n\n\n--------------------------------------------------------------------------------\n\n11. 小休止：プランに含まれる朝食は実際に食べられるのか？\n少し横道にそれますが、PlanとEntitlementの違いは、ホテルの宿泊プランにたとえることができます。\n\nたとえば、「朝食付きプラン」という商品があります。\n\nPlan\n＝ 朝食付き宿泊プラン\n\n\nこのプランには、朝食というサービスが含まれています。\n\nPlan includesFeature 朝食\n\n\nしかし、実際の宿泊者が今朝、朝食を利用できるかどうかは、もう少し具体的な状態に依存します。\n\n * 宿泊日は今日なのか\n * 朝食券は有効か\n * 朝食時間内か\n * 予約がキャンセルされていないか\n * すでにチェックアウトしていないか\n\nメニューや商品説明に書かれていることと、個別の顧客が現時点で利用できることは違います。\n\nPlan\n＝ メニューや商品設計\n\nEntitlement\n＝ 個別顧客に発生している利用資格\n\n\nもちろん、SaaSはホテルではありませんが、\n\n商品に含まれていること\n\n\nと、\n\n個別顧客が現在利用できること\n\n\nを分ける感覚は、よく似ています。\n\n\n--------------------------------------------------------------------------------\n\n12. Featureの粒度をどう決めるか\nFeatureを設計するとき、難しいのは粒度です。\n\nたとえば、「監査ログ」を1つのFeatureにすることもできます。\n\naudit-log\n\n\n一方で、細かく分けることもできます。\n\naudit-log.view\naudit-log.export\naudit-log.retention\naudit-log.external-streaming\n\n\nどちらが正しいのでしょうか。絶対的な正解はありません。\n\nFeatureは、\n\n商品や提供条件として、独立して制御したい単位\n\n\nで切るのが基本です。\n\nたとえば、次の条件なら別Featureにする意味があります。\n\n * 別料金で販売する\n * Planによって提供可否が異なる\n * Tenantごとに有効・無効を切り替える\n * 段階的にリリースする\n * 法的・地域的な提供制限がある\n * β機能として一部顧客だけに提供する\n * 障害時に単独で停止したい\n\n逆に、常に一緒に提供され、単独で制御する必要がないものを細かく分けすぎると、Feature管理が複雑になります。\n\n細かすぎるFeature\n→ Entitlementが大量に増える\n→ 商品設計が読みにくくなる\n→ 判定ロジックが複雑になる\n\n\nFeatureは、画面の数やAPIの数ではなく、\n\n契約・商品・提供制御上の意味\n\n\nで切るのがよいでしょう。\n\n\n--------------------------------------------------------------------------------\n\n13. Featureに階層を持たせる\nFeatureには、親子関係を持たせられます。\n\nたとえば、「AI機能」という大きなFeatureの下に、個別機能があるとします。\n\nAI機能\n├── AI文章生成\n├── AI要約\n├── AI分類\n└── AI検索\n\n\nこれを次のように表現します。\n\nAI文章生成\nsubFeatureOf\nAI機能\n\n\n\ngraph TD\n    AI[AI機能\nFeature]\n    G[AI文章生成\nFeature]\n    S[AI要約\nFeature]\n    C[AI分類\nFeature]\n    R[AI検索\nFeature]\n\n    AI --> G\n    AI --> S\n    AI --> C\n    AI --> R\n\n\n\nただし、親FeatureへのEntitlementが、すべての子Featureを自動的に有効にするかどうかは、別途ルールを決める必要があります。\n\nAI機能を利用可能\n→ AI文章生成も利用可能\n\n\nと推論したい場合もあります。\n\nしかし、\n\nAI機能というカテゴリは有効だが、\nAI検索は別契約\n\n\nという商品設計もあり得ます。\n\nそのため、Feature階層は最初から利用可否の継承を意味するとは限りません。\n\nsubFeatureOf\n＝ 機能分類・構成上の親子関係\n\nEntitlementの継承\n＝ 別途定義するビジネスルール\n\n\nこの2つは分けて考えます。\n\n\n--------------------------------------------------------------------------------\n\n14. Entitlementの状態\nEntitlementには、状態を持たせます。最小限の例として、次の状態を考えます。\n\nactive\ninactive\ntrial\nsuspended\nexpired\n\n\nそれぞれの意味は、次のようになります。\n\n状態意味active通常利用可能inactive付与情報はあるが利用不可trial試用として利用可能suspended一時停止中expired有効期限切れ\nただし、trialを状態として扱うか、付与理由として扱うかには議論があります。\n\nたとえば、\n\nstatus: active\ngrantType: trial\n\n\nと分ける設計も可能です。\n\nこちらの方が、状態と付与経路を明確に分離できます。\n\nstatus\n＝ 現在有効かどうか\n\ngrantType\n＝ どのような理由・経路で付与されたか\n\n\n本格的なモデルでは、次のように分ける方が扱いやすいでしょう。\n\nstatus:\nactive\ninactive\nsuspended\nexpired\n\ngrantType:\nplan\nadd_on\ntrial\nmanual\npromotion\nbeta\nmigration\n\n\n今回はFeatureとEntitlementに集中するため、grantTypeは簡単な文字列属性として扱います。\n\n以降、この記事では分離後の方を採用します。 つまり、statusの取りうる値はactive・inactive・suspended・expiredの4つで、\ntrialはgrantType側の値です。第8節でEntitlement 3の状態をtrialと書きましたが、あれはstatus: active / \ngrantType: trialと読み替えてください。第34節のSHACLも、この4値を前提にしています。\n\n\n--------------------------------------------------------------------------------\n\n15. Entitlementの有効期間\nEntitlementには、有効期間を持たせられます。\n\nvalidFrom\nvalidUntil\n\n\nたとえば、AI文章生成Featureを1か月だけ試用できるとします。\n\nEntitlement\n├── Tenant: 株式会社ABC\n├── Feature: AI文章生成\n├── status: active\n├── grantType: trial\n├── validFrom: 2026-08-01\n└── validUntil: 2026-08-31\n\n\nここで注意したいのは、\n\nstatusがactiveである\n\n\nことと、\n\n現在日時が有効期間内である\n\n\nことの両方を確認する必要がある点です。\n\nEntitlementが利用可能かどうかは、概念的には次のように考えられます。\n\n利用可能なEntitlement\n＝\nstatusがactive\nかつ\n現在日時がvalidFrom以降\nかつ\nvalidUntilがない、または現在日時がvalidUntil以前\n\n\nこの日時判定は、OWLの推論だけで扱うより、アプリケーションやルールエンジンで判定する方が現実的です。\n\n\n--------------------------------------------------------------------------------\n\n16. Entitlementはどこから来たのか\nEntitlementには、付与根拠があります。たとえば、次のような経路があります。\n\nPlanに含まれていた\n追加契約された\n無料トライアルとして付与された\n営業担当者が個別に付与した\nβプログラム参加Tenantとして付与された\n旧契約から移行された\n\n\nそこで、EntitlementからSubscriptionを参照できるようにします。\n\nEntitlement\n→ derivedFromSubscription\n→ Subscription\n\n\nEnterprise Plan契約に基づくSAML SSO Entitlementなら、次のようになります。\n\nEntitlement\n├── beneficiary: 株式会社ABCテナント\n├── feature: SAML SSO\n├── status: active\n└── derivedFromSubscription:\n    株式会社ABCのEnterprise Subscription\n\n\nただし、すべてのEntitlementがSubscriptionに由来するとは限りません。β提供や手動付与では、Subscriptionを参照しないこともあるため、\nderivedFromSubscriptionは必須にはしません。\n\n\n--------------------------------------------------------------------------------\n\n17. 今回追加するクラスと関係\n今回、新たに追加するクラスは2つです。\n\nFeature\nEntitlement\n\n\n中心となる関係は次のとおりです。\n\n主語関係目的語意味PlanincludesFeatureFeature標準商品にFeatureが含まれるFeatureincludedInPlanPlan\nincludesFeatureの逆TenanthasEntitlementEntitlementTenantがFeature利用資格を持つEntitlement\nentitledTenantTenantEntitlementの対象TenantEntitlemententitlesFeatureFeature\n利用可能になるFeatureFeaturehasFeatureEntitlementEntitlemententitlesFeatureの逆\nEntitlementderivedFromSubscriptionSubscriptionEntitlementの契約上の根拠Feature\nhasSubFeatureFeature下位Featureを持つFeaturesubFeatureOfFeature上位Featureに属する\n全体像は次のようになります。\n\n\ngraph TD\n    T[Tenant]\n    S[Subscription]\n    P[Plan]\n    E[Entitlement]\n    F[Feature]\n    SF[Sub Feature]\n\n    T -->|hasSubscription| S\n    S -->|basedOnPlan| P\n\n    P -->|includesFeature| F\n\n    T -->|hasEntitlement| E\n    E -->|entitlesFeature| F\n    E -->|derivedFromSubscription| S\n\n    F -->|hasSubFeature| SF\n\n\n\n\n--------------------------------------------------------------------------------\n\n18. オントロジー定義のTurtle\nここから、FeatureとEntitlementを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\n#\n# Classes\n#\n\nsaas:Feature a owl:Class ;\n    rdfs:label \"Feature\"@en ,\n               \"機能\"@ja ;\n    rdfs:comment\n        \"SaaS提供者が提供可能な、識別可能な機能または能力。\"@ja .\n\nsaas:Entitlement a owl:Class ;\n    rdfs:label \"Entitlement\"@en ,\n               \"機能利用資格\"@ja ;\n    rdfs:comment\n        \"特定のTenantに、特定のFeatureを提供する権利、資格または状態。\"@ja .\n\n#\n# Plan and Feature\n#\n\nsaas:includesFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Plan ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"includes feature\"@en ,\n               \"機能を含む\"@ja ;\n    rdfs:comment\n        \"商品設計上、PlanにFeatureが含まれていることを表す。\"@ja .\n\nsaas:includedInPlan a owl:ObjectProperty ;\n    owl:inverseOf saas:includesFeature ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Plan ;\n    rdfs:label \"included in plan\"@en ,\n               \"プランに含まれる\"@ja .\n\n#\n# Tenant and Subscription\n#\n# 第1回で定義したプロパティですが、今回のEntitlementから参照するため、\n# また第37節・第38節のSHACLが依存するため、ここで再掲します。\n#\n\nsaas:hasSubscription a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Subscription ;\n    rdfs:label \"has subscription\"@en ,\n               \"契約を持つ\"@ja .\n\nsaas:subscriptionBelongsToTenant a owl:ObjectProperty ;\n    owl:inverseOf saas:hasSubscription ;\n    rdfs:domain saas:Subscription ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"subscription belongs to tenant\"@en ,\n               \"契約が属するテナント\"@ja .\n\nsaas:basedOnPlan a owl:ObjectProperty ;\n    rdfs:domain saas:Subscription ;\n    rdfs:range saas:Plan ;\n    rdfs:label \"based on plan\"@en ,\n               \"プランに基づく\"@ja .\n\n#\n# Tenant and Entitlement\n#\n\nsaas:hasEntitlement a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Entitlement ;\n    rdfs:label \"has entitlement\"@en ,\n               \"機能利用資格を持つ\"@ja .\n\nsaas:entitledTenant a owl:ObjectProperty ;\n    owl:inverseOf saas:hasEntitlement ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"entitled tenant\"@en ,\n               \"利用資格の対象テナント\"@ja .\n\nsaas:entitlesFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"entitles feature\"@en ,\n               \"利用可能にする機能\"@ja .\n\nsaas:hasFeatureEntitlement a owl:ObjectProperty ;\n    owl:inverseOf saas:entitlesFeature ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Entitlement ;\n    rdfs:label \"has feature entitlement\"@en ,\n               \"この機能に対する利用資格\"@ja .\n\n#\n# Entitlement source\n#\n\nsaas:derivedFromSubscription a owl:ObjectProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range saas:Subscription ;\n    rdfs:label \"derived from subscription\"@en ,\n               \"契約に基づく\"@ja ;\n    rdfs:comment\n        \"EntitlementがどのSubscriptionを根拠として付与されたかを表す。\"@ja .\n\n#\n# Feature hierarchy\n#\n\nsaas:hasSubFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"has sub-feature\"@en ,\n               \"下位機能を持つ\"@ja .\n\nsaas:subFeatureOf a owl:ObjectProperty ;\n    owl:inverseOf saas:hasSubFeature ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"sub-feature of\"@en ,\n               \"上位機能に属する\"@ja .\n\n#\n# Datatype properties\n#\n\nsaas:featureCode a owl:DatatypeProperty ;\n    rdfs:domain saas:Feature ;\n    rdfs:range xsd:string ;\n    rdfs:label \"feature code\"@en ,\n               \"機能コード\"@ja .\n\nsaas:entitlementStatus a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:string ;\n    rdfs:label \"entitlement status\"@en ,\n               \"利用資格状態\"@ja .\n\nsaas:grantType a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:string ;\n    rdfs:label \"grant type\"@en ,\n               \"付与種別\"@ja .\n\nsaas:validFrom a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:dateTime ;\n    rdfs:label \"valid from\"@en ,\n               \"有効開始日時\"@ja .\n\nsaas:validUntil a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:dateTime ;\n    rdfs:label \"valid until\"@en ,\n               \"有効終了日時\"@ja .\n\nsaas:grantedAt a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:dateTime ;\n    rdfs:label \"granted at\"@en ,\n               \"付与日時\"@ja .\n\nsaas:grantReason a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:string ;\n    rdfs:label \"grant reason\"@en ,\n               \"付与理由\"@ja .\n\n\n\n\n\n\nvalidFromとvalidUntilには、rdfs:domain saas:Entitlementを付けています。名前は汎用的ですが、\nこのオントロジーではEntitlement専用です。\n\nRDFSのrdfs:domain\nは「このプロパティを持つものは、そのクラスである」という推論規則なので、うっかり他のクラスに使うと、推論器がそれをEntitlementだと判定してしまいます。\n\nex:subscription-x a saas:Subscription ;\n    saas:validFrom \"2026-01-01T00:00:00+09:00\"^^xsd:dateTime .\n\n↓ OWL推論後\n\nex:subscription-x は Subscription かつ Entitlement\n\n\nContractやSubscriptionなど、他のクラスにも有効期間を持たせたくなったときは、validFromを使い回さず、contractStartや\nsubscriptionStartのような別名を用意してください。\n\n\n--------------------------------------------------------------------------------\n\n19. Featureコードを持たせる理由\nFeatureには、人間向けの名称とは別に、安定したFeatureコードを持たせます。\n\nex:feature-saml-sso\n    saas:name \"SAML SSO\" ;\n    saas:featureCode \"security.saml-sso\" .\n\n\n名称は、後から変更される可能性があります。\n\n旧名称:\nAI文章生成\n\n新名称:\nAIライティングアシスタント\n\n\nまた、多言語化されることもあります。\n\n日本語:\nAI文章生成\n\n英語:\nAI Writing Assistant\n\n\n一方、アプリケーションの判定に使う識別子は、簡単には変えたくありません。\n\nai.text-generation\n\n\nそのため、\n\nname\n＝ 人間が読む表示名\n\nfeatureCode\n＝ システムが参照する安定識別子\n\n\nとして分けます。\n\nURIそのものを安定識別子として使う方法もありますが、アプリケーションコードや設定ファイルでは、短いFeatureコードが便利な場合があります。\n\n\n--------------------------------------------------------------------------------\n\n20. Featureの具体的なインスタンス\n株式会社ABCが利用するプロジェクト管理SaaSを考えます。このSaaSには、次のFeatureがあります。\n\n基本プロジェクト管理\nCSVエクスポート\nAPIアクセス\nSAML SSO\n監査ログ\nAI文章生成\nAI要約\n\n\nTurtleでは、次のように表現します。\n\n@prefix saas: <https://example.com/ontology/saas#> .\n@prefix ex:   <https://example.com/data/> .\n\n#\n# Core features\n#\n\nex:feature-project-management a saas:Feature ;\n    saas:name \"基本プロジェクト管理\" ;\n    saas:featureCode \"core.project-management\" .\n\nex:feature-csv-export a saas:Feature ;\n    saas:name \"CSVエクスポート\" ;\n    saas:featureCode \"data.csv-export\" .\n\nex:feature-api-access a saas:Feature ;\n    saas:name \"APIアクセス\" ;\n    saas:featureCode \"integration.api-access\" .\n\nex:feature-saml-sso a saas:Feature ;\n    saas:name \"SAML SSO\" ;\n    saas:featureCode \"security.saml-sso\" .\n\nex:feature-audit-log a saas:Feature ;\n    saas:name \"監査ログ\" ;\n    saas:featureCode \"security.audit-log\" .\n\n#\n# AI feature hierarchy\n#\n\nex:feature-ai a saas:Feature ;\n    saas:name \"AI機能\" ;\n    saas:featureCode \"ai\" ;\n    saas:hasSubFeature\n        ex:feature-ai-text-generation,\n        ex:feature-ai-summarization .\n\nex:feature-ai-text-generation a saas:Feature ;\n    saas:name \"AI文章生成\" ;\n    saas:featureCode \"ai.text-generation\" ;\n    saas:subFeatureOf ex:feature-ai .\n\nex:feature-ai-summarization a saas:Feature ;\n    saas:name \"AI要約\" ;\n    saas:featureCode \"ai.summarization\" ;\n    saas:subFeatureOf ex:feature-ai .\n\n\n\n--------------------------------------------------------------------------------\n\n\n\n21. Planに含まれるFeatureの具体例\n次に、Standard PlanとEnterprise Planを定義します。\n\n#\n# Plans\n#\n\nex:plan-standard a saas:Plan ;\n    saas:name \"Standard Plan\" ;\n    saas:includesFeature\n        ex:feature-project-management,\n        ex:feature-csv-export,\n        ex:feature-api-access .\n\nex:plan-enterprise a saas:Plan ;\n    saas:name \"Enterprise Plan\" ;\n    saas:includesFeature\n        ex:feature-project-management,\n        ex:feature-csv-export,\n        ex:feature-api-access,\n        ex:feature-saml-sso,\n        ex:feature-audit-log,\n        ex:feature-ai-text-generation,\n        ex:feature-ai-summarization .\n\n\nこれにより、商品定義として次のことを表現できます。\n\nStandard Planには、\n基本プロジェクト管理、\nCSVエクスポート、\nAPIアクセスが含まれる。\n\nEnterprise Planには、\nそれらに加えて、\nSAML SSO、\n監査ログ、\nAI文章生成、\nAI要約が含まれる。\n\n\nただし、これはまだ商品定義であり、特定Tenantで実際に利用可能かどうかは、Entitlementで表現します。\n\n\n--------------------------------------------------------------------------------\n\n22. 株式会社ABCのSubscription\n株式会社ABCテナントは、Standard Planを契約しているとします。\n\n第1回の例では、同じテナントがProfessional\nPlanを契約している想定でしたが、あれは第1回だけの最小例です。以降はこのStandard契約で読み進めてください(第1回のTurtleをそのまま残していると、テナントに2つのSubscriptionがぶら下がった状態になります)。\n\nex:subscription-abc-standard a saas:Subscription ;\n    saas:name \"株式会社ABC Standard契約\" ;\n    saas:status \"active\" ;\n    saas:basedOnPlan ex:plan-standard .\n\nex:tenant-abc a saas:Tenant ;\n    saas:name \"株式会社ABCテナント\" ;\n    saas:hasSubscription ex:subscription-abc-standard .\n\n\nStandard Planには、次のFeatureが含まれています。\n\n基本プロジェクト管理\nCSVエクスポート\nAPIアクセス\n\n\nそのため、株式会社ABCテナントには、これらに対応するEntitlementが作られます。\n\n\n--------------------------------------------------------------------------------\n\n23. 株式会社ABCのEntitlement\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n\n#\n# Entitlements derived from Standard Plan\n#\n\nex:entitlement-abc-project-management a saas:Entitlement ;\n    saas:entitledTenant ex:tenant-abc ;\n    saas:entitlesFeature ex:feature-project-management ;\n    saas:derivedFromSubscription ex:subscription-abc-standard ;\n    saas:entitlementStatus \"active\" ;\n    saas:grantType \"plan\" ;\n    saas:grantedAt \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime ;\n    saas:validFrom \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime .\n\nex:entitlement-abc-csv-export a saas:Entitlement ;\n    saas:entitledTenant ex:tenant-abc ;\n    saas:entitlesFeature ex:feature-csv-export ;\n    saas:derivedFromSubscription ex:subscription-abc-standard ;\n    saas:entitlementStatus \"active\" ;\n    saas:grantType \"plan\" ;\n    saas:grantedAt \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime ;\n    saas:validFrom \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime .\n\nex:entitlement-abc-api-access a saas:Entitlement ;\n    saas:entitledTenant ex:tenant-abc ;\n    saas:entitlesFeature ex:feature-api-access ;\n    saas:derivedFromSubscription ex:subscription-abc-standard ;\n    saas:entitlementStatus \"active\" ;\n    saas:grantType \"plan\" ;\n    saas:grantedAt \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime ;\n    saas:validFrom \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime .\n\n\nTenant側からもEntitlementを明示できます。\n\nex:tenant-abc\n    saas:hasEntitlement\n        ex:entitlement-abc-project-management,\n        ex:entitlement-abc-csv-export,\n        ex:entitlement-abc-api-access .\n\n\nhasEntitlementとentitledTenantは逆プロパティとして定義しているため、推論環境では片方だけを書いても、もう片方を導出できます。\n\nただし、「推論環境」は自動的には用意されません。たとえばpyshaclで検証する場合、オントロジーを-eで渡し、-i owlrl\nでOWL推論を有効にする必要があります。\n\npyshacl -s shapes.ttl -e ontology.ttl -i owlrl data.ttl\n\n\n-iを付けない、あるいは-i rdfsにした場合、owl:inverseOfは効きません。hasEntitlement\nだけを書いたデータは、第34節のShape(entitledTenantが必須)で違反として報告されます。\n\n\n--------------------------------------------------------------------------------\n\n24. Standard PlanだがAI機能を個別提供する\nここで、株式会社ABCだけにAI文章生成を無料トライアルとして提供するとします。Standard\nPlanにはAI文章生成は含まれていませんが、Tenant個別のEntitlementを作ることで提供できます。\n\nex:entitlement-abc-ai-text-generation-trial\n    a saas:Entitlement ;\n\n    saas:entitledTenant ex:tenant-abc ;\n\n    saas:entitlesFeature\n        ex:feature-ai-text-generation ;\n\n    saas:entitlementStatus \"active\" ;\n\n    saas:grantType \"trial\" ;\n\n    saas:grantReason\n        \"Enterprise Plan導入検討のためのトライアル\" ;\n\n    saas:grantedAt\n        \"2026-08-10T09:00:00+09:00\"^^xsd:dateTime ;\n\n    saas:validFrom\n        \"2026-08-10T09:00:00+09:00\"^^xsd:dateTime ;\n\n    saas:validUntil\n        \"2099-12-31T23:59:59+09:00\"^^xsd:dateTime .\n\n\nこのEntitlementはStandard Subscriptionに由来するものではないため、derivedFromSubscription\nを持たせていません。\n\nこれにより、次の状態を表現できます。\n\n株式会社ABCはStandard Planを契約している。\n\nStandard PlanにはAI文章生成は含まれない。\n\nしかし、株式会社ABCには、\nAI文章生成のTrial Entitlementがある。\n\nしたがって、トライアル期間中は利用可能である。\n\n\nPlanとEntitlementを分ける大きな利点です。\n\n\n--------------------------------------------------------------------------------\n\n25. Featureが一時停止されている例\n株式会社ABCはAPIアクセスFeatureを契約上利用できますが、セキュリティ上の理由で一時的に停止されたとします。\n\nRDFグラフは同じ主語・述語へ新しい値を書くだけでは古い値を上書きしません。activeを残したままsuspended\nを追加すると、状態が2つあるデータになります。ここではSPARQL Updateで旧状態を削除してから、新状態を追加します。\n\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nDELETE {\n    ex:entitlement-abc-api-access\n        saas:entitlementStatus ?oldStatus .\n}\nINSERT {\n    ex:entitlement-abc-api-access\n        saas:entitlementStatus \"suspended\" ;\n        saas:grantReason\n            \"APIキー漏えいの疑いにより一時停止\" .\n}\nWHERE {\n    OPTIONAL {\n        ex:entitlement-abc-api-access\n            saas:entitlementStatus ?oldStatus .\n    }\n}\n\n\nこのクエリはSELECTではなくUPDATEなので、sparqlqueryコマンドでは実行できません。\n\n$ sparqlquery data.ttl -qf update.rq -f csv\npyparsing.exceptions.ParseException: Expected {SelectQuery | ConstructQuery |\nDescribeQuery | AskQuery}, found 'DELETE'\n\n\nRDFLibから実行し、結果をファイルに書き戻します。\n\n# update.py\nimport rdflib\n\ng = rdflib.Graph()\ng.parse(\"data.ttl\", format=\"turtle\")\ng.update(open(\"update.rq\").read())\ng.serialize(\"data.ttl\", format=\"turtle\")\n\n\npython update.py\n\n\nこの場合、\n\nStandard PlanにはAPIアクセスが含まれる。\n\nSubscriptionも有効である。\n\nしかし、APIアクセスEntitlementはsuspendedである。\n\n\nしたがって、現在はAPIアクセスを許可しません。\n\nこのように、Entitlementを明示的に持つことで、\n\n商品上は提供対象\nしかし\n運用上は一時停止\n\n\nという状態を表現できます。\n\n\n--------------------------------------------------------------------------------\n\n26. PermissionとEntitlementを組み合わせた具体例\nAI文章生成を実行するには、次の2条件が必要だとします。\n\nTenant側:\nai.text-generation Featureの有効なEntitlement\n\nUser側:\nai.generate Permission\n\n\nRoleとPermissionを次のように定義します。\n\nex:permission-ai-generate a saas:Permission ;\n    saas:name \"ai.generate\" .\n\nex:role-ai-user a saas:Role ;\n    saas:name \"AI User\" ;\n    saas:grantsPermission\n        ex:permission-ai-generate .\n\n\n山田太郎さんに、営業Workspaceの範囲でAI User Roleを付与します。\n\nex:role-assignment-yamada-ai-user\n    a saas:RoleAssignment ;\n\n    saas:assignedTo\n        ex:membership-yamada-abc ;\n\n    saas:assignedRole\n        ex:role-ai-user ;\n\n    saas:appliesToWorkspace\n        ex:workspace-sales ;\n\n    saas:assignedAt\n        \"2026-08-10T09:10:00+09:00\"^^xsd:dateTime .\n\n\nさらに、株式会社ABCテナントにはAI文章生成のTrial Entitlementがあります。\n\nTenant:\nAI文章生成を利用可能\n\n山田太郎:\n営業Workspaceでai.generateを実行可能\n\n\nしたがって、山田太郎さんは営業WorkspaceでAI文章生成を利用できますが、佐藤花子さんにAI User\nRoleがなければ、Tenantとしては利用可能でも、佐藤花子さんは利用できません。\n\n\n--------------------------------------------------------------------------------\n\n27. 利用可否の判定フロー\nAI文章生成のようなFeatureを利用できるかは、次の順番で確認できます。\n\n1. Userを特定する\n\n2. 対象Tenantに対する\n   有効なMembershipを取得する\n\n3. 対象WorkspaceまたはTenant Scopeで、\n   必要なRoleAssignmentを確認する\n\n4. Roleが必要なPermissionを持つか確認する\n\n5. Tenantに対象Featureの\n   有効なEntitlementがあるか確認する\n\n6. Entitlementの有効期間内か確認する\n\n7. すべて満たせば操作を許可する\n\n\n概念図は次のようになります。\n\n\ngraph TD\n    U[User]\n    M[Membership]\n    RA[RoleAssignment]\n    R[Role]\n    P[Permission]\n\n    T[Tenant]\n    E[Entitlement]\n    F[Feature]\n\n    U --> M\n    M --> RA\n    RA --> R\n    R --> P\n\n    M --> T\n    T --> E\n    E --> F\n\n    P --> D{Permissionあり?}\n    F --> G{Entitlement有効?}\n\n    D -->|Yes| A{両方満たす?}\n    G -->|Yes| A\n\n    A -->|Yes| OK[操作許可]\n\n\n\n\n\n最終的な判断は、次の形です。\n\n利用者に許可されている\nかつ\nTenantに提供されている\n\n\n\n--------------------------------------------------------------------------------\n\n28. 全インスタンスの関係図\n\ngraph TD\n    T[株式会社ABC\nTenant]\n    S[Standard契約\nSubscription]\n    P[Standard Plan]\n\n    E1[CSV Export Entitlement\nactive]\n    E2[API Access Entitlement\nsuspended]\n    E3[AI Text Trial Entitlement\nactive]\n\n    F1[CSV Export\nFeature]\n    F2[API Access\nFeature]\n    F3[AI Text Generation\nFeature]\n\n    M[山田Membership]\n    RA[AI User RoleAssignment]\n    R[AI User Role]\n    PM[ai.generate Permission]\n\n    T --> S\n    S --> P\n\n    P -->|includesFeature| F1\n    P -->|includesFeature| F2\n\n    T --> E1\n    E1 --> F1\n    E1 --> S\n\n    T --> E2\n    E2 --> F2\n    E2 --> S\n\n    T --> E3\n    E3 --> F3\n\n    M --> T\n    M --> RA\n    RA --> R\n    R --> PM\n\n\n\n\n\nこの図から、次のことが分かります。\n\nCSVエクスポート\n→ Planに含まれる\n→ Entitlementもactive\n\nAPIアクセス\n→ Planに含まれる\n→ しかしEntitlementはsuspended\n\nAI文章生成\n→ Planには含まれない\n→ しかしTrial Entitlementがactive\n\n\n\n--------------------------------------------------------------------------------\n\n29. SPARQLで問い合わせる\nこの節以降の問い合わせは、第25節の更新を適用したあとのグラフを前提にします。つまり、APIアクセスのEntitlementはsuspended\nになっている状態です。まだ適用していない場合は、先に第25節のupdate.pyを実行してください。\n\n29.1 Planに含まれるFeatureを取得する\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nSELECT ?feature ?featureName ?featureCode\nWHERE {\n    ex:plan-standard\n        saas:includesFeature ?feature .\n\n    ?feature\n        saas:name ?featureName ;\n        saas:featureCode ?featureCode .\n}\nORDER BY ?featureCode\n\n\n結果は次のとおりです。\n\nfeature,featureName,featureCode\nhttps://example.com/data/feature-project-management,基本プロジェクト管理,core.project-management\nhttps://example.com/data/feature-csv-export,CSVエクスポート,data.csv-export\nhttps://example.com/data/feature-api-access,APIアクセス,integration.api-access\n\n\n以降の節では、見やすさのために主要な列だけを抜き出して示します。\n\nこれは商品定義を問い合わせています。まだ、株式会社ABCで現在利用可能かどうかを調べているわけではありません。\n\n\n--------------------------------------------------------------------------------\n\n29.2 Tenantが持つすべてのEntitlementを取得する\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nSELECT\n    ?entitlement\n    ?feature\n    ?featureName\n    ?status\n    ?grantType\nWHERE {\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant ex:tenant-abc ;\n        saas:entitlesFeature ?feature ;\n        saas:entitlementStatus ?status ;\n        saas:grantType ?grantType .\n\n    ?feature saas:name ?featureName .\n}\nORDER BY ?featureName\n\n\n結果のイメージは次のようになります。\n\nFeaturestatusgrantTypeAI文章生成activetrialAPIアクセスsuspendedplanCSVエクスポートactiveplan\n基本プロジェクト管理activeplan\n--------------------------------------------------------------------------------\n\n29.3 Tenantで有効なFeatureを取得する\nまずは日時を考慮せず、状態だけで判定します。\n\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nSELECT DISTINCT\n    ?feature\n    ?featureName\n    ?featureCode\nWHERE {\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant ex:tenant-abc ;\n        saas:entitlesFeature ?feature ;\n        saas:entitlementStatus \"active\" .\n\n    ?feature\n        saas:name ?featureName ;\n        saas:featureCode ?featureCode .\n}\nORDER BY ?featureCode\n\n\nこの問い合わせでは、Entitlementがsuspendedのため、APIアクセスは取得されません。\n\n\n--------------------------------------------------------------------------------\n\n29.4 現在時刻を考慮して有効なFeatureを取得する\nSPARQLのNOW()を使って、有効期間も確認します。\n\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nSELECT DISTINCT\n    ?feature\n    ?featureName\n    ?featureCode\nWHERE {\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant ex:tenant-abc ;\n        saas:entitlesFeature ?feature ;\n        saas:entitlementStatus \"active\" .\n\n    OPTIONAL {\n        ?entitlement saas:validFrom ?validFrom .\n    }\n\n    OPTIONAL {\n        ?entitlement saas:validUntil ?validUntil .\n    }\n\n    FILTER (\n        !BOUND(?validFrom) ||\n        ?validFrom <= NOW()\n    )\n\n    FILTER (\n        !BOUND(?validUntil) ||\n        NOW() <= ?validUntil\n    )\n\n    ?feature\n        saas:name ?featureName ;\n        saas:featureCode ?featureCode .\n}\nORDER BY ?featureCode\n\n\nこれにより、次の条件を満たすEntitlementだけを取得します。\n\nstatusがactive\n\nvalidFromがない、\nまたは現在時刻がvalidFrom以降\n\nvalidUntilがない、\nまたは現在時刻がvalidUntil以前\n\n\n\n--------------------------------------------------------------------------------\n\n29.5 特定FeatureをTenantが利用できるか確認する\n株式会社ABCがAI文章生成を利用可能か確認します。\n\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nASK {\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant ex:tenant-abc ;\n        saas:entitlesFeature\n            ex:feature-ai-text-generation ;\n        saas:entitlementStatus \"active\" .\n\n    OPTIONAL {\n        ?entitlement saas:validFrom ?validFrom .\n    }\n\n    OPTIONAL {\n        ?entitlement saas:validUntil ?validUntil .\n    }\n\n    FILTER (\n        !BOUND(?validFrom) ||\n        ?validFrom <= NOW()\n    )\n\n    FILTER (\n        !BOUND(?validUntil) ||\n        NOW() <= ?validUntil\n    )\n}\n\n\n利用可能であれば、結果はtrueになります。\n\n\n--------------------------------------------------------------------------------\n\n29.6 Entitlementの付与根拠を確認する\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nSELECT\n    ?featureName\n    ?grantType\n    ?subscriptionName\n    ?reason\nWHERE {\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant ex:tenant-abc ;\n        saas:entitlesFeature ?feature ;\n        saas:grantType ?grantType .\n\n    ?feature saas:name ?featureName .\n\n    OPTIONAL {\n        ?entitlement\n            saas:derivedFromSubscription ?subscription .\n\n        ?subscription\n            saas:name ?subscriptionName .\n    }\n\n    OPTIONAL {\n        ?entitlement\n            saas:grantReason ?reason .\n    }\n}\nORDER BY ?featureName\n\n\nこの問い合わせにより、次の違いを確認できます。\n\nCSVエクスポート\n→ Standard Subscriptionに基づく\n\nAI文章生成\n→ Trialとして個別付与\n\n\n\n--------------------------------------------------------------------------------\n\n30. PermissionとEntitlementの両方を確認するSPARQL\n山田太郎さんが営業WorkspaceでAI文章生成を利用できるか確認します。必要な条件は、次のとおりです。\n\n山田太郎のMembershipが、\n株式会社ABCテナントに対して有効である。\n\nそのMembershipに、\n営業WorkspaceをScopeとするRoleAssignmentがある。\n\nそのRoleがai.generate Permissionを持つ。\n\n株式会社ABCテナントに、\nAI文章生成Featureの有効なEntitlementがある。\n\n\n第27節の判定フローのうち、「有効なMembershipを取得する」という手順を省略しないことが重要です。RoleAssignmentのURIを指定するだけでは、そのMembershipが停止されていないか、そもそも対象Tenantに属しているかを確認できません。\n\nなお、ex:membership-yamada-abcやex:workspace-sales\nは前回のデータで定義したものなので、この問い合わせは前回のデータと今回のデータを合わせたグラフに対して実行します。\n\nSPARQLでは次のようになります。\n\nPREFIX saas: <https://example.com/ontology/saas#>\nPREFIX ex:   <https://example.com/data/>\n\nASK {\n    #\n    # Membership check\n    #\n\n    ex:membership-yamada-abc\n        a saas:Membership ;\n        saas:membershipBelongsToTenant ex:tenant-abc ;\n        saas:status \"active\" .\n\n    #\n    # Permission check\n    #\n\n    ?roleAssignment\n        a saas:RoleAssignment ;\n        saas:assignedTo\n            ex:membership-yamada-abc ;\n        saas:appliesToWorkspace\n            ex:workspace-sales ;\n        saas:assignedRole ?role .\n\n    ?role\n        saas:grantsPermission\n            ex:permission-ai-generate .\n\n    #\n    # Entitlement check\n    #\n\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant\n            ex:tenant-abc ;\n        saas:entitlesFeature\n            ex:feature-ai-text-generation ;\n        saas:entitlementStatus \"active\" .\n\n    OPTIONAL {\n        ?entitlement saas:validFrom ?validFrom .\n    }\n\n    OPTIONAL {\n        ?entitlement saas:validUntil ?validUntil .\n    }\n\n    FILTER (\n        !BOUND(?validFrom) ||\n        ?validFrom <= NOW()\n    )\n\n    FILTER (\n        !BOUND(?validUntil) ||\n        NOW() <= ?validUntil\n    )\n}\n\n\nこの問い合わせは、PermissionとEntitlementを明確に分けています。\n\nRole・Permission側\n＝ 山田太郎が操作してよいか\n\nEntitlement側\n＝ 株式会社ABCに機能が提供されているか\n\n\n両方がそろったときだけ、trueになります。\n\n\n--------------------------------------------------------------------------------\n\n31. プロパティチェーンによる利用可能Featureの推論\nEntitlementの状態や有効期間をいったん無視すれば、次の関係から、\n\nTenant\n→ hasEntitlement\n→ Entitlement\n→ entitlesFeature\n→ Feature\n\n\nTenantとFeatureの直接関係を推論できます。\n\nsaas:hasEntitledFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"has entitled feature\"@en ,\n               \"利用資格のある機能を持つ\"@ja ;\n\n    owl:propertyChainAxiom (\n        saas:hasEntitlement\n        saas:entitlesFeature\n    ) .\n\n\nこれにより、次の関係を導出できます。\n\nex:tenant-abc\n    saas:hasEntitledFeature\n        ex:feature-ai-text-generation .\n\n\nただし、owl:propertyChainAxiom\nはOWLの推論規則なので、SPARQLで問い合わせるだけでは何も導出されません。第23節と同じく、OWL-RL推論器を通す必要があります。\n\npyshacl -s shapes.ttl -e ontology.ttl -i owlrl data.ttl\n\n\nRDFLibから直接推論する場合は、owlrlパッケージを使います。\n\nimport rdflib, owlrl\n\ng = rdflib.Graph()\ng.parse(\"ontology.ttl\", format=\"turtle\")\ng.parse(\"data.ttl\", format=\"turtle\")\nowlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g)\n\n\nさらに、この推論には注意が必要です。\n\nプロパティチェーンは、\n\nEntitlementが存在する\n\n\nことだけを見ています。\n\n次の条件は考慮していません。\n\n * entitlementStatusがactiveか\n * 有効期間内か\n * Subscriptionが有効か\n * Entitlementが停止されていないか\n\nしたがって、hasEntitledFeatureという名前を、\n\n現在利用可能なFeature\n\n\nという意味で使うのは危険です。\n\nより正確には、\n\nFeatureへのEntitlement関係が存在する\n\n\n程度の意味です。\n\n現在利用可能かどうかは、SPARQL、SHACL Rules、アプリケーションロジック、ポリシーエンジンなどで判定する方が安全です。\n\n\n--------------------------------------------------------------------------------\n\n32. 小休止：推論は「真実」ではなく「ルールの結果」である\nオントロジーの推論はとても便利ですが、推論された事実は、絶対的な真実ではありません。\n\n与えられたデータと、\n定義したルールから導かれた結果\n\n\nです。\n\nたとえば、\n\nTenant\n→ Entitlement\n→ Feature\n\n\nから、\n\nTenantはFeatureを利用可能\n\n\nと推論するルールを作ったとします。\n\nしかし、Entitlementが停止中なら、その推論は実務上正しくありません。これは推論エンジンが間違えたのではありません。\n\nこちらが、\n\nEntitlementが存在すれば利用可能\n\n\nという粗いルールを与えたためです。\n\nオントロジーを作るときは、\n\n何を推論できるか\n\n\nだけでなく、\n\nその推論がどの条件を無視しているか\n\n\nを意識する必要があります。\n\n特に認可、課金、契約、セキュリティでは、単純なプロパティチェーンだけを最終判断に使わない方がよいでしょう。\n\n\n--------------------------------------------------------------------------------\n\n33. SHACLによるFeatureの検証\nFeatureには、名称とFeatureコードが必要だとします。\n\n@prefix sh:   <http://www.w3.org/ns/shacl#> .\n@prefix saas: <https://example.com/ontology/saas#> .\n@prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n@prefix xsd:  <http://www.w3.org/2001/XMLSchema#> .\n\nsaas:FeatureShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Feature ;\n\n    sh:property [\n        sh:path saas:name ;\n        sh:minCount 1 ;\n        sh:or (\n            [ sh:datatype xsd:string ]\n            [ sh:datatype rdf:langString ]\n        ) ;\n        sh:message\n            \"Featureには名称が必要です。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:featureCode ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:datatype xsd:string ;\n        sh:pattern \"^[a-z0-9]+([._-][a-z0-9]+)*$\" ;\n        sh:message\n            \"Featureには有効なFeatureコードが1つ必要です。\"@ja\n    ] .\n\n\nこのパターンでは、たとえば次のコードを許可します。\n\nai\nai.text-generation\nsecurity.saml-sso\ndata.csv-export\n\n\n大文字や空白を含むコードは許可しません。\n\nAI Text Generation\nSAML SSO\n\n\n表示名ではなく、システム識別子として安定させるためです。\n\nnameの方にsh:orを使っているのは、第19節で触れた多言語化のためです。sh:datatype xsd:string\nだけにすると、言語タグ付きのリテラルが違反になります。\n\nsaas:name \"AI文章生成\"@ja    # sh:datatype xsd:string だけだと違反になる\n\n\nfeatureCodeは表示名ではないので、言語タグ付きを弾くsh:datatype xsd:stringのままにしています。\n\n\n--------------------------------------------------------------------------------\n\n34. SHACLによるEntitlementの基本検証\nEntitlementには、必ず1つのTenantと1つのFeatureが必要です。statusとgrantTypeは、第14節で分離した値だけを許可します(\ntrialはstatusではなくgrantTypeの値である点に注意してください)。\n\nsaas:EntitlementShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Entitlement ;\n\n    sh:property [\n        sh:path saas:entitledTenant ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Tenant ;\n        sh:message\n            \"Entitlementには対象Tenantが1つ必要です。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:entitlesFeature ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Feature ;\n        sh:message\n            \"Entitlementには対象Featureが1つ必要です。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:entitlementStatus ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:in (\n            \"active\"\n            \"inactive\"\n            \"suspended\"\n            \"expired\"\n        ) ;\n        sh:message\n            \"Entitlementのstatusは定義済みの値である必要があります。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:grantType ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:in (\n            \"plan\"\n            \"add_on\"\n            \"trial\"\n            \"manual\"\n            \"promotion\"\n            \"beta\"\n            \"migration\"\n        ) ;\n        sh:message\n            \"EntitlementのgrantTypeは定義済みの値である必要があります。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:validFrom ;\n        sh:maxCount 1 ;\n        sh:datatype xsd:dateTime\n    ] ;\n\n    sh:property [\n        sh:path saas:validUntil ;\n        sh:maxCount 1 ;\n        sh:datatype xsd:dateTime\n    ] .\n\n\n\n--------------------------------------------------------------------------------\n\n35. 有効開始日時と終了日時の整合性\nvalidUntilがvalidFromより前になってはいけません。誤った例は次のとおりです。\n\nvalidFrom:\n2026-09-01\n\nvalidUntil:\n2026-08-31\n\n\nSHACL-SPARQLで検証できます。\n\nなお、以下ではsh:selectの文字列の中にPREFIXを直接書いています。pyshacl\nはこの書き方をそのまま受け付けますが、SHACLの仕様が用意している正式な仕組みはsh:prefixesとsh:declare\nです。別の実装に移すときは、そちらに書き換えが必要になることがあります。\n\nsaas:EntitlementValidityPeriodShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Entitlement ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"EntitlementのvalidUntilはvalidFrom以降でなければなりません。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas:\n                <https://example.com/ontology/saas#>\n\n            SELECT $this\n            WHERE {\n                $this\n                    saas:validFrom ?validFrom ;\n                    saas:validUntil ?validUntil .\n\n                FILTER (?validUntil < ?validFrom)\n            }\n        \"\"\"\n    ] .\n\n\n\n--------------------------------------------------------------------------------\n\n36. Plan由来Entitlementの契約整合性\ngrantTypeがplanである場合、derivedFromSubscriptionを必須にしたいとします。\n\ngrantType = plan\n\nならば、\n\nderivedFromSubscriptionが必要\n\n\nSHACL-SPARQLでは、次のように書けます。\n\nsaas:PlanEntitlementSubscriptionShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Entitlement ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"grantTypeがplanのEntitlementにはderivedFromSubscriptionが必要です。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas:\n                <https://example.com/ontology/saas#>\n\n            SELECT $this\n            WHERE {\n                $this saas:grantType \"plan\" .\n\n                FILTER NOT EXISTS {\n                    $this\n                        saas:derivedFromSubscription\n                            ?subscription .\n                }\n            }\n        \"\"\"\n    ] .\n\n\n反対に、trialやmanualではSubscriptionがなくても構いません。\n\n\n--------------------------------------------------------------------------------\n\n37. EntitlementとSubscriptionのTenant整合性\nEntitlementの対象Tenantと、根拠となるSubscriptionのTenantは一致する必要があります。誤った状態は次のようなものです。\n\nEntitlementの対象:\nTenant A\n\n根拠Subscriptionの所有者:\nTenant B\n\n\nこれはクロステナント不整合であり、前回のTeamとWorkspaceのTenant整合性と同じように、非常に重要な制約です。\n\nsaas:EntitlementSubscriptionTenantConsistencyShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Entitlement ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"Entitlementの対象Tenantと、根拠SubscriptionのTenantは一致する必要があります。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas:\n                <https://example.com/ontology/saas#>\n\n            SELECT $this\n            WHERE {\n                $this\n                    saas:entitledTenant\n                        ?entitledTenant ;\n                    saas:derivedFromSubscription\n                        ?subscription .\n\n                OPTIONAL {\n                    ?subscription\n                        ( ^saas:hasSubscription\n                          | saas:subscriptionBelongsToTenant )\n                            ?subscriptionTenant .\n                }\n\n                FILTER (\n                    !BOUND(?subscriptionTenant) ||\n                    ?entitledTenant !=\n                    ?subscriptionTenant\n                )\n            }\n        \"\"\"\n    ] .\n\n\nここでは逆向きのパスを使っています。\n\n?subscription\n    ^saas:hasSubscription\n        ?subscriptionTenant .\n\n\nこれは、次の関係を逆向きにたどっています。\n\nTenant\n→ hasSubscription\n→ Subscription\n\n\nOPTIONALと!BOUNDを使っているのには理由があります。\n\n逆向きのパスだけで書くと、Subscription側からTenantを辿れないデータが、違反として検出されません。 たとえば、Tenant\nhasSubscription Subscription\nが1本も書かれていない状態で、別TenantのSubscriptionを根拠にしたEntitlementを作っても、パターンが一致しないので何も報告されません。\n\n検出したい違反が、\n「そもそもパターンに当たらない」ために\n黙って素通りする\n\n\nこれは、制約としては最悪の失敗の仕方です。そこで、OPTIONALでTenantを取りに行き、取れなかった場合も違反にします。\n\nまた、選択パス(|)で^saas:hasSubscriptionとsaas:subscriptionBelongsToTenant\nの両方を見ているのは、どちら向きに書かれていても辿れるようにするためです。第18節で定義したとおり、この2つは逆プロパティですが、第23節で書いたとおり、推論器を通していなければ片方からもう片方は導出されません。\n\n\n--------------------------------------------------------------------------------\n\n38. Plan由来EntitlementのFeature整合性\ngrantTypeがplanの場合、Entitlement対象Featureは、根拠SubscriptionのPlanに含まれているべきです。\n\nEntitlement\n→ derivedFromSubscription\n→ Subscription\n→ basedOnPlan\n→ Plan\n→ includesFeature\n→ Feature\n\n\nこの経路が成立するかを検証します。\n\nsaas:PlanEntitlementFeatureConsistencyShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Entitlement ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"Plan由来のEntitlement対象Featureは、SubscriptionのPlanに含まれている必要があります。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas:\n                <https://example.com/ontology/saas#>\n\n            SELECT $this\n            WHERE {\n                $this\n                    saas:grantType \"plan\" ;\n                    saas:entitlesFeature ?feature ;\n                    saas:derivedFromSubscription\n                        ?subscription .\n\n                ?subscription\n                    saas:basedOnPlan ?plan .\n\n                FILTER NOT EXISTS {\n                    ?plan\n                        saas:includesFeature\n                            ?feature .\n                }\n            }\n        \"\"\"\n    ] .\n\n\nただし、この制約は、自社の商品設計に合わせて採用する必要があります。\n\nたとえば、Subscriptionに個別契約Featureを含められる場合は、\n\nPlanには含まれないが、\nSubscriptionの個別契約条件には含まれる\n\n\nことがあり得ます。\n\nその場合、grantTypeをplanではなくadd_onやmanualにするか、契約明細の概念を追加する必要があります。\n\n今回はFeatureとEntitlementに限定するため、そこまでは進みません。\n\n\n--------------------------------------------------------------------------------\n\n39. 同一Tenant・同一Featureの重複Entitlement\n同じTenantとFeatureに対して、複数のEntitlementが存在することがあります。\n\nPlan由来のEntitlement\n\n追加契約由来のEntitlement\n\n期間限定Trial Entitlement\n\n\nこれは、必ずしも不正ではありません。たとえば、Enterprise Plan由来のAI\nFeatureと、追加のβ機能Entitlementが並存することがあります。しかし、同じFeatureに対して、複数のactive\nEntitlementがあると、どれを採用すべきか曖昧になる場合があります。\n\nEntitlement A\n├── Feature: AI文章生成\n├── status: active\n└── validUntil: 2026-09-01\n\nEntitlement B\n├── Feature: AI文章生成\n├── status: active\n└── validUntil: 2027-03-31\n\n\nこの場合、次の設計方針が考えられます。\n\n方針A：重複を禁止する\nTenant × Featureごとに、\nactive Entitlementは最大1つ\n\n\n単純で扱いやすい方法です。\n\n方針B：重複を許可し、どれか1つが有効なら利用可能\n有効なEntitlementが1つ以上あれば利用可能\n\n\n複数の付与経路を履歴として残しやすくなります。\n\n方針C：優先順位を決める\nmanual\nadd_on\nplan\ntrial\n\n\nなどの優先順位を定義します。\n\n共通オントロジーとしては、重複を許容する方が柔軟です。\n\n最終的な利用可否は、\n\n有効なEntitlementが1件以上存在するか\n\n\nで判定できます。\n\nただし、Entitlementの停止方法には注意が必要で、1つを停止しても、別のactive\nEntitlementが残っていれば、Featureは利用可能なままです。\n\n\n--------------------------------------------------------------------------------\n\n40. Featureの廃止\nSaaSでは、Featureが廃止されることもあります。Featureそのものを削除すると、過去の契約や監査履歴が読めなくなる可能性があるため、Featureにも状態を持たせる方法があります。\n\nactive\ndeprecated\nretired\n\n\nたとえば、\n\nsaas:featureStatus a owl:DatatypeProperty ;\n    rdfs:domain saas:Feature ;\n    rdfs:range xsd:string .\n\nex:feature-legacy-report\n    a saas:Feature ;\n    saas:name \"旧レポート機能\" ;\n    saas:featureCode \"report.legacy\" ;\n    saas:featureStatus \"retired\" .\n\n\nFeatureがretiredであっても、過去のEntitlementは履歴として残せます。\n\nFeatureは廃止済み\n\nしかし、\n\n過去にどのTenantが利用できたかは残る\n\n\nただし、今回はFeatureとEntitlementの最小モデルに集中するため、Feature状態は必須要素にはしません。\n\n\n--------------------------------------------------------------------------------\n\n41. Feature Flagとの違い\nFeatureという言葉から、Feature Flagを思い浮かべる人もいるでしょう。Feature\nFlagは、一般にソフトウェアの挙動を動的に切り替える仕組みです。\n\n新UIを有効化する\n新しい検索エンジンを使う\nβ版アルゴリズムを一部Userに公開する\n\n\nEntitlementと似ていますが、同じではありません。\n\nEntitlement\n＝ 契約・商品・提供資格の観点\n\nFeature Flag\n＝ リリース・運用・技術制御の観点\n\n\nたとえば、TenantにAI FeatureのEntitlementがあっても、障害対応でFeature\nFlagがOFFになっていれば、実際には利用できない場合があります。\n\nEntitlement: active\nFeature Flag: off\n\n\n反対に、開発環境でFeature FlagがONでも、TenantにEntitlementがなければ、本番利用を許可すべきではありません。\n\n概念的には、最終利用可否は次のようになることがあります。\n\nPermission\nかつ\nEntitlement\nかつ\nFeature Flag\nかつ\n運用条件\n\n\n今回のモデルではFeature Flagを扱いませんが、\n\nEntitlementだけで、\nあらゆる実行可否を表現しようとしない\n\n\nことは重要です。\n\n\n--------------------------------------------------------------------------------\n\n42. 小休止：契約上使えることと、技術的に動くこと\nSaaSでは、ときどき次の2つが混同されます。\n\n契約上使ってよい\n\n技術的に使える\n\n\n本来使えないPlanなのに、APIエンドポイントへ直接アクセスすると動いてしまう。管理画面では非表示だが、URLを知っていれば開けてしまう。Feature\nFlagだけで画面を隠し、バックエンドではEntitlementを検証していない。\n\nこうした実装は、単なるUI上の不具合ではありません。契約違反、情報漏えい、請求漏れ、セキュリティ事故につながる可能性があります。\n\nオントロジー上でFeatureとEntitlementを分けておくことは、概念整理だけでなく、\n\nどの層で何を確認すべきか\n\n\nを明確にする効果があります。\n\nフロントエンド\n→ 表示制御\n\nバックエンド\n→ EntitlementとPermissionの最終検証\n\n契約管理\n→ SubscriptionとEntitlementの生成\n\n運用管理\n→ 停止・再開・個別付与\n\n\n画面にボタンが見えないことは、認可ではありません。同じように、Feature FlagがOFFであることも、契約管理ではありません。\n\n\n--------------------------------------------------------------------------------\n\n43. 今回の最小関係一覧\n主語関係目的語意味PlanincludesFeatureFeaturePlanの商品構成にFeatureが含まれるFeatureincludedInPlan\nPlanFeatureがPlanに含まれるTenanthasEntitlementEntitlementTenantがFeature利用資格を持つ\nEntitlemententitledTenantTenantEntitlementの対象TenantEntitlemententitlesFeature\nFeatureEntitlementが利用可能にするFeatureFeaturehasFeatureEntitlementEntitlement\nFeatureに対するEntitlementEntitlementderivedFromSubscriptionSubscription\nEntitlementの契約上の根拠FeaturehasSubFeatureFeatureFeatureが下位Featureを持つFeature\nsubFeatureOfFeatureFeatureが上位Featureに属するTenanthasEntitledFeatureFeature\nhasEntitlement+entitlesFeatureから推論(第31節)\nEntitlementの根拠をたどるために、第1回で定義した次の関係も使います(第18節で再掲しました)。\n\n主語関係目的語意味TenanthasSubscriptionSubscriptionTenantがPlanを契約するSubscription\nsubscriptionBelongsToTenantTenanthasSubscriptionの逆SubscriptionbasedOnPlanPlan\n契約が基づくPlan主な属性は次のとおりです。\n\nクラス属性意味FeaturefeatureCode安定した機能識別子FeaturefeatureStatus機能の提供状態(第40節)Entitlement\nentitlementStatus現在の状態EntitlementgrantType付与経路・種別EntitlementvalidFrom有効開始日時\nEntitlementvalidUntil有効終了日時EntitlementgrantedAt付与日時EntitlementgrantReason付与理由\n--------------------------------------------------------------------------------\n\n44. 今回のモデルで答えられる質問\nFeatureに関する質問\nこのSaaSには、どのFeatureが存在するか。\n\nAI機能の下位Featureは何か。\n\nこのFeatureは、どのPlanに含まれるか。\n\nEnterprise Planに含まれ、\nStandard Planには含まれないFeatureは何か。\n\n\nEntitlementに関する質問\nこのTenantには、\nどのFeatureのEntitlementがあるか。\n\n現在activeなEntitlementはどれか。\n\nTrialとして付与されているFeatureは何か。\n\nこのEntitlementは、\nどのSubscriptionに基づいているか。\n\n1か月以内に期限切れになるEntitlementはどれか。\n\n\nPermissionと組み合わせた質問\n山田太郎は、\n営業WorkspaceでAI文章生成を利用できるか。\n\nFeatureは利用可能だが、\n操作Permissionを持たないUserは誰か。\n\nPermissionは持っているが、\nTenantにEntitlementがないため\n利用できない機能は何か。\n\n\n商品・契約差分に関する質問\nPlanには含まれていないが、\n個別に付与されているFeatureは何か。\n\nPlanには含まれるが、\n現在suspendedのFeatureは何か。\n\n同じPlanを契約しているTenant間で、\nEntitlementに差があるFeatureは何か。\n\n\n\n--------------------------------------------------------------------------------\n\n45. 今回のモデルのまとめ\n今回の内容を最も単純にまとめると、次のようになります。\n\nFeatureは、\nSaaSが提供可能な機能・能力である。\n\nPlanは、\n標準商品としてFeatureを含む。\n\nSubscriptionは、\nTenantによるPlanの個別契約である。\n\nEntitlementは、\n特定Tenantに特定Featureを提供する\n権利・資格・状態である。\n\nPermissionは、\n利用者が操作してよいかを表す。\n\nEntitlementは、\nTenantに機能が提供されているかを表す。\n\n\n図にすると、次の形です。\n\n\ngraph LR\n    T[Tenant]\n    S[Subscription]\n    P[Plan]\n    E[Entitlement]\n    F[Feature]\n\n    M[Membership]\n    RA[RoleAssignment]\n    R[Role]\n    PM[Permission]\n\n    T --> S\n    S --> P\n    P --> F\n\n    T --> E\n    E --> F\n    E --> S\n\n    M --> T\n    M --> RA\n    RA --> R\n    R --> PM\n\n\n\n\n\nそして、利用者がFeatureを操作できるかどうかは、基本的に次の組み合わせで決まります。\n\nTenant側:\n有効なEntitlementがある\n\nUser側:\n必要なPermissionがある\n\n\nつまり、\n\n利用可能\n＝\nEntitlement\nかつ\nPermission\n\n\nです。\n\n\n--------------------------------------------------------------------------------\n\n46. ここまでのB2B SaaSオントロジー\n第1回から今回までのモデルをまとめると、次のようになります。\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        │       └── Feature\n        │\n        └── Entitlement\n            ├── Feature\n            └── Subscription\n\n\nより意味を意識して読むと、次のようになります。\n\n現実世界のOrganizationが、\nSaaS上のTenantを利用する。\n\nUserは、\nMembershipを通じてTenantに参加する。\n\nTenantには、\nWorkspaceとTeamがある。\n\nMembershipには、\nScope付きのRoleAssignmentがある。\n\nRoleは、\nPermissionを付与する。\n\nTenantは、\nSubscriptionを通じてPlanを契約する。\n\nPlanは、\n標準商品としてFeatureを含む。\n\nTenantは、\nEntitlementを通じて、\n実際に利用可能なFeatureを持つ。\n\n\n\n--------------------------------------------------------------------------------\n\n47. 次回予告！\nここまでで、\n\nこのTenantに、\nこのFeatureが提供されているか\n\n\nを表現できるようになりました。\n\nしかし、Featureが利用可能だとしても、まだ次の問題が残っています。\n\n何回まで使えるのか。\n\n何人まで使えるのか。\n\n何GBまで保存できるのか。\n\nAPIを月に何回実行できるのか。\n\nAI生成を月に何回使えるのか。\n\n\nFeatureとEntitlementが表すのは、基本的に、\n\n利用できるか、できないか\n\n\nです。\n\nしかし、現実のSaaS契約には、\n\n利用できるが、上限がある\n\n\nという状態が大量にあります。\n\nその先では、Limit、Quota、Usageなどの概念が必要になりますが、それらはFeatureやEntitlementとは別の問題です。\n\nまず今回の段階では、次の区別を確実にします。\n\nFeature\n＝ どのような機能が存在するか\n\nEntitlement\n＝ そのTenantに機能が提供されているか\n\nPermission\n＝ その人が機能を操作してよいか\n\n\nこの3つを分けて表現できれば、B2B SaaSの料金プラン、オプション契約、トライアル、β提供、個別開放、機能停止を、かなり自然に扱えるようになります。\n\n今回も巨大な記事になってしまいましたが、まだまだ続きます！ げんなりしないで、一緒に頑張って学んでいきましょう！\n\n次回もまた絶対見てくれよな！","html":"<h1></h1><p>オッス！おらやぎぃ！</p><p>第3回目になりました！</p><p>ここまで読んでいただいている方は、きっとSaaSというものの構造についてかなり詳しくなっているのではないでしょうか？！</p><p>第2回は、テナントをベースとしたさらなる深掘りをしてみました。</p><p>第3回は、そのテナントがどんな機能を使えるのか？使っても良いのか？というのを考えていきたいと思います。</p><p>よくあるSaaSは、料金プランやオプションなどがあって、契約しているプランによって、テナントごとに使える機能や量が決まっていたりすると思います。それをどのように表現していくのか？を考えていきたいと思います。</p><h1 id=\"0-%E6%A8%A9%E9%99%90%E3%81%8C%E3%81%82%E3%81%A3%E3%81%A6%E3%82%82%E4%BD%BF%E3%81%88%E3%81%AA%E3%81%84%EF%BC%9F\">0. 権限があっても使えない？</h1><h2 id=\"feature%E3%81%A8entitlement%E3%81%A7%E3%80%8C%E5%A5%91%E7%B4%84%E4%B8%8A%E4%BD%BF%E3%81%88%E3%82%8B%E6%A9%9F%E8%83%BD%E3%80%8D%E3%82%92%E8%A1%A8%E7%8F%BE%E3%81%99%E3%82%8B\">FeatureとEntitlementで「契約上使える機能」を表現する</h2><p>前回までに、B2B SaaSの利用構造と権限構造を作りました。</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><p>このモデルを使えば、次のようなことを表現できます。</p><pre><code class=\"language-text\">山田太郎は、株式会社ABCテナントに所属している。\n\n山田太郎は、営業WorkspaceのEditorである。\n\nEditor Roleは、resource.updateというPermissionを持つ。\n</code></pre><p>ここまでを見ると、山田太郎さんがResourceを更新できるかどうかは、Permissionだけで判定できそうに見えますが、実際のB2B SaaSでは、それだけでは足りません。</p><p>たとえば、SaaSに「AIによる文章生成機能」があるとします。</p><p>山田太郎さんのRoleには、次のPermissionが含まれています。</p><pre><code class=\"language-text\">ai.generate\n</code></pre><p>それでも、株式会社ABCがAI機能を含まないStandard Planを契約していたら、AI文章生成は利用できないでしょう。</p><pre><code class=\"language-text\">山田太郎はAI生成を操作してよい。\n\nしかし、\n\n株式会社ABCにはAI生成機能が提供されていない。\n</code></pre><p>反対の状況もあります。</p><p>株式会社ABCがAI機能を含むEnterprise Planを契約していたとしても、山田太郎さんに<code>ai.generate</code>というPermissionがなければ、山田太郎さんはその機能を利用できません。</p><pre><code class=\"language-text\">株式会社ABCにはAI生成機能が提供されている。\n\nしかし、\n\n山田太郎にはAI生成を実行する権限がない。\n</code></pre><p>ここには、異なる2種類の「できる」があります。</p><pre><code class=\"language-text\">Permission\n＝ その人が操作してよいか\n\nEntitlement\n＝ そのTenantに、その機能が提供されているか\n</code></pre><p>今回は、この違いを表現するために、次の2つの概念を追加します。</p><pre><code class=\"language-text\">Feature\nEntitlement\n</code></pre><hr><h1 id=\"1-%E3%81%BE%E3%81%9A%E3%80%81%E4%BD%95%E3%81%8C%E5%95%8F%E9%A1%8C%E3%81%AA%E3%81%AE%E3%81%8B\">1. まず、何が問題なのか</h1><p>多くのSaaSでは、料金プランによって利用できる機能が変わります。</p><p>たとえば、次のような料金体系があるとします。</p><!--kg-card-begin: html--><table data-line=\"112\" class=\"code-line\" dir=\"auto\" style=\"border-collapse: collapse; margin-bottom: 0.7em;\"><thead data-line=\"112\" class=\"code-line\" dir=\"auto\"><tr data-line=\"112\" 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);\">Feature</th><th style=\"text-align: right; 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);\">Free</th><th style=\"text-align: right; 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);\">Standard</th><th style=\"text-align: right; 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);\">Enterprise</th></tr></thead><tbody data-line=\"114\" class=\"code-line\" dir=\"auto\"><tr data-line=\"114\" class=\"code-line\" dir=\"auto\"><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); text-align: right;\">○</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18); text-align: right;\">○</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18); text-align: right;\">○</td></tr><tr data-line=\"115\" 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);\">CSVエクスポート</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); text-align: right;\">×</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); text-align: right;\">○</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); text-align: right;\">○</td></tr><tr data-line=\"116\" 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);\">APIアクセス</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); text-align: right;\">×</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); text-align: right;\">○</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); text-align: right;\">○</td></tr><tr data-line=\"117\" 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);\">SAML SSO</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); text-align: right;\">×</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); text-align: right;\">×</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); text-align: right;\">○</td></tr><tr data-line=\"118\" 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);\">監査ログ</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); text-align: right;\">×</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); text-align: right;\">×</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); text-align: right;\">○</td></tr><tr data-line=\"119\" 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);\">AI文章生成</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); text-align: right;\">×</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); text-align: right;\">オプション</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); text-align: right;\">○</td></tr></tbody></table><!--kg-card-end: html--><p>この表には、少なくとも次の3種類の概念が含まれています。</p><pre><code class=\"language-text\">Plan\nFeature\nTenantごとの利用可否\n</code></pre><p>Planは、SaaS提供者が販売する標準商品です。</p><pre><code class=\"language-text\">Standard Plan\nEnterprise Plan\n</code></pre><p>Featureは、SaaSが提供する機能です。</p><pre><code class=\"language-text\">CSVエクスポート\nAPIアクセス\nSAML SSO\n監査ログ\nAI文章生成\n</code></pre><p>そして、実際に特定のTenantがそのFeatureを利用できるかどうかを表すのがEntitlementです。</p><pre><code class=\"language-text\">株式会社ABCテナントは、\nAI文章生成Featureを利用できる。\n</code></pre><p>この「利用できる」という関係を、単純なプロパティではなく、独立した概念として扱います。</p><hr><h1 id=\"2-feature%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B\">2. Featureとは何か</h1><p>Featureを、次のように定義します。</p><pre><code class=\"language-text\">Feature\n＝ SaaSが提供可能な、識別可能な機能・能力\n</code></pre><p>Featureの例には、次のようなものがあります。</p><ul><li>CSVエクスポート</li><li>PDF出力</li><li>APIアクセス</li><li>SAML SSO</li><li>SCIM連携</li><li>監査ログ</li><li>AI文章生成</li><li>高度な検索</li><li>ワークフロー承認</li><li>カスタムRole</li><li>外部共有</li><li>データ保持期間の設定</li><li>IPアドレス制限</li><li>独自ドメイン</li><li>ホワイトラベル</li><li>優先サポート</li></ul><p>Featureは、「画面上のボタン」と同じとは限りません。たとえば、「SAML SSO」というFeatureには、複数の画面やAPIが関係するかもしれません。</p><pre><code class=\"language-text\">SAML SSO Feature\n├── IdPメタデータの登録\n├── SPメタデータの取得\n├── SSOテスト\n├── SSO強制設定\n└── 証明書の更新\n</code></pre><p>逆に、画面上に1つのボタンがあっても、それが独立したFeatureとして管理されるとは限りません。</p><p>Featureとは、単なるUI部品ではなく、</p><pre><code class=\"language-text\">商品・契約・提供制御の単位として識別したい能力\n</code></pre><p>です。</p><hr><h1 id=\"3-feature%E3%81%AFresource%E3%81%AA%E3%81%AE%E3%81%8B\">3. FeatureはResourceなのか</h1><p>ここで、少し立ち止まって考えてみます。</p><p>前回までに、SaaS上の業務対象をResourceとして表現しました。</p><pre><code class=\"language-text\">顧客\n商談\nタスク\n設計書\n監査イベント\n</code></pre><p>では、FeatureもResourceなのでしょうか。</p><p>この連載では、FeatureとResourceは別の概念として扱います。</p><pre><code class=\"language-text\">Resource\n＝ TenantやWorkspaceが管理する業務データ\n\nFeature\n＝ SaaS提供者が提供する機能・能力\n</code></pre><p>たとえば、「顧客CSVエクスポート」というFeatureがあったとしても、CSVエクスポートそのものは顧客業務データではありません。</p><pre><code class=\"language-text\">Customer\n＝ Resource\n\nCSV Export\n＝ Feature\n</code></pre><p>FeatureはResourceに対して何らかの操作を提供することがあります。</p><pre><code class=\"language-text\">CSV Export Feature\n→ Customer Resourceを出力する\n</code></pre><p>しかし、FeatureとResourceは役割が異なります。</p><!--kg-card-begin: html--><table data-line=\"253\" class=\"code-line\" dir=\"auto\" style=\"border-collapse: collapse; margin-bottom: 0.7em;\"><thead data-line=\"253\" class=\"code-line\" dir=\"auto\"><tr data-line=\"253\" 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=\"255\" class=\"code-line\" dir=\"auto\"><tr data-line=\"255\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Resource</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);\">Tenant、Workspace</td></tr><tr data-line=\"256\" 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);\">Feature</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);\">CSV出力、SSO、AI生成</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></tr></tbody></table><!--kg-card-end: html--><hr><h1 id=\"4-feature%E3%81%AFpermission%E3%81%AA%E3%81%AE%E3%81%8B\">4. FeatureはPermissionなのか</h1><p>FeatureとPermissionも、よく混同されます。</p><p>たとえば、</p><pre><code class=\"language-text\">Feature: CSVエクスポート\n\nPermission: customer.export\n</code></pre><p>は、とても似ています。</p><p>しかし、意味は異なります。</p><pre><code class=\"language-text\">Feature\n＝ SaaSとして、その能力を提供できるか\n\nPermission\n＝ その利用者が、その操作をしてよいか\n</code></pre><p>Featureは商品・契約側の概念であり、Permissionは認可・アクセス制御側の概念です。</p><p>たとえば、株式会社ABCがCSVエクスポートFeatureを利用できるとしても、すべてのUserがCSVを出力できるとは限りません。</p><pre><code class=\"language-text\">株式会社ABCテナント\n→ CSVエクスポートFeatureが有効\n\n山田太郎\n→ customer.export Permissionを持つ\n\n佐藤花子\n→ customer.export Permissionを持たない\n</code></pre><p>この場合、山田太郎さんはCSVを出力できますが、佐藤花子さんは出力できません。</p><p>つまり、一般的な操作可否は次のように考えられます。</p><pre><code class=\"language-text\">操作可能\n＝\nTenantがFeatureを利用可能\nかつ\n利用者がPermissionを持つ\n</code></pre><p>より厳密には、MembershipやRoleAssignmentの有効状態、適用Scopeなども確認します。</p><pre><code class=\"language-text\">操作可能\n＝\n有効なMembershipがある\nかつ\n対象Scopeで必要なPermissionを持つ\nかつ\n対象TenantにFeatureの有効なEntitlementがある\n</code></pre><hr><h1 id=\"5-feature%E3%81%AFplan%E3%81%AB%E7%9B%B4%E6%8E%A5%E7%B5%90%E3%81%B3%E4%BB%98%E3%81%91%E3%82%8C%E3%81%B0%E3%82%88%E3%81%84%E3%81%AE%E3%81%8B\">5. FeatureはPlanに直接結び付ければよいのか</h1><p>単純な料金体系であれば、PlanとFeatureを直接結び付けることもできます。</p><pre><code class=\"language-text\">Enterprise Plan\n→ includesFeature\n→ SAML SSO\n</code></pre><p>この構造は自然です。</p><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph LR\n    P[Enterprise Plan]\n    F[SAML SSO Feature]\n\n    P -->|includesFeature| F\n\n</pre><!--kg-card-end: html--><p>しかし、これだけでは、実際のTenantごとの状態を十分に表現できません。</p><p>たとえば、次のようなケースがあります。</p><ul><li>Standard Planだが、AI機能を追加契約している</li><li>Enterprise Planだが、監査ログだけ一時的に停止されている</li><li>特定Tenantだけβ機能を提供している</li><li>障害対応として一時的に上位機能を開放した</li><li>契約交渉により、通常のPlanにない機能を個別提供した</li><li>無料トライアル期間中だけSAML SSOを使える</li><li>旧料金プランのTenantだけ、廃止済み機能を継続利用できる</li><li>契約は有効だが、法務確認が終わるまでAI機能を無効化している</li></ul><p>こうした状態は、</p><pre><code class=\"language-text\">Plan includesFeature Feature\n</code></pre><p>だけでは表現できません。</p><p>Planは標準商品ですが、実際にTenantへ何が提供されているかは、個別契約や運用状況によって変わります。</p><p>そこで、Entitlementという概念を導入します。</p><hr><h1 id=\"6-entitlement%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%8B\">6. Entitlementとは何か</h1><p>Entitlementは、日本語に訳しにくい言葉です。直訳すると、</p><ul><li>権利</li><li>資格</li><li>利用権</li><li>受給資格</li><li>正当な権原</li></ul><p>などになります。</p><p>B2B SaaSの文脈では、次のように定義すると分かりやすいでしょう。</p><pre><code class=\"language-text\">Entitlement\n＝ 特定のTenantに、特定のFeatureを提供する権利・資格・状態\n</code></pre><p>もう少し平たく言えば、</p><pre><code class=\"language-text\">このTenantは、このFeatureを使える\n</code></pre><p>という事実です。</p><p>たとえば、</p><pre><code class=\"language-text\">株式会社ABCテナントは、\nCSVエクスポートFeatureを利用できる。\n</code></pre><p>という事実を、Entitlementとして表します。</p><pre><code class=\"language-text\">Entitlement\n├── beneficiary: 株式会社ABCテナント\n├── feature: CSVエクスポート\n└── status: active\n</code></pre><p>ここでは<code>beneficiary</code>を「権利を受ける主体」という意味で使っています。</p><p>この連載では、最初はEntitlementの対象をTenantに限定します。</p><pre><code class=\"language-text\">Entitlementの対象\n＝ Tenant\n</code></pre><p>将来的には、Organization、Workspace、User、API Clientなどを対象にする可能性もありますが、最初から広げると意味が曖昧になります。</p><p>まずは、</p><pre><code class=\"language-text\">Tenantに対するFeature提供\n</code></pre><p>として固定します。</p><hr><h1 id=\"7-%E3%81%AA%E3%81%9Centitlement%E3%82%92%E4%B8%AD%E9%96%93%E6%A6%82%E5%BF%B5%E3%81%AB%E3%81%99%E3%82%8B%E3%81%AE%E3%81%8B\">7. なぜEntitlementを中間概念にするのか</h1><p>単純に次の関係を使うこともできます。</p><pre><code class=\"language-text\">Tenant canUseFeature Feature\n</code></pre><p>たとえば、</p><pre><code class=\"language-turtle\">ex:tenant-abc\n    saas:canUseFeature ex:feature-csv-export .\n</code></pre><p>これは簡単で、分かりやすい表現です。</p><p>しかし、実際のFeature提供には、さまざまな付随情報があります。</p><ul><li>有効か無効か</li><li>いつから有効か</li><li>いつまで有効か</li><li>どの契約に基づくか</li><li>どのPlanから継承されたか</li><li>追加オプションとして付与されたか</li><li>手動で付与されたか</li><li>無料トライアルなのか</li><li>β提供なのか</li><li>誰が有効化したか</li><li>なぜ停止されたか</li></ul><p>単純な、</p><pre><code class=\"language-text\">Tenant canUseFeature Feature\n</code></pre><p>という関係だけでは、これらを表現できません。</p><p>そこで、関係そのものを独立した概念にします。</p><pre><code class=\"language-text\">Tenant\n→ Entitlement\n→ Feature\n</code></pre><p>これは、前回までに登場したMembershipやRoleAssignmentと同じ考え方です。</p><pre><code class=\"language-text\">User\n→ Membership\n→ Tenant\n\nMembership\n→ RoleAssignment\n→ Role\n\nTenant\n→ Entitlement\n→ Feature\n</code></pre><p>関係に属性や履歴が必要になったら、関係を独立した概念にする。これは、B2B SaaSオントロジーでは何度も登場する重要なパターンです。</p><hr><h1 id=\"8-feature%E3%81%A8entitlement%E3%81%AE%E9%96%A2%E4%BF%82\">8. FeatureとEntitlementの関係</h1><p>Featureは、SaaS提供者が定義します。</p><p>Entitlementは、Tenantごとに発生します。</p><pre><code class=\"language-text\">Feature\n＝ 提供可能な機能の定義\n\nEntitlement\n＝ 特定Tenantへの提供状態\n</code></pre><p>たとえば、SaaS全体には1つの「SAML SSO Feature」が存在します。</p><pre><code class=\"language-text\">Feature:\nSAML SSO\n</code></pre><p>このFeatureに対して、Tenantごとに別々のEntitlementが作られます。</p><pre><code class=\"language-text\">Entitlement 1\n├── Tenant: 株式会社ABC\n├── Feature: SAML SSO\n└── status: active\n\nEntitlement 2\n├── Tenant: 株式会社XYZ\n├── Feature: SAML SSO\n└── status: inactive\n\nEntitlement 3\n├── Tenant: 株式会社DEF\n├── Feature: SAML SSO\n└── status: trial\n</code></pre><p>Featureは1つですが、EntitlementはTenantごとに異なります。</p><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph TD\n    F[SAML SSO<br>Feature]\n\n    E1[ABC SSO Entitlement<br>active]\n    E2[XYZ SSO Entitlement<br>inactive]\n    E3[DEF SSO Entitlement<br>trial]\n\n    T1[株式会社ABC<br>Tenant]\n    T2[株式会社XYZ<br>Tenant]\n    T3[株式会社DEF<br>Tenant]\n\n    T1 --> E1\n    E1 --> F\n\n    T2 --> E2\n    E2 --> F\n\n    T3 --> E3\n    E3 --> F\n\n</pre><!--kg-card-end: html--><hr><h1 id=\"9-plan%E3%83%BBsubscription%E3%83%BBentitlement%E3%81%AE%E9%81%95%E3%81%84\">9. Plan・Subscription・Entitlementの違い</h1><p>ここで、Plan、Subscription、Entitlementの違いを整理します。</p><h2 id=\"91-plan\">9.1 Plan</h2><pre><code class=\"language-text\">Plan\n＝ SaaS提供者が定義する標準商品\n</code></pre><p>例：</p><pre><code class=\"language-text\">Free\nStandard\nProfessional\nEnterprise\n</code></pre><h2 id=\"92-subscription\">9.2 Subscription</h2><pre><code class=\"language-text\">Subscription\n＝ TenantによるPlanの個別契約\n</code></pre><p>例：</p><pre><code class=\"language-text\">株式会社ABCのEnterprise Plan契約\n</code></pre><h2 id=\"93-entitlement\">9.3 Entitlement</h2><pre><code class=\"language-text\">Entitlement\n＝ Tenantに対する個別Featureの提供状態\n</code></pre><p>例：</p><pre><code class=\"language-text\">株式会社ABCテナントに、\nSAML SSO Featureが提供されている。\n</code></pre><p>図にすると、次のようになります。</p><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph LR\n    T[Tenant]\n    S[Subscription]\n    P[Plan]\n    E[Entitlement]\n    F[Feature]\n\n    T -->|hasSubscription| S\n    S -->|basedOnPlan| P\n    T -->|hasEntitlement| E\n    E -->|entitlesFeature| F\n    E -->|derivedFrom| S\n\n</pre><!--kg-card-end: html--><p>PlanとFeatureを商品定義として結び付け、SubscriptionとEntitlementをTenantごとの実態として結び付けます。</p><pre><code class=\"language-text\">商品定義側\nPlan → Feature\n\nTenant個別側\nTenant → Subscription → Plan\nTenant → Entitlement → Feature\n</code></pre><hr><h1 id=\"10-plan%E3%81%8B%E3%82%89feature%E3%82%92%E7%9B%B4%E6%8E%A5%E6%8E%A8%E8%AB%96%E3%81%97%E3%81%AA%E3%81%84%E7%90%86%E7%94%B1\">10. PlanからFeatureを直接推論しない理由</h1><p>ここは少し重要な設計判断です。</p><p>たとえば、Enterprise PlanがSAML SSOを含むなら、</p><pre><code class=\"language-text\">株式会社ABC\n→ Enterprise Planを契約\n→ SAML SSOを利用可能\n</code></pre><p>と自動的に推論したくなります。</p><p>概念的には自然ですが、実運用では注意が必要です。</p><p>SubscriptionがEnterprise Planに基づいていたとしても、次のようなケースがあります。</p><ul><li>Subscriptionがまだ開始前である</li><li>Subscriptionが停止中である</li><li>支払い遅延により一部機能が停止されている</li><li>SAML SSOの初期設定が未完了である</li><li>特定地域では提供対象外である</li><li>セキュリティ審査が完了していない</li><li>管理者が明示的に機能を無効化した</li><li>一時的な障害対応で停止している</li></ul><p>したがって、</p><pre><code class=\"language-text\">Planに含まれる\n</code></pre><p>ことと、</p><pre><code class=\"language-text\">現在そのTenantで利用可能\n</code></pre><p>であることは、完全には同じではありません。</p><p>この連載では、次のように分けます。</p><pre><code class=\"language-text\">Plan includesFeature Feature\n＝ 商品設計上、そのPlanにFeatureが含まれる\n\nEntitlement status active\n＝ 特定Tenantに、現在そのFeatureが提供されている\n</code></pre><p>最終的な利用可否では、Entitlementを参照します。PlanはEntitlementを生成する根拠にはなりますが、Entitlementそのものではありません。</p><hr><h1 id=\"11-%E5%B0%8F%E4%BC%91%E6%AD%A2%EF%BC%9A%E3%83%97%E3%83%A9%E3%83%B3%E3%81%AB%E5%90%AB%E3%81%BE%E3%82%8C%E3%82%8B%E6%9C%9D%E9%A3%9F%E3%81%AF%E5%AE%9F%E9%9A%9B%E3%81%AB%E9%A3%9F%E3%81%B9%E3%82%89%E3%82%8C%E3%82%8B%E3%81%AE%E3%81%8B%EF%BC%9F\">11. 小休止：プランに含まれる朝食は実際に食べられるのか？</h1><p>少し横道にそれますが、PlanとEntitlementの違いは、ホテルの宿泊プランにたとえることができます。</p><p>たとえば、「朝食付きプラン」という商品があります。</p><pre><code class=\"language-text\">Plan\n＝ 朝食付き宿泊プラン\n</code></pre><p>このプランには、朝食というサービスが含まれています。</p><pre><code class=\"language-text\">Plan includesFeature 朝食\n</code></pre><p>しかし、実際の宿泊者が今朝、朝食を利用できるかどうかは、もう少し具体的な状態に依存します。</p><ul><li>宿泊日は今日なのか</li><li>朝食券は有効か</li><li>朝食時間内か</li><li>予約がキャンセルされていないか</li><li>すでにチェックアウトしていないか</li></ul><p>メニューや商品説明に書かれていることと、個別の顧客が現時点で利用できることは違います。</p><pre><code class=\"language-text\">Plan\n＝ メニューや商品設計\n\nEntitlement\n＝ 個別顧客に発生している利用資格\n</code></pre><p>もちろん、SaaSはホテルではありませんが、</p><pre><code class=\"language-text\">商品に含まれていること\n</code></pre><p>と、</p><pre><code class=\"language-text\">個別顧客が現在利用できること\n</code></pre><p>を分ける感覚は、よく似ています。</p><hr><h1 id=\"12-feature%E3%81%AE%E7%B2%92%E5%BA%A6%E3%82%92%E3%81%A9%E3%81%86%E6%B1%BA%E3%82%81%E3%82%8B%E3%81%8B\">12. Featureの粒度をどう決めるか</h1><p>Featureを設計するとき、難しいのは粒度です。</p><p>たとえば、「監査ログ」を1つのFeatureにすることもできます。</p><pre><code class=\"language-text\">audit-log\n</code></pre><p>一方で、細かく分けることもできます。</p><pre><code class=\"language-text\">audit-log.view\naudit-log.export\naudit-log.retention\naudit-log.external-streaming\n</code></pre><p>どちらが正しいのでしょうか。絶対的な正解はありません。</p><p>Featureは、</p><pre><code class=\"language-text\">商品や提供条件として、独立して制御したい単位\n</code></pre><p>で切るのが基本です。</p><p>たとえば、次の条件なら別Featureにする意味があります。</p><ul><li>別料金で販売する</li><li>Planによって提供可否が異なる</li><li>Tenantごとに有効・無効を切り替える</li><li>段階的にリリースする</li><li>法的・地域的な提供制限がある</li><li>β機能として一部顧客だけに提供する</li><li>障害時に単独で停止したい</li></ul><p>逆に、常に一緒に提供され、単独で制御する必要がないものを細かく分けすぎると、Feature管理が複雑になります。</p><pre><code class=\"language-text\">細かすぎるFeature\n→ Entitlementが大量に増える\n→ 商品設計が読みにくくなる\n→ 判定ロジックが複雑になる\n</code></pre><p>Featureは、画面の数やAPIの数ではなく、</p><pre><code class=\"language-text\">契約・商品・提供制御上の意味\n</code></pre><p>で切るのがよいでしょう。</p><hr><h1 id=\"13-feature%E3%81%AB%E9%9A%8E%E5%B1%A4%E3%82%92%E6%8C%81%E3%81%9F%E3%81%9B%E3%82%8B\">13. Featureに階層を持たせる</h1><p>Featureには、親子関係を持たせられます。</p><p>たとえば、「AI機能」という大きなFeatureの下に、個別機能があるとします。</p><pre><code class=\"language-text\">AI機能\n├── AI文章生成\n├── AI要約\n├── AI分類\n└── AI検索\n</code></pre><p>これを次のように表現します。</p><pre><code class=\"language-text\">AI文章生成\nsubFeatureOf\nAI機能\n</code></pre><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph TD\n    AI[AI機能<br>Feature]\n    G[AI文章生成<br>Feature]\n    S[AI要約<br>Feature]\n    C[AI分類<br>Feature]\n    R[AI検索<br>Feature]\n\n    AI --> G\n    AI --> S\n    AI --> C\n    AI --> R\n\n</pre><!--kg-card-end: html--><p>ただし、親FeatureへのEntitlementが、すべての子Featureを自動的に有効にするかどうかは、別途ルールを決める必要があります。</p><pre><code class=\"language-text\">AI機能を利用可能\n→ AI文章生成も利用可能\n</code></pre><p>と推論したい場合もあります。</p><p>しかし、</p><pre><code class=\"language-text\">AI機能というカテゴリは有効だが、\nAI検索は別契約\n</code></pre><p>という商品設計もあり得ます。</p><p>そのため、Feature階層は最初から利用可否の継承を意味するとは限りません。</p><pre><code class=\"language-text\">subFeatureOf\n＝ 機能分類・構成上の親子関係\n\nEntitlementの継承\n＝ 別途定義するビジネスルール\n</code></pre><p>この2つは分けて考えます。</p><hr><h1 id=\"14-entitlement%E3%81%AE%E7%8A%B6%E6%85%8B\">14. Entitlementの状態</h1><p>Entitlementには、状態を持たせます。最小限の例として、次の状態を考えます。</p><pre><code class=\"language-text\">active\ninactive\ntrial\nsuspended\nexpired\n</code></pre><p>それぞれの意味は、次のようになります。</p><!--kg-card-begin: html--><table data-line=\"892\" class=\"code-line\" dir=\"auto\" style=\"border-collapse: collapse; margin-bottom: 0.7em;\"><thead data-line=\"892\" class=\"code-line\" dir=\"auto\"><tr data-line=\"892\" 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></tr></thead><tbody data-line=\"894\" class=\"code-line\" dir=\"auto\"><tr data-line=\"894\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">active</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">通常利用可能</td></tr><tr data-line=\"895\" 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);\">inactive</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=\"896\" 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);\">trial</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=\"897\" 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);\">suspended</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=\"898\" 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);\">expired</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></tbody></table><!--kg-card-end: html--><p>ただし、<code>trial</code>を状態として扱うか、付与理由として扱うかには議論があります。</p><p>たとえば、</p><pre><code class=\"language-text\">status: active\ngrantType: trial\n</code></pre><p>と分ける設計も可能です。</p><p>こちらの方が、状態と付与経路を明確に分離できます。</p><pre><code class=\"language-text\">status\n＝ 現在有効かどうか\n\ngrantType\n＝ どのような理由・経路で付与されたか\n</code></pre><p>本格的なモデルでは、次のように分ける方が扱いやすいでしょう。</p><pre><code class=\"language-text\">status:\nactive\ninactive\nsuspended\nexpired\n\ngrantType:\nplan\nadd_on\ntrial\nmanual\npromotion\nbeta\nmigration\n</code></pre><p>今回はFeatureとEntitlementに集中するため、<code>grantType</code>は簡単な文字列属性として扱います。</p><p><strong>以降、この記事では分離後の方を採用します。</strong> つまり、<code>status</code>の取りうる値は<code>active</code>・<code>inactive</code>・<code>suspended</code>・<code>expired</code>の4つで、<code>trial</code>は<code>grantType</code>側の値です。第8節でEntitlement 3の状態を<code>trial</code>と書きましたが、あれは<code>status: active</code> / <code>grantType: trial</code>と読み替えてください。第34節のSHACLも、この4値を前提にしています。</p><hr><h1 id=\"15-entitlement%E3%81%AE%E6%9C%89%E5%8A%B9%E6%9C%9F%E9%96%93\">15. Entitlementの有効期間</h1><p>Entitlementには、有効期間を持たせられます。</p><pre><code class=\"language-text\">validFrom\nvalidUntil\n</code></pre><p>たとえば、AI文章生成Featureを1か月だけ試用できるとします。</p><pre><code class=\"language-text\">Entitlement\n├── Tenant: 株式会社ABC\n├── Feature: AI文章生成\n├── status: active\n├── grantType: trial\n├── validFrom: 2026-08-01\n└── validUntil: 2026-08-31\n</code></pre><p>ここで注意したいのは、</p><pre><code class=\"language-text\">statusがactiveである\n</code></pre><p>ことと、</p><pre><code class=\"language-text\">現在日時が有効期間内である\n</code></pre><p>ことの両方を確認する必要がある点です。</p><p>Entitlementが利用可能かどうかは、概念的には次のように考えられます。</p><pre><code class=\"language-text\">利用可能なEntitlement\n＝\nstatusがactive\nかつ\n現在日時がvalidFrom以降\nかつ\nvalidUntilがない、または現在日時がvalidUntil以前\n</code></pre><p>この日時判定は、OWLの推論だけで扱うより、アプリケーションやルールエンジンで判定する方が現実的です。</p><hr><h1 id=\"16-entitlement%E3%81%AF%E3%81%A9%E3%81%93%E3%81%8B%E3%82%89%E6%9D%A5%E3%81%9F%E3%81%AE%E3%81%8B\">16. Entitlementはどこから来たのか</h1><p>Entitlementには、付与根拠があります。たとえば、次のような経路があります。</p><pre><code class=\"language-text\">Planに含まれていた\n追加契約された\n無料トライアルとして付与された\n営業担当者が個別に付与した\nβプログラム参加Tenantとして付与された\n旧契約から移行された\n</code></pre><p>そこで、EntitlementからSubscriptionを参照できるようにします。</p><pre><code class=\"language-text\">Entitlement\n→ derivedFromSubscription\n→ Subscription\n</code></pre><p>Enterprise Plan契約に基づくSAML SSO Entitlementなら、次のようになります。</p><pre><code class=\"language-text\">Entitlement\n├── beneficiary: 株式会社ABCテナント\n├── feature: SAML SSO\n├── status: active\n└── derivedFromSubscription:\n    株式会社ABCのEnterprise Subscription\n</code></pre><p>ただし、すべてのEntitlementがSubscriptionに由来するとは限りません。β提供や手動付与では、Subscriptionを参照しないこともあるため、<code>derivedFromSubscription</code>は必須にはしません。</p><hr><h1 id=\"17-%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%E3%81%A8%E9%96%A2%E4%BF%82\">17. 今回追加するクラスと関係</h1><p>今回、新たに追加するクラスは2つです。</p><pre><code class=\"language-text\">Feature\nEntitlement\n</code></pre><p>中心となる関係は次のとおりです。</p><!--kg-card-begin: html--><table data-line=\"1044\" class=\"code-line\" dir=\"auto\" style=\"border-collapse: collapse; margin-bottom: 0.7em;\"><thead data-line=\"1044\" class=\"code-line\" dir=\"auto\"><tr data-line=\"1044\" 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=\"1046\" class=\"code-line\" dir=\"auto\"><tr data-line=\"1046\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Plan</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">includesFeature</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Feature</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">標準商品にFeatureが含まれる</td></tr><tr data-line=\"1047\" 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);\">Feature</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);\">includedInPlan</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);\">Plan</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);\"><code style=\"font-family: Menlo, Monaco, &quot;Courier New&quot;, monospace; color: rgb(140, 140, 140); background-color: rgb(38, 38, 38); padding: 1px 3px; border-radius: 4px; font-size: 1em; line-height: 1.357em;\">includesFeature</code>の逆</td></tr><tr data-line=\"1048\" 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);\">hasEntitlement</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);\">Entitlement</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がFeature利用資格を持つ</td></tr><tr data-line=\"1049\" 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);\">Entitlement</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);\">entitledTenant</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);\">Entitlementの対象Tenant</td></tr><tr data-line=\"1050\" 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);\">Entitlement</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);\">entitlesFeature</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);\">Feature</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);\">利用可能になるFeature</td></tr><tr data-line=\"1051\" 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);\">Feature</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);\">hasFeatureEntitlement</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);\">Entitlement</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);\"><code style=\"font-family: Menlo, Monaco, &quot;Courier New&quot;, monospace; color: rgb(140, 140, 140); background-color: rgb(38, 38, 38); padding: 1px 3px; border-radius: 4px; font-size: 1em; line-height: 1.357em;\">entitlesFeature</code>の逆</td></tr><tr data-line=\"1052\" 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);\">Entitlement</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);\">derivedFromSubscription</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);\">Subscription</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);\">Entitlementの契約上の根拠</td></tr><tr data-line=\"1053\" 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);\">Feature</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);\">hasSubFeature</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);\">Feature</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);\">下位Featureを持つ</td></tr><tr data-line=\"1054\" 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);\">Feature</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);\">subFeatureOf</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);\">Feature</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);\">上位Featureに属する</td></tr></tbody></table><!--kg-card-end: html--><p>全体像は次のようになります。</p><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph TD\n    T[Tenant]\n    S[Subscription]\n    P[Plan]\n    E[Entitlement]\n    F[Feature]\n    SF[Sub Feature]\n\n    T -->|hasSubscription| S\n    S -->|basedOnPlan| P\n\n    P -->|includesFeature| F\n\n    T -->|hasEntitlement| E\n    E -->|entitlesFeature| F\n    E -->|derivedFromSubscription| S\n\n    F -->|hasSubFeature| SF\n\n</pre><!--kg-card-end: html--><hr><h1 id=\"18-%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\">18. オントロジー定義のTurtle</h1><p>ここから、FeatureとEntitlementを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\n#\n# Classes\n#\n\nsaas:Feature a owl:Class ;\n    rdfs:label \"Feature\"@en ,\n               \"機能\"@ja ;\n    rdfs:comment\n        \"SaaS提供者が提供可能な、識別可能な機能または能力。\"@ja .\n\nsaas:Entitlement a owl:Class ;\n    rdfs:label \"Entitlement\"@en ,\n               \"機能利用資格\"@ja ;\n    rdfs:comment\n        \"特定のTenantに、特定のFeatureを提供する権利、資格または状態。\"@ja .\n\n#\n# Plan and Feature\n#\n\nsaas:includesFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Plan ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"includes feature\"@en ,\n               \"機能を含む\"@ja ;\n    rdfs:comment\n        \"商品設計上、PlanにFeatureが含まれていることを表す。\"@ja .\n\nsaas:includedInPlan a owl:ObjectProperty ;\n    owl:inverseOf saas:includesFeature ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Plan ;\n    rdfs:label \"included in plan\"@en ,\n               \"プランに含まれる\"@ja .\n\n#\n# Tenant and Subscription\n#\n# 第1回で定義したプロパティですが、今回のEntitlementから参照するため、\n# また第37節・第38節のSHACLが依存するため、ここで再掲します。\n#\n\nsaas:hasSubscription a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Subscription ;\n    rdfs:label \"has subscription\"@en ,\n               \"契約を持つ\"@ja .\n\nsaas:subscriptionBelongsToTenant a owl:ObjectProperty ;\n    owl:inverseOf saas:hasSubscription ;\n    rdfs:domain saas:Subscription ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"subscription belongs to tenant\"@en ,\n               \"契約が属するテナント\"@ja .\n\nsaas:basedOnPlan a owl:ObjectProperty ;\n    rdfs:domain saas:Subscription ;\n    rdfs:range saas:Plan ;\n    rdfs:label \"based on plan\"@en ,\n               \"プランに基づく\"@ja .\n\n#\n# Tenant and Entitlement\n#\n\nsaas:hasEntitlement a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Entitlement ;\n    rdfs:label \"has entitlement\"@en ,\n               \"機能利用資格を持つ\"@ja .\n\nsaas:entitledTenant a owl:ObjectProperty ;\n    owl:inverseOf saas:hasEntitlement ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"entitled tenant\"@en ,\n               \"利用資格の対象テナント\"@ja .\n\nsaas:entitlesFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"entitles feature\"@en ,\n               \"利用可能にする機能\"@ja .\n\nsaas:hasFeatureEntitlement a owl:ObjectProperty ;\n    owl:inverseOf saas:entitlesFeature ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Entitlement ;\n    rdfs:label \"has feature entitlement\"@en ,\n               \"この機能に対する利用資格\"@ja .\n\n#\n# Entitlement source\n#\n\nsaas:derivedFromSubscription a owl:ObjectProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range saas:Subscription ;\n    rdfs:label \"derived from subscription\"@en ,\n               \"契約に基づく\"@ja ;\n    rdfs:comment\n        \"EntitlementがどのSubscriptionを根拠として付与されたかを表す。\"@ja .\n\n#\n# Feature hierarchy\n#\n\nsaas:hasSubFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"has sub-feature\"@en ,\n               \"下位機能を持つ\"@ja .\n\nsaas:subFeatureOf a owl:ObjectProperty ;\n    owl:inverseOf saas:hasSubFeature ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"sub-feature of\"@en ,\n               \"上位機能に属する\"@ja .\n\n#\n# Datatype properties\n#\n\nsaas:featureCode a owl:DatatypeProperty ;\n    rdfs:domain saas:Feature ;\n    rdfs:range xsd:string ;\n    rdfs:label \"feature code\"@en ,\n               \"機能コード\"@ja .\n\nsaas:entitlementStatus a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:string ;\n    rdfs:label \"entitlement status\"@en ,\n               \"利用資格状態\"@ja .\n\nsaas:grantType a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:string ;\n    rdfs:label \"grant type\"@en ,\n               \"付与種別\"@ja .\n\nsaas:validFrom a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:dateTime ;\n    rdfs:label \"valid from\"@en ,\n               \"有効開始日時\"@ja .\n\nsaas:validUntil a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:dateTime ;\n    rdfs:label \"valid until\"@en ,\n               \"有効終了日時\"@ja .\n\nsaas:grantedAt a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:dateTime ;\n    rdfs:label \"granted at\"@en ,\n               \"付与日時\"@ja .\n\nsaas:grantReason a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:string ;\n    rdfs:label \"grant reason\"@en ,\n               \"付与理由\"@ja .\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\n#\n# Classes\n#\n\nsaas:Feature a owl:Class ;\n    rdfs:label \"Feature\"@en ,\n               \"機能\"@ja ;\n    rdfs:comment\n        \"SaaS提供者が提供可能な、識別可能な機能または能力。\"@ja .\n\nsaas:Entitlement a owl:Class ;\n    rdfs:label \"Entitlement\"@en ,\n               \"機能利用資格\"@ja ;\n    rdfs:comment\n        \"特定のTenantに、特定のFeatureを提供する権利、資格または状態。\"@ja .\n\n#\n# Plan and Feature\n#\n\nsaas:includesFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Plan ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"includes feature\"@en ,\n               \"機能を含む\"@ja ;\n    rdfs:comment\n        \"商品設計上、PlanにFeatureが含まれていることを表す。\"@ja .\n\nsaas:includedInPlan a owl:ObjectProperty ;\n    owl:inverseOf saas:includesFeature ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Plan ;\n    rdfs:label \"included in plan\"@en ,\n               \"プランに含まれる\"@ja .\n\n#\n# Tenant and Subscription\n#\n# 第1回で定義したプロパティですが、今回のEntitlementから参照するため、\n# また第37節・第38節のSHACLが依存するため、ここで再掲します。\n#\n\nsaas:hasSubscription a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Subscription ;\n    rdfs:label \"has subscription\"@en ,\n               \"契約を持つ\"@ja .\n\nsaas:subscriptionBelongsToTenant a owl:ObjectProperty ;\n    owl:inverseOf saas:hasSubscription ;\n    rdfs:domain saas:Subscription ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"subscription belongs to tenant\"@en ,\n               \"契約が属するテナント\"@ja .\n\nsaas:basedOnPlan a owl:ObjectProperty ;\n    rdfs:domain saas:Subscription ;\n    rdfs:range saas:Plan ;\n    rdfs:label \"based on plan\"@en ,\n               \"プランに基づく\"@ja .\n\n#\n# Tenant and Entitlement\n#\n\nsaas:hasEntitlement a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Entitlement ;\n    rdfs:label \"has entitlement\"@en ,\n               \"機能利用資格を持つ\"@ja .\n\nsaas:entitledTenant a owl:ObjectProperty ;\n    owl:inverseOf saas:hasEntitlement ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range saas:Tenant ;\n    rdfs:label \"entitled tenant\"@en ,\n               \"利用資格の対象テナント\"@ja .\n\nsaas:entitlesFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"entitles feature\"@en ,\n               \"利用可能にする機能\"@ja .\n\nsaas:hasFeatureEntitlement a owl:ObjectProperty ;\n    owl:inverseOf saas:entitlesFeature ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Entitlement ;\n    rdfs:label \"has feature entitlement\"@en ,\n               \"この機能に対する利用資格\"@ja .\n\n#\n# Entitlement source\n#\n\nsaas:derivedFromSubscription a owl:ObjectProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range saas:Subscription ;\n    rdfs:label \"derived from subscription\"@en ,\n               \"契約に基づく\"@ja ;\n    rdfs:comment\n        \"EntitlementがどのSubscriptionを根拠として付与されたかを表す。\"@ja .\n\n#\n# Feature hierarchy\n#\n\nsaas:hasSubFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"has sub-feature\"@en ,\n               \"下位機能を持つ\"@ja .\n\nsaas:subFeatureOf a owl:ObjectProperty ;\n    owl:inverseOf saas:hasSubFeature ;\n    rdfs:domain saas:Feature ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"sub-feature of\"@en ,\n               \"上位機能に属する\"@ja .\n\n#\n# Datatype properties\n#\n\nsaas:featureCode a owl:DatatypeProperty ;\n    rdfs:domain saas:Feature ;\n    rdfs:range xsd:string ;\n    rdfs:label \"feature code\"@en ,\n               \"機能コード\"@ja .\n\nsaas:entitlementStatus a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:string ;\n    rdfs:label \"entitlement status\"@en ,\n               \"利用資格状態\"@ja .\n\nsaas:grantType a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:string ;\n    rdfs:label \"grant type\"@en ,\n               \"付与種別\"@ja .\n\nsaas:validFrom a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:dateTime ;\n    rdfs:label \"valid from\"@en ,\n               \"有効開始日時\"@ja .\n\nsaas:validUntil a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:dateTime ;\n    rdfs:label \"valid until\"@en ,\n               \"有効終了日時\"@ja .\n\nsaas:grantedAt a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:dateTime ;\n    rdfs:label \"granted at\"@en ,\n               \"付与日時\"@ja .\n\nsaas:grantReason a owl:DatatypeProperty ;\n    rdfs:domain saas:Entitlement ;\n    rdfs:range xsd:string ;\n    rdfs:label \"grant reason\"@en ,\n               \"付与理由\"@ja .\n\n\n</script><!--kg-card-end: html--><p></p><p><code>validFrom</code>と<code>validUntil</code>には、<code>rdfs:domain saas:Entitlement</code>を付けています。名前は汎用的ですが、<strong>このオントロジーではEntitlement専用</strong>です。</p><p>RDFSの<code>rdfs:domain</code>は「このプロパティを持つものは、そのクラスである」という推論規則なので、うっかり他のクラスに使うと、推論器がそれをEntitlementだと判定してしまいます。</p><pre><code class=\"language-text\">ex:subscription-x a saas:Subscription ;\n    saas:validFrom \"2026-01-01T00:00:00+09:00\"^^xsd:dateTime .\n\n↓ OWL推論後\n\nex:subscription-x は Subscription かつ Entitlement\n</code></pre><p>ContractやSubscriptionなど、他のクラスにも有効期間を持たせたくなったときは、<code>validFrom</code>を使い回さず、<code>contractStart</code>や<code>subscriptionStart</code>のような別名を用意してください。</p><hr><h1 id=\"19-feature%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E6%8C%81%E3%81%9F%E3%81%9B%E3%82%8B%E7%90%86%E7%94%B1\">19. Featureコードを持たせる理由</h1><p>Featureには、人間向けの名称とは別に、安定したFeatureコードを持たせます。</p><pre><code class=\"language-turtle\">ex:feature-saml-sso\n    saas:name \"SAML SSO\" ;\n    saas:featureCode \"security.saml-sso\" .\n</code></pre><p>名称は、後から変更される可能性があります。</p><pre><code class=\"language-text\">旧名称:\nAI文章生成\n\n新名称:\nAIライティングアシスタント\n</code></pre><p>また、多言語化されることもあります。</p><pre><code class=\"language-text\">日本語:\nAI文章生成\n\n英語:\nAI Writing Assistant\n</code></pre><p>一方、アプリケーションの判定に使う識別子は、簡単には変えたくありません。</p><pre><code class=\"language-text\">ai.text-generation\n</code></pre><p>そのため、</p><pre><code class=\"language-text\">name\n＝ 人間が読む表示名\n\nfeatureCode\n＝ システムが参照する安定識別子\n</code></pre><p>として分けます。</p><p>URIそのものを安定識別子として使う方法もありますが、アプリケーションコードや設定ファイルでは、短いFeatureコードが便利な場合があります。</p><hr><h1 id=\"20-feature%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\">20. Featureの具体的なインスタンス</h1><p>株式会社ABCが利用するプロジェクト管理SaaSを考えます。このSaaSには、次のFeatureがあります。</p><pre><code class=\"language-text\">基本プロジェクト管理\nCSVエクスポート\nAPIアクセス\nSAML SSO\n監査ログ\nAI文章生成\nAI要約\n</code></pre><p>Turtleでは、次のように表現します。</p><pre><code class=\"language-turtle\">@prefix saas: &lt;https://example.com/ontology/saas#&gt; .\n@prefix ex:   &lt;https://example.com/data/&gt; .\n\n#\n# Core features\n#\n\nex:feature-project-management a saas:Feature ;\n    saas:name \"基本プロジェクト管理\" ;\n    saas:featureCode \"core.project-management\" .\n\nex:feature-csv-export a saas:Feature ;\n    saas:name \"CSVエクスポート\" ;\n    saas:featureCode \"data.csv-export\" .\n\nex:feature-api-access a saas:Feature ;\n    saas:name \"APIアクセス\" ;\n    saas:featureCode \"integration.api-access\" .\n\nex:feature-saml-sso a saas:Feature ;\n    saas:name \"SAML SSO\" ;\n    saas:featureCode \"security.saml-sso\" .\n\nex:feature-audit-log a saas:Feature ;\n    saas:name \"監査ログ\" ;\n    saas:featureCode \"security.audit-log\" .\n\n#\n# AI feature hierarchy\n#\n\nex:feature-ai a saas:Feature ;\n    saas:name \"AI機能\" ;\n    saas:featureCode \"ai\" ;\n    saas:hasSubFeature\n        ex:feature-ai-text-generation,\n        ex:feature-ai-summarization .\n\nex:feature-ai-text-generation a saas:Feature ;\n    saas:name \"AI文章生成\" ;\n    saas:featureCode \"ai.text-generation\" ;\n    saas:subFeatureOf ex:feature-ai .\n\nex:feature-ai-summarization a saas:Feature ;\n    saas:name \"AI要約\" ;\n    saas:featureCode \"ai.summarization\" ;\n    saas:subFeatureOf ex:feature-ai .\n</code></pre><!--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\n#\n# Core features\n#\n\nex:feature-project-management a saas:Feature ;\n    saas:name \"基本プロジェクト管理\" ;\n    saas:featureCode \"core.project-management\" .\n\nex:feature-csv-export a saas:Feature ;\n    saas:name \"CSVエクスポート\" ;\n    saas:featureCode \"data.csv-export\" .\n\nex:feature-api-access a saas:Feature ;\n    saas:name \"APIアクセス\" ;\n    saas:featureCode \"integration.api-access\" .\n\nex:feature-saml-sso a saas:Feature ;\n    saas:name \"SAML SSO\" ;\n    saas:featureCode \"security.saml-sso\" .\n\nex:feature-audit-log a saas:Feature ;\n    saas:name \"監査ログ\" ;\n    saas:featureCode \"security.audit-log\" .\n\n#\n# AI feature hierarchy\n#\n\nex:feature-ai a saas:Feature ;\n    saas:name \"AI機能\" ;\n    saas:featureCode \"ai\" ;\n    saas:hasSubFeature\n        ex:feature-ai-text-generation,\n        ex:feature-ai-summarization .\n\nex:feature-ai-text-generation a saas:Feature ;\n    saas:name \"AI文章生成\" ;\n    saas:featureCode \"ai.text-generation\" ;\n    saas:subFeatureOf ex:feature-ai .\n\nex:feature-ai-summarization a saas:Feature ;\n    saas:name \"AI要約\" ;\n    saas:featureCode \"ai.summarization\" ;\n    saas:subFeatureOf ex:feature-ai .\n\n\n</script><!--kg-card-end: html--><hr><p></p><h1 id=\"21-plan%E3%81%AB%E5%90%AB%E3%81%BE%E3%82%8C%E3%82%8Bfeature%E3%81%AE%E5%85%B7%E4%BD%93%E4%BE%8B\">21. Planに含まれるFeatureの具体例</h1><p>次に、Standard PlanとEnterprise Planを定義します。</p><pre><code class=\"language-turtle\">#\n# Plans\n#\n\nex:plan-standard a saas:Plan ;\n    saas:name \"Standard Plan\" ;\n    saas:includesFeature\n        ex:feature-project-management,\n        ex:feature-csv-export,\n        ex:feature-api-access .\n\nex:plan-enterprise a saas:Plan ;\n    saas:name \"Enterprise Plan\" ;\n    saas:includesFeature\n        ex:feature-project-management,\n        ex:feature-csv-export,\n        ex:feature-api-access,\n        ex:feature-saml-sso,\n        ex:feature-audit-log,\n        ex:feature-ai-text-generation,\n        ex:feature-ai-summarization .\n</code></pre><p>これにより、商品定義として次のことを表現できます。</p><pre><code class=\"language-text\">Standard Planには、\n基本プロジェクト管理、\nCSVエクスポート、\nAPIアクセスが含まれる。\n\nEnterprise Planには、\nそれらに加えて、\nSAML SSO、\n監査ログ、\nAI文章生成、\nAI要約が含まれる。\n</code></pre><p>ただし、これはまだ商品定義であり、特定Tenantで実際に利用可能かどうかは、Entitlementで表現します。</p><hr><h1 id=\"22-%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BEabc%E3%81%AEsubscription\">22. 株式会社ABCのSubscription</h1><p>株式会社ABCテナントは、Standard Planを契約しているとします。</p><p>第1回の例では、同じテナントがProfessional Planを契約している想定でしたが、あれは第1回だけの最小例です。以降はこのStandard契約で読み進めてください(第1回のTurtleをそのまま残していると、テナントに2つのSubscriptionがぶら下がった状態になります)。</p><pre><code class=\"language-turtle\">ex:subscription-abc-standard a saas:Subscription ;\n    saas:name \"株式会社ABC Standard契約\" ;\n    saas:status \"active\" ;\n    saas:basedOnPlan ex:plan-standard .\n\nex:tenant-abc a saas:Tenant ;\n    saas:name \"株式会社ABCテナント\" ;\n    saas:hasSubscription ex:subscription-abc-standard .\n</code></pre><p>Standard Planには、次のFeatureが含まれています。</p><pre><code class=\"language-text\">基本プロジェクト管理\nCSVエクスポート\nAPIアクセス\n</code></pre><p>そのため、株式会社ABCテナントには、これらに対応するEntitlementが作られます。</p><hr><h1 id=\"23-%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BEabc%E3%81%AEentitlement\">23. 株式会社ABCのEntitlement</h1><pre><code class=\"language-turtle\">@prefix xsd: &lt;http://www.w3.org/2001/XMLSchema#&gt; .\n\n#\n# Entitlements derived from Standard Plan\n#\n\nex:entitlement-abc-project-management a saas:Entitlement ;\n    saas:entitledTenant ex:tenant-abc ;\n    saas:entitlesFeature ex:feature-project-management ;\n    saas:derivedFromSubscription ex:subscription-abc-standard ;\n    saas:entitlementStatus \"active\" ;\n    saas:grantType \"plan\" ;\n    saas:grantedAt \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime ;\n    saas:validFrom \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime .\n\nex:entitlement-abc-csv-export a saas:Entitlement ;\n    saas:entitledTenant ex:tenant-abc ;\n    saas:entitlesFeature ex:feature-csv-export ;\n    saas:derivedFromSubscription ex:subscription-abc-standard ;\n    saas:entitlementStatus \"active\" ;\n    saas:grantType \"plan\" ;\n    saas:grantedAt \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime ;\n    saas:validFrom \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime .\n\nex:entitlement-abc-api-access a saas:Entitlement ;\n    saas:entitledTenant ex:tenant-abc ;\n    saas:entitlesFeature ex:feature-api-access ;\n    saas:derivedFromSubscription ex:subscription-abc-standard ;\n    saas:entitlementStatus \"active\" ;\n    saas:grantType \"plan\" ;\n    saas:grantedAt \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime ;\n    saas:validFrom \"2026-08-01T00:00:00+09:00\"^^xsd:dateTime .\n</code></pre><p>Tenant側からもEntitlementを明示できます。</p><pre><code class=\"language-turtle\">ex:tenant-abc\n    saas:hasEntitlement\n        ex:entitlement-abc-project-management,\n        ex:entitlement-abc-csv-export,\n        ex:entitlement-abc-api-access .\n</code></pre><p><code>hasEntitlement</code>と<code>entitledTenant</code>は逆プロパティとして定義しているため、推論環境では片方だけを書いても、もう片方を導出できます。</p><p>ただし、「推論環境」は自動的には用意されません。たとえば<code>pyshacl</code>で検証する場合、オントロジーを<code>-e</code>で渡し、<code>-i owlrl</code>でOWL推論を有効にする必要があります。</p><pre><code class=\"language-bash\">pyshacl -s shapes.ttl -e ontology.ttl -i owlrl data.ttl\n</code></pre><p><code>-i</code>を付けない、あるいは<code>-i rdfs</code>にした場合、<code>owl:inverseOf</code>は効きません。<code>hasEntitlement</code>だけを書いたデータは、第34節のShape(<code>entitledTenant</code>が必須)で違反として報告されます。</p><hr><h1 id=\"24-standard-plan%E3%81%A0%E3%81%8Cai%E6%A9%9F%E8%83%BD%E3%82%92%E5%80%8B%E5%88%A5%E6%8F%90%E4%BE%9B%E3%81%99%E3%82%8B\">24. Standard PlanだがAI機能を個別提供する</h1><p>ここで、株式会社ABCだけにAI文章生成を無料トライアルとして提供するとします。Standard PlanにはAI文章生成は含まれていませんが、Tenant個別のEntitlementを作ることで提供できます。</p><pre><code class=\"language-turtle\">ex:entitlement-abc-ai-text-generation-trial\n    a saas:Entitlement ;\n\n    saas:entitledTenant ex:tenant-abc ;\n\n    saas:entitlesFeature\n        ex:feature-ai-text-generation ;\n\n    saas:entitlementStatus \"active\" ;\n\n    saas:grantType \"trial\" ;\n\n    saas:grantReason\n        \"Enterprise Plan導入検討のためのトライアル\" ;\n\n    saas:grantedAt\n        \"2026-08-10T09:00:00+09:00\"^^xsd:dateTime ;\n\n    saas:validFrom\n        \"2026-08-10T09:00:00+09:00\"^^xsd:dateTime ;\n\n    saas:validUntil\n        \"2099-12-31T23:59:59+09:00\"^^xsd:dateTime .\n</code></pre><p>このEntitlementはStandard Subscriptionに由来するものではないため、<code>derivedFromSubscription</code>を持たせていません。</p><p>これにより、次の状態を表現できます。</p><pre><code class=\"language-text\">株式会社ABCはStandard Planを契約している。\n\nStandard PlanにはAI文章生成は含まれない。\n\nしかし、株式会社ABCには、\nAI文章生成のTrial Entitlementがある。\n\nしたがって、トライアル期間中は利用可能である。\n</code></pre><p>PlanとEntitlementを分ける大きな利点です。</p><hr><h1 id=\"25-feature%E3%81%8C%E4%B8%80%E6%99%82%E5%81%9C%E6%AD%A2%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E4%BE%8B\">25. Featureが一時停止されている例</h1><p>株式会社ABCはAPIアクセスFeatureを契約上利用できますが、セキュリティ上の理由で一時的に停止されたとします。</p><p>RDFグラフは同じ主語・述語へ新しい値を書くだけでは古い値を上書きしません。<code>active</code>を残したまま<code>suspended</code>を追加すると、状態が2つあるデータになります。ここではSPARQL Updateで旧状態を削除してから、新状態を追加します。</p><pre><code class=\"language-sparql\">PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\nPREFIX ex:   &lt;https://example.com/data/&gt;\n\nDELETE {\n    ex:entitlement-abc-api-access\n        saas:entitlementStatus ?oldStatus .\n}\nINSERT {\n    ex:entitlement-abc-api-access\n        saas:entitlementStatus \"suspended\" ;\n        saas:grantReason\n            \"APIキー漏えいの疑いにより一時停止\" .\n}\nWHERE {\n    OPTIONAL {\n        ex:entitlement-abc-api-access\n            saas:entitlementStatus ?oldStatus .\n    }\n}\n</code></pre><p>このクエリはSELECTではなくUPDATEなので、<code>sparqlquery</code>コマンドでは実行できません。</p><pre><code class=\"language-console\">$ sparqlquery data.ttl -qf update.rq -f csv\npyparsing.exceptions.ParseException: Expected {SelectQuery | ConstructQuery |\nDescribeQuery | AskQuery}, found 'DELETE'\n</code></pre><p>RDFLibから実行し、結果をファイルに書き戻します。</p><pre><code class=\"language-python\"># update.py\nimport rdflib\n\ng = rdflib.Graph()\ng.parse(\"data.ttl\", format=\"turtle\")\ng.update(open(\"update.rq\").read())\ng.serialize(\"data.ttl\", format=\"turtle\")\n</code></pre><pre><code class=\"language-bash\">python update.py\n</code></pre><p>この場合、</p><pre><code class=\"language-text\">Standard PlanにはAPIアクセスが含まれる。\n\nSubscriptionも有効である。\n\nしかし、APIアクセスEntitlementはsuspendedである。\n</code></pre><p>したがって、現在はAPIアクセスを許可しません。</p><p>このように、Entitlementを明示的に持つことで、</p><pre><code class=\"language-text\">商品上は提供対象\nしかし\n運用上は一時停止\n</code></pre><p>という状態を表現できます。</p><hr><h1 id=\"26-permission%E3%81%A8entitlement%E3%82%92%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%81%9F%E5%85%B7%E4%BD%93%E4%BE%8B\">26. PermissionとEntitlementを組み合わせた具体例</h1><p>AI文章生成を実行するには、次の2条件が必要だとします。</p><pre><code class=\"language-text\">Tenant側:\nai.text-generation Featureの有効なEntitlement\n\nUser側:\nai.generate Permission\n</code></pre><p>RoleとPermissionを次のように定義します。</p><pre><code class=\"language-turtle\">ex:permission-ai-generate a saas:Permission ;\n    saas:name \"ai.generate\" .\n\nex:role-ai-user a saas:Role ;\n    saas:name \"AI User\" ;\n    saas:grantsPermission\n        ex:permission-ai-generate .\n</code></pre><p>山田太郎さんに、営業Workspaceの範囲でAI User Roleを付与します。</p><pre><code class=\"language-turtle\">ex:role-assignment-yamada-ai-user\n    a saas:RoleAssignment ;\n\n    saas:assignedTo\n        ex:membership-yamada-abc ;\n\n    saas:assignedRole\n        ex:role-ai-user ;\n\n    saas:appliesToWorkspace\n        ex:workspace-sales ;\n\n    saas:assignedAt\n        \"2026-08-10T09:10:00+09:00\"^^xsd:dateTime .\n</code></pre><p>さらに、株式会社ABCテナントにはAI文章生成のTrial Entitlementがあります。</p><pre><code class=\"language-text\">Tenant:\nAI文章生成を利用可能\n\n山田太郎:\n営業Workspaceでai.generateを実行可能\n</code></pre><p>したがって、山田太郎さんは営業WorkspaceでAI文章生成を利用できますが、佐藤花子さんにAI User Roleがなければ、Tenantとしては利用可能でも、佐藤花子さんは利用できません。</p><hr><h1 id=\"27-%E5%88%A9%E7%94%A8%E5%8F%AF%E5%90%A6%E3%81%AE%E5%88%A4%E5%AE%9A%E3%83%95%E3%83%AD%E3%83%BC\">27. 利用可否の判定フロー</h1><p>AI文章生成のようなFeatureを利用できるかは、次の順番で確認できます。</p><pre><code class=\"language-text\">1. Userを特定する\n\n2. 対象Tenantに対する\n   有効なMembershipを取得する\n\n3. 対象WorkspaceまたはTenant Scopeで、\n   必要なRoleAssignmentを確認する\n\n4. Roleが必要なPermissionを持つか確認する\n\n5. Tenantに対象Featureの\n   有効なEntitlementがあるか確認する\n\n6. Entitlementの有効期間内か確認する\n\n7. すべて満たせば操作を許可する\n</code></pre><p>概念図は次のようになります。</p><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph TD\n    U[User]\n    M[Membership]\n    RA[RoleAssignment]\n    R[Role]\n    P[Permission]\n\n    T[Tenant]\n    E[Entitlement]\n    F[Feature]\n\n    U --> M\n    M --> RA\n    RA --> R\n    R --> P\n\n    M --> T\n    T --> E\n    E --> F\n\n    P --> D{Permissionあり?}\n    F --> G{Entitlement有効?}\n\n    D -->|Yes| A{両方満たす?}\n    G -->|Yes| A\n\n    A -->|Yes| OK[操作許可]\n\n</pre><!--kg-card-end: html--><p></p><p>最終的な判断は、次の形です。</p><pre><code class=\"language-text\">利用者に許可されている\nかつ\nTenantに提供されている\n</code></pre><hr><h1 id=\"28-%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\">28. 全インスタンスの関係図</h1><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph TD\n    T[株式会社ABC<br>Tenant]\n    S[Standard契約<br>Subscription]\n    P[Standard Plan]\n\n    E1[CSV Export Entitlement<br>active]\n    E2[API Access Entitlement<br>suspended]\n    E3[AI Text Trial Entitlement<br>active]\n\n    F1[CSV Export<br>Feature]\n    F2[API Access<br>Feature]\n    F3[AI Text Generation<br>Feature]\n\n    M[山田Membership]\n    RA[AI User RoleAssignment]\n    R[AI User Role]\n    PM[ai.generate Permission]\n\n    T --> S\n    S --> P\n\n    P -->|includesFeature| F1\n    P -->|includesFeature| F2\n\n    T --> E1\n    E1 --> F1\n    E1 --> S\n\n    T --> E2\n    E2 --> F2\n    E2 --> S\n\n    T --> E3\n    E3 --> F3\n\n    M --> T\n    M --> RA\n    RA --> R\n    R --> PM\n\n</pre><!--kg-card-end: html--><p></p><p>この図から、次のことが分かります。</p><pre><code class=\"language-text\">CSVエクスポート\n→ Planに含まれる\n→ Entitlementもactive\n\nAPIアクセス\n→ Planに含まれる\n→ しかしEntitlementはsuspended\n\nAI文章生成\n→ Planには含まれない\n→ しかしTrial Entitlementがactive\n</code></pre><hr><h1 id=\"29-sparql%E3%81%A7%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B\">29. SPARQLで問い合わせる</h1><p>この節以降の問い合わせは、<strong>第25節の更新を適用したあとのグラフ</strong>を前提にします。つまり、APIアクセスのEntitlementは<code>suspended</code>になっている状態です。まだ適用していない場合は、先に第25節の<code>update.py</code>を実行してください。</p><h2 id=\"291-plan%E3%81%AB%E5%90%AB%E3%81%BE%E3%82%8C%E3%82%8Bfeature%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B\">29.1 Planに含まれるFeatureを取得する</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 ?feature ?featureName ?featureCode\nWHERE {\n    ex:plan-standard\n        saas:includesFeature ?feature .\n\n    ?feature\n        saas:name ?featureName ;\n        saas:featureCode ?featureCode .\n}\nORDER BY ?featureCode\n</code></pre><p>結果は次のとおりです。</p><pre><code class=\"language-text\">feature,featureName,featureCode\nhttps://example.com/data/feature-project-management,基本プロジェクト管理,core.project-management\nhttps://example.com/data/feature-csv-export,CSVエクスポート,data.csv-export\nhttps://example.com/data/feature-api-access,APIアクセス,integration.api-access\n</code></pre><p>以降の節では、見やすさのために主要な列だけを抜き出して示します。</p><p>これは商品定義を問い合わせています。まだ、株式会社ABCで現在利用可能かどうかを調べているわけではありません。</p><hr><h2 id=\"292-tenant%E3%81%8C%E6%8C%81%E3%81%A4%E3%81%99%E3%81%B9%E3%81%A6%E3%81%AEentitlement%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B\">29.2 Tenantが持つすべてのEntitlementを取得する</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\n    ?entitlement\n    ?feature\n    ?featureName\n    ?status\n    ?grantType\nWHERE {\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant ex:tenant-abc ;\n        saas:entitlesFeature ?feature ;\n        saas:entitlementStatus ?status ;\n        saas:grantType ?grantType .\n\n    ?feature saas:name ?featureName .\n}\nORDER BY ?featureName\n</code></pre><p>結果のイメージは次のようになります。</p><!--kg-card-begin: html--><table data-line=\"1905\" class=\"code-line\" dir=\"auto\" style=\"border-collapse: collapse; margin-bottom: 0.7em;\"><thead data-line=\"1905\" class=\"code-line\" dir=\"auto\"><tr data-line=\"1905\" 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);\">Feature</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);\">status</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);\">grantType</th></tr></thead><tbody data-line=\"1907\" class=\"code-line\" dir=\"auto\"><tr data-line=\"1907\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">AI文章生成</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">active</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">trial</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);\">APIアクセス</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);\">suspended</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);\">plan</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);\">CSVエクスポート</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);\">active</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);\">plan</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);\">基本プロジェクト管理</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);\">active</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);\">plan</td></tr></tbody></table><!--kg-card-end: html--><hr><h2 id=\"293-tenant%E3%81%A7%E6%9C%89%E5%8A%B9%E3%81%AAfeature%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B\">29.3 Tenantで有効なFeatureを取得する</h2><p>まずは日時を考慮せず、状態だけで判定します。</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 DISTINCT\n    ?feature\n    ?featureName\n    ?featureCode\nWHERE {\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant ex:tenant-abc ;\n        saas:entitlesFeature ?feature ;\n        saas:entitlementStatus \"active\" .\n\n    ?feature\n        saas:name ?featureName ;\n        saas:featureCode ?featureCode .\n}\nORDER BY ?featureCode\n</code></pre><p>この問い合わせでは、Entitlementが<code>suspended</code>のため、APIアクセスは取得されません。</p><hr><h2 id=\"294-%E7%8F%BE%E5%9C%A8%E6%99%82%E5%88%BB%E3%82%92%E8%80%83%E6%85%AE%E3%81%97%E3%81%A6%E6%9C%89%E5%8A%B9%E3%81%AAfeature%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B\">29.4 現在時刻を考慮して有効なFeatureを取得する</h2><p>SPARQLの<code>NOW()</code>を使って、有効期間も確認します。</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 DISTINCT\n    ?feature\n    ?featureName\n    ?featureCode\nWHERE {\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant ex:tenant-abc ;\n        saas:entitlesFeature ?feature ;\n        saas:entitlementStatus \"active\" .\n\n    OPTIONAL {\n        ?entitlement saas:validFrom ?validFrom .\n    }\n\n    OPTIONAL {\n        ?entitlement saas:validUntil ?validUntil .\n    }\n\n    FILTER (\n        !BOUND(?validFrom) ||\n        ?validFrom &lt;= NOW()\n    )\n\n    FILTER (\n        !BOUND(?validUntil) ||\n        NOW() &lt;= ?validUntil\n    )\n\n    ?feature\n        saas:name ?featureName ;\n        saas:featureCode ?featureCode .\n}\nORDER BY ?featureCode\n</code></pre><p>これにより、次の条件を満たすEntitlementだけを取得します。</p><pre><code class=\"language-text\">statusがactive\n\nvalidFromがない、\nまたは現在時刻がvalidFrom以降\n\nvalidUntilがない、\nまたは現在時刻がvalidUntil以前\n</code></pre><hr><h2 id=\"295-%E7%89%B9%E5%AE%9Afeature%E3%82%92tenant%E3%81%8C%E5%88%A9%E7%94%A8%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%8B%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B\">29.5 特定FeatureをTenantが利用できるか確認する</h2><p>株式会社ABCがAI文章生成を利用可能か確認します。</p><pre><code class=\"language-sparql\">PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\nPREFIX ex:   &lt;https://example.com/data/&gt;\n\nASK {\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant ex:tenant-abc ;\n        saas:entitlesFeature\n            ex:feature-ai-text-generation ;\n        saas:entitlementStatus \"active\" .\n\n    OPTIONAL {\n        ?entitlement saas:validFrom ?validFrom .\n    }\n\n    OPTIONAL {\n        ?entitlement saas:validUntil ?validUntil .\n    }\n\n    FILTER (\n        !BOUND(?validFrom) ||\n        ?validFrom &lt;= NOW()\n    )\n\n    FILTER (\n        !BOUND(?validUntil) ||\n        NOW() &lt;= ?validUntil\n    )\n}\n</code></pre><p>利用可能であれば、結果は<code>true</code>になります。</p><hr><h2 id=\"296-entitlement%E3%81%AE%E4%BB%98%E4%B8%8E%E6%A0%B9%E6%8B%A0%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B\">29.6 Entitlementの付与根拠を確認する</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\n    ?featureName\n    ?grantType\n    ?subscriptionName\n    ?reason\nWHERE {\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant ex:tenant-abc ;\n        saas:entitlesFeature ?feature ;\n        saas:grantType ?grantType .\n\n    ?feature saas:name ?featureName .\n\n    OPTIONAL {\n        ?entitlement\n            saas:derivedFromSubscription ?subscription .\n\n        ?subscription\n            saas:name ?subscriptionName .\n    }\n\n    OPTIONAL {\n        ?entitlement\n            saas:grantReason ?reason .\n    }\n}\nORDER BY ?featureName\n</code></pre><p>この問い合わせにより、次の違いを確認できます。</p><pre><code class=\"language-text\">CSVエクスポート\n→ Standard Subscriptionに基づく\n\nAI文章生成\n→ Trialとして個別付与\n</code></pre><hr><h1 id=\"30-permission%E3%81%A8entitlement%E3%81%AE%E4%B8%A1%E6%96%B9%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8Bsparql\">30. PermissionとEntitlementの両方を確認するSPARQL</h1><p>山田太郎さんが営業WorkspaceでAI文章生成を利用できるか確認します。必要な条件は、次のとおりです。</p><pre><code class=\"language-text\">山田太郎のMembershipが、\n株式会社ABCテナントに対して有効である。\n\nそのMembershipに、\n営業WorkspaceをScopeとするRoleAssignmentがある。\n\nそのRoleがai.generate Permissionを持つ。\n\n株式会社ABCテナントに、\nAI文章生成Featureの有効なEntitlementがある。\n</code></pre><p>第27節の判定フローのうち、「有効なMembershipを取得する」という手順を省略しないことが重要です。RoleAssignmentのURIを指定するだけでは、そのMembershipが停止されていないか、そもそも対象Tenantに属しているかを確認できません。</p><p>なお、<code>ex:membership-yamada-abc</code>や<code>ex:workspace-sales</code>は前回のデータで定義したものなので、この問い合わせは前回のデータと今回のデータを合わせたグラフに対して実行します。</p><p>SPARQLでは次のようになります。</p><pre><code class=\"language-sparql\">PREFIX saas: &lt;https://example.com/ontology/saas#&gt;\nPREFIX ex:   &lt;https://example.com/data/&gt;\n\nASK {\n    #\n    # Membership check\n    #\n\n    ex:membership-yamada-abc\n        a saas:Membership ;\n        saas:membershipBelongsToTenant ex:tenant-abc ;\n        saas:status \"active\" .\n\n    #\n    # Permission check\n    #\n\n    ?roleAssignment\n        a saas:RoleAssignment ;\n        saas:assignedTo\n            ex:membership-yamada-abc ;\n        saas:appliesToWorkspace\n            ex:workspace-sales ;\n        saas:assignedRole ?role .\n\n    ?role\n        saas:grantsPermission\n            ex:permission-ai-generate .\n\n    #\n    # Entitlement check\n    #\n\n    ?entitlement\n        a saas:Entitlement ;\n        saas:entitledTenant\n            ex:tenant-abc ;\n        saas:entitlesFeature\n            ex:feature-ai-text-generation ;\n        saas:entitlementStatus \"active\" .\n\n    OPTIONAL {\n        ?entitlement saas:validFrom ?validFrom .\n    }\n\n    OPTIONAL {\n        ?entitlement saas:validUntil ?validUntil .\n    }\n\n    FILTER (\n        !BOUND(?validFrom) ||\n        ?validFrom &lt;= NOW()\n    )\n\n    FILTER (\n        !BOUND(?validUntil) ||\n        NOW() &lt;= ?validUntil\n    )\n}\n</code></pre><p>この問い合わせは、PermissionとEntitlementを明確に分けています。</p><pre><code class=\"language-text\">Role・Permission側\n＝ 山田太郎が操作してよいか\n\nEntitlement側\n＝ 株式会社ABCに機能が提供されているか\n</code></pre><p>両方がそろったときだけ、<code>true</code>になります。</p><hr><h1 id=\"31-%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%E5%88%A9%E7%94%A8%E5%8F%AF%E8%83%BDfeature%E3%81%AE%E6%8E%A8%E8%AB%96\">31. プロパティチェーンによる利用可能Featureの推論</h1><p>Entitlementの状態や有効期間をいったん無視すれば、次の関係から、</p><pre><code class=\"language-text\">Tenant\n→ hasEntitlement\n→ Entitlement\n→ entitlesFeature\n→ Feature\n</code></pre><p>TenantとFeatureの直接関係を推論できます。</p><pre><code class=\"language-turtle\">saas:hasEntitledFeature a owl:ObjectProperty ;\n    rdfs:domain saas:Tenant ;\n    rdfs:range saas:Feature ;\n    rdfs:label \"has entitled feature\"@en ,\n               \"利用資格のある機能を持つ\"@ja ;\n\n    owl:propertyChainAxiom (\n        saas:hasEntitlement\n        saas:entitlesFeature\n    ) .\n</code></pre><p>これにより、次の関係を導出できます。</p><pre><code class=\"language-turtle\">ex:tenant-abc\n    saas:hasEntitledFeature\n        ex:feature-ai-text-generation .\n</code></pre><p>ただし、<code>owl:propertyChainAxiom</code>はOWLの推論規則なので、SPARQLで問い合わせるだけでは何も導出されません。第23節と同じく、OWL-RL推論器を通す必要があります。</p><pre><code class=\"language-bash\">pyshacl -s shapes.ttl -e ontology.ttl -i owlrl data.ttl\n</code></pre><p>RDFLibから直接推論する場合は、<code>owlrl</code>パッケージを使います。</p><pre><code class=\"language-python\">import rdflib, owlrl\n\ng = rdflib.Graph()\ng.parse(\"ontology.ttl\", format=\"turtle\")\ng.parse(\"data.ttl\", format=\"turtle\")\nowlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g)\n</code></pre><p>さらに、この推論には注意が必要です。</p><p>プロパティチェーンは、</p><pre><code class=\"language-text\">Entitlementが存在する\n</code></pre><p>ことだけを見ています。</p><p>次の条件は考慮していません。</p><ul><li><code>entitlementStatus</code>がactiveか</li><li>有効期間内か</li><li>Subscriptionが有効か</li><li>Entitlementが停止されていないか</li></ul><p>したがって、<code>hasEntitledFeature</code>という名前を、</p><pre><code class=\"language-text\">現在利用可能なFeature\n</code></pre><p>という意味で使うのは危険です。</p><p>より正確には、</p><pre><code class=\"language-text\">FeatureへのEntitlement関係が存在する\n</code></pre><p>程度の意味です。</p><p>現在利用可能かどうかは、SPARQL、SHACL Rules、アプリケーションロジック、ポリシーエンジンなどで判定する方が安全です。</p><hr><h1 id=\"32-%E5%B0%8F%E4%BC%91%E6%AD%A2%EF%BC%9A%E6%8E%A8%E8%AB%96%E3%81%AF%E3%80%8C%E7%9C%9F%E5%AE%9F%E3%80%8D%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F%E3%80%8C%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%AE%E7%B5%90%E6%9E%9C%E3%80%8D%E3%81%A7%E3%81%82%E3%82%8B\">32. 小休止：推論は「真実」ではなく「ルールの結果」である</h1><p>オントロジーの推論はとても便利ですが、推論された事実は、絶対的な真実ではありません。</p><pre><code class=\"language-text\">与えられたデータと、\n定義したルールから導かれた結果\n</code></pre><p>です。</p><p>たとえば、</p><pre><code class=\"language-text\">Tenant\n→ Entitlement\n→ Feature\n</code></pre><p>から、</p><pre><code class=\"language-text\">TenantはFeatureを利用可能\n</code></pre><p>と推論するルールを作ったとします。</p><p>しかし、Entitlementが停止中なら、その推論は実務上正しくありません。これは推論エンジンが間違えたのではありません。</p><p>こちらが、</p><pre><code class=\"language-text\">Entitlementが存在すれば利用可能\n</code></pre><p>という粗いルールを与えたためです。</p><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=\"33-shacl%E3%81%AB%E3%82%88%E3%82%8Bfeature%E3%81%AE%E6%A4%9C%E8%A8%BC\">33. SHACLによるFeatureの検証</h1><p>Featureには、名称とFeatureコードが必要だとします。</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 rdf:  &lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&gt; .\n@prefix xsd:  &lt;http://www.w3.org/2001/XMLSchema#&gt; .\n\nsaas:FeatureShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Feature ;\n\n    sh:property [\n        sh:path saas:name ;\n        sh:minCount 1 ;\n        sh:or (\n            [ sh:datatype xsd:string ]\n            [ sh:datatype rdf:langString ]\n        ) ;\n        sh:message\n            \"Featureには名称が必要です。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:featureCode ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:datatype xsd:string ;\n        sh:pattern \"^[a-z0-9]+([._-][a-z0-9]+)*$\" ;\n        sh:message\n            \"Featureには有効なFeatureコードが1つ必要です。\"@ja\n    ] .\n</code></pre><p>このパターンでは、たとえば次のコードを許可します。</p><pre><code class=\"language-text\">ai\nai.text-generation\nsecurity.saml-sso\ndata.csv-export\n</code></pre><p>大文字や空白を含むコードは許可しません。</p><pre><code class=\"language-text\">AI Text Generation\nSAML SSO\n</code></pre><p>表示名ではなく、システム識別子として安定させるためです。</p><p><code>name</code>の方に<code>sh:or</code>を使っているのは、第19節で触れた多言語化のためです。<code>sh:datatype xsd:string</code>だけにすると、言語タグ付きのリテラルが違反になります。</p><pre><code class=\"language-turtle\">saas:name \"AI文章生成\"@ja    # sh:datatype xsd:string だけだと違反になる\n</code></pre><p><code>featureCode</code>は表示名ではないので、言語タグ付きを弾く<code>sh:datatype xsd:string</code>のままにしています。</p><hr><h1 id=\"34-shacl%E3%81%AB%E3%82%88%E3%82%8Bentitlement%E3%81%AE%E5%9F%BA%E6%9C%AC%E6%A4%9C%E8%A8%BC\">34. SHACLによるEntitlementの基本検証</h1><p>Entitlementには、必ず1つのTenantと1つのFeatureが必要です。<code>status</code>と<code>grantType</code>は、第14節で分離した値だけを許可します(<code>trial</code>は<code>status</code>ではなく<code>grantType</code>の値である点に注意してください)。</p><pre><code class=\"language-turtle\">saas:EntitlementShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Entitlement ;\n\n    sh:property [\n        sh:path saas:entitledTenant ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Tenant ;\n        sh:message\n            \"Entitlementには対象Tenantが1つ必要です。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:entitlesFeature ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:class saas:Feature ;\n        sh:message\n            \"Entitlementには対象Featureが1つ必要です。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:entitlementStatus ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:in (\n            \"active\"\n            \"inactive\"\n            \"suspended\"\n            \"expired\"\n        ) ;\n        sh:message\n            \"Entitlementのstatusは定義済みの値である必要があります。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:grantType ;\n        sh:minCount 1 ;\n        sh:maxCount 1 ;\n        sh:in (\n            \"plan\"\n            \"add_on\"\n            \"trial\"\n            \"manual\"\n            \"promotion\"\n            \"beta\"\n            \"migration\"\n        ) ;\n        sh:message\n            \"EntitlementのgrantTypeは定義済みの値である必要があります。\"@ja\n    ] ;\n\n    sh:property [\n        sh:path saas:validFrom ;\n        sh:maxCount 1 ;\n        sh:datatype xsd:dateTime\n    ] ;\n\n    sh:property [\n        sh:path saas:validUntil ;\n        sh:maxCount 1 ;\n        sh:datatype xsd:dateTime\n    ] .\n</code></pre><hr><h1 id=\"35-%E6%9C%89%E5%8A%B9%E9%96%8B%E5%A7%8B%E6%97%A5%E6%99%82%E3%81%A8%E7%B5%82%E4%BA%86%E6%97%A5%E6%99%82%E3%81%AE%E6%95%B4%E5%90%88%E6%80%A7\">35. 有効開始日時と終了日時の整合性</h1><p><code>validUntil</code>が<code>validFrom</code>より前になってはいけません。誤った例は次のとおりです。</p><pre><code class=\"language-text\">validFrom:\n2026-09-01\n\nvalidUntil:\n2026-08-31\n</code></pre><p>SHACL-SPARQLで検証できます。</p><p>なお、以下では<code>sh:select</code>の文字列の中に<code>PREFIX</code>を直接書いています。<code>pyshacl</code>はこの書き方をそのまま受け付けますが、SHACLの仕様が用意している正式な仕組みは<code>sh:prefixes</code>と<code>sh:declare</code>です。別の実装に移すときは、そちらに書き換えが必要になることがあります。</p><pre><code class=\"language-turtle\">saas:EntitlementValidityPeriodShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Entitlement ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"EntitlementのvalidUntilはvalidFrom以降でなければなりません。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas:\n                &lt;https://example.com/ontology/saas#&gt;\n\n            SELECT $this\n            WHERE {\n                $this\n                    saas:validFrom ?validFrom ;\n                    saas:validUntil ?validUntil .\n\n                FILTER (?validUntil &lt; ?validFrom)\n            }\n        \"\"\"\n    ] .\n</code></pre><hr><h1 id=\"36-plan%E7%94%B1%E6%9D%A5entitlement%E3%81%AE%E5%A5%91%E7%B4%84%E6%95%B4%E5%90%88%E6%80%A7\">36. Plan由来Entitlementの契約整合性</h1><p><code>grantType</code>が<code>plan</code>である場合、<code>derivedFromSubscription</code>を必須にしたいとします。</p><pre><code class=\"language-text\">grantType = plan\n\nならば、\n\nderivedFromSubscriptionが必要\n</code></pre><p>SHACL-SPARQLでは、次のように書けます。</p><pre><code class=\"language-turtle\">saas:PlanEntitlementSubscriptionShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Entitlement ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"grantTypeがplanのEntitlementにはderivedFromSubscriptionが必要です。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas:\n                &lt;https://example.com/ontology/saas#&gt;\n\n            SELECT $this\n            WHERE {\n                $this saas:grantType \"plan\" .\n\n                FILTER NOT EXISTS {\n                    $this\n                        saas:derivedFromSubscription\n                            ?subscription .\n                }\n            }\n        \"\"\"\n    ] .\n</code></pre><p>反対に、<code>trial</code>や<code>manual</code>ではSubscriptionがなくても構いません。</p><hr><h1 id=\"37-entitlement%E3%81%A8subscription%E3%81%AEtenant%E6%95%B4%E5%90%88%E6%80%A7\">37. EntitlementとSubscriptionのTenant整合性</h1><p>Entitlementの対象Tenantと、根拠となるSubscriptionのTenantは一致する必要があります。誤った状態は次のようなものです。</p><pre><code class=\"language-text\">Entitlementの対象:\nTenant A\n\n根拠Subscriptionの所有者:\nTenant B\n</code></pre><p>これはクロステナント不整合であり、前回のTeamとWorkspaceのTenant整合性と同じように、非常に重要な制約です。</p><pre><code class=\"language-turtle\">saas:EntitlementSubscriptionTenantConsistencyShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Entitlement ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"Entitlementの対象Tenantと、根拠SubscriptionのTenantは一致する必要があります。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas:\n                &lt;https://example.com/ontology/saas#&gt;\n\n            SELECT $this\n            WHERE {\n                $this\n                    saas:entitledTenant\n                        ?entitledTenant ;\n                    saas:derivedFromSubscription\n                        ?subscription .\n\n                OPTIONAL {\n                    ?subscription\n                        ( ^saas:hasSubscription\n                          | saas:subscriptionBelongsToTenant )\n                            ?subscriptionTenant .\n                }\n\n                FILTER (\n                    !BOUND(?subscriptionTenant) ||\n                    ?entitledTenant !=\n                    ?subscriptionTenant\n                )\n            }\n        \"\"\"\n    ] .\n</code></pre><p>ここでは逆向きのパスを使っています。</p><pre><code class=\"language-sparql\">?subscription\n    ^saas:hasSubscription\n        ?subscriptionTenant .\n</code></pre><p>これは、次の関係を逆向きにたどっています。</p><pre><code class=\"language-text\">Tenant\n→ hasSubscription\n→ Subscription\n</code></pre><p><code>OPTIONAL</code>と<code>!BOUND</code>を使っているのには理由があります。</p><p>逆向きのパスだけで書くと、<strong>Subscription側からTenantを辿れないデータが、違反として検出されません。</strong> たとえば、<code>Tenant hasSubscription Subscription</code>が1本も書かれていない状態で、別TenantのSubscriptionを根拠にしたEntitlementを作っても、パターンが一致しないので何も報告されません。</p><pre><code class=\"language-text\">検出したい違反が、\n「そもそもパターンに当たらない」ために\n黙って素通りする\n</code></pre><p>これは、制約としては最悪の失敗の仕方です。そこで、<code>OPTIONAL</code>でTenantを取りに行き、<strong>取れなかった場合も違反にします</strong>。</p><p>また、選択パス(<code>|</code>)で<code>^saas:hasSubscription</code>と<code>saas:subscriptionBelongsToTenant</code>の両方を見ているのは、どちら向きに書かれていても辿れるようにするためです。第18節で定義したとおり、この2つは逆プロパティですが、第23節で書いたとおり、推論器を通していなければ片方からもう片方は導出されません。</p><hr><h1 id=\"38-plan%E7%94%B1%E6%9D%A5entitlement%E3%81%AEfeature%E6%95%B4%E5%90%88%E6%80%A7\">38. Plan由来EntitlementのFeature整合性</h1><p><code>grantType</code>が<code>plan</code>の場合、Entitlement対象Featureは、根拠SubscriptionのPlanに含まれているべきです。</p><pre><code class=\"language-text\">Entitlement\n→ derivedFromSubscription\n→ Subscription\n→ basedOnPlan\n→ Plan\n→ includesFeature\n→ Feature\n</code></pre><p>この経路が成立するかを検証します。</p><pre><code class=\"language-turtle\">saas:PlanEntitlementFeatureConsistencyShape\n    a sh:NodeShape ;\n\n    sh:targetClass saas:Entitlement ;\n\n    sh:sparql [\n        a sh:SPARQLConstraint ;\n\n        sh:message\n            \"Plan由来のEntitlement対象Featureは、SubscriptionのPlanに含まれている必要があります。\"@ja ;\n\n        sh:select \"\"\"\n            PREFIX saas:\n                &lt;https://example.com/ontology/saas#&gt;\n\n            SELECT $this\n            WHERE {\n                $this\n                    saas:grantType \"plan\" ;\n                    saas:entitlesFeature ?feature ;\n                    saas:derivedFromSubscription\n                        ?subscription .\n\n                ?subscription\n                    saas:basedOnPlan ?plan .\n\n                FILTER NOT EXISTS {\n                    ?plan\n                        saas:includesFeature\n                            ?feature .\n                }\n            }\n        \"\"\"\n    ] .\n</code></pre><p>ただし、この制約は、自社の商品設計に合わせて採用する必要があります。</p><p>たとえば、Subscriptionに個別契約Featureを含められる場合は、</p><pre><code class=\"language-text\">Planには含まれないが、\nSubscriptionの個別契約条件には含まれる\n</code></pre><p>ことがあり得ます。</p><p>その場合、<code>grantType</code>を<code>plan</code>ではなく<code>add_on</code>や<code>manual</code>にするか、契約明細の概念を追加する必要があります。</p><p>今回はFeatureとEntitlementに限定するため、そこまでは進みません。</p><hr><h1 id=\"39-%E5%90%8C%E4%B8%80tenant%E3%83%BB%E5%90%8C%E4%B8%80feature%E3%81%AE%E9%87%8D%E8%A4%87entitlement\">39. 同一Tenant・同一Featureの重複Entitlement</h1><p>同じTenantとFeatureに対して、複数のEntitlementが存在することがあります。</p><pre><code class=\"language-text\">Plan由来のEntitlement\n\n追加契約由来のEntitlement\n\n期間限定Trial Entitlement\n</code></pre><p>これは、必ずしも不正ではありません。たとえば、Enterprise Plan由来のAI Featureと、追加のβ機能Entitlementが並存することがあります。しかし、同じFeatureに対して、複数のactive Entitlementがあると、どれを採用すべきか曖昧になる場合があります。</p><pre><code class=\"language-text\">Entitlement A\n├── Feature: AI文章生成\n├── status: active\n└── validUntil: 2026-09-01\n\nEntitlement B\n├── Feature: AI文章生成\n├── status: active\n└── validUntil: 2027-03-31\n</code></pre><p>この場合、次の設計方針が考えられます。</p><h2 id=\"%E6%96%B9%E9%87%9Da%EF%BC%9A%E9%87%8D%E8%A4%87%E3%82%92%E7%A6%81%E6%AD%A2%E3%81%99%E3%82%8B\">方針A：重複を禁止する</h2><pre><code class=\"language-text\">Tenant × Featureごとに、\nactive Entitlementは最大1つ\n</code></pre><p>単純で扱いやすい方法です。</p><h2 id=\"%E6%96%B9%E9%87%9Db%EF%BC%9A%E9%87%8D%E8%A4%87%E3%82%92%E8%A8%B1%E5%8F%AF%E3%81%97%E3%80%81%E3%81%A9%E3%82%8C%E3%81%8B1%E3%81%A4%E3%81%8C%E6%9C%89%E5%8A%B9%E3%81%AA%E3%82%89%E5%88%A9%E7%94%A8%E5%8F%AF%E8%83%BD\">方針B：重複を許可し、どれか1つが有効なら利用可能</h2><pre><code class=\"language-text\">有効なEntitlementが1つ以上あれば利用可能\n</code></pre><p>複数の付与経路を履歴として残しやすくなります。</p><h2 id=\"%E6%96%B9%E9%87%9Dc%EF%BC%9A%E5%84%AA%E5%85%88%E9%A0%86%E4%BD%8D%E3%82%92%E6%B1%BA%E3%82%81%E3%82%8B\">方針C：優先順位を決める</h2><pre><code class=\"language-text\">manual\nadd_on\nplan\ntrial\n</code></pre><p>などの優先順位を定義します。</p><p>共通オントロジーとしては、重複を許容する方が柔軟です。</p><p>最終的な利用可否は、</p><pre><code class=\"language-text\">有効なEntitlementが1件以上存在するか\n</code></pre><p>で判定できます。</p><p>ただし、Entitlementの停止方法には注意が必要で、1つを停止しても、別のactive Entitlementが残っていれば、Featureは利用可能なままです。</p><hr><h1 id=\"40-feature%E3%81%AE%E5%BB%83%E6%AD%A2\">40. Featureの廃止</h1><p>SaaSでは、Featureが廃止されることもあります。Featureそのものを削除すると、過去の契約や監査履歴が読めなくなる可能性があるため、Featureにも状態を持たせる方法があります。</p><pre><code class=\"language-text\">active\ndeprecated\nretired\n</code></pre><p>たとえば、</p><pre><code class=\"language-turtle\">saas:featureStatus a owl:DatatypeProperty ;\n    rdfs:domain saas:Feature ;\n    rdfs:range xsd:string .\n\nex:feature-legacy-report\n    a saas:Feature ;\n    saas:name \"旧レポート機能\" ;\n    saas:featureCode \"report.legacy\" ;\n    saas:featureStatus \"retired\" .\n</code></pre><p>Featureが<code>retired</code>であっても、過去のEntitlementは履歴として残せます。</p><pre><code class=\"language-text\">Featureは廃止済み\n\nしかし、\n\n過去にどのTenantが利用できたかは残る\n</code></pre><p>ただし、今回はFeatureとEntitlementの最小モデルに集中するため、Feature状態は必須要素にはしません。</p><hr><h1 id=\"41-feature-flag%E3%81%A8%E3%81%AE%E9%81%95%E3%81%84\">41. Feature Flagとの違い</h1><p>Featureという言葉から、Feature Flagを思い浮かべる人もいるでしょう。Feature Flagは、一般にソフトウェアの挙動を動的に切り替える仕組みです。</p><pre><code class=\"language-text\">新UIを有効化する\n新しい検索エンジンを使う\nβ版アルゴリズムを一部Userに公開する\n</code></pre><p>Entitlementと似ていますが、同じではありません。</p><pre><code class=\"language-text\">Entitlement\n＝ 契約・商品・提供資格の観点\n\nFeature Flag\n＝ リリース・運用・技術制御の観点\n</code></pre><p>たとえば、TenantにAI FeatureのEntitlementがあっても、障害対応でFeature FlagがOFFになっていれば、実際には利用できない場合があります。</p><pre><code class=\"language-text\">Entitlement: active\nFeature Flag: off\n</code></pre><p>反対に、開発環境でFeature FlagがONでも、TenantにEntitlementがなければ、本番利用を許可すべきではありません。</p><p>概念的には、最終利用可否は次のようになることがあります。</p><pre><code class=\"language-text\">Permission\nかつ\nEntitlement\nかつ\nFeature Flag\nかつ\n運用条件\n</code></pre><p>今回のモデルではFeature Flagを扱いませんが、</p><pre><code class=\"language-text\">Entitlementだけで、\nあらゆる実行可否を表現しようとしない\n</code></pre><p>ことは重要です。</p><hr><h1 id=\"42-%E5%B0%8F%E4%BC%91%E6%AD%A2%EF%BC%9A%E5%A5%91%E7%B4%84%E4%B8%8A%E4%BD%BF%E3%81%88%E3%82%8B%E3%81%93%E3%81%A8%E3%81%A8%E3%80%81%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AB%E5%8B%95%E3%81%8F%E3%81%93%E3%81%A8\">42. 小休止：契約上使えることと、技術的に動くこと</h1><p>SaaSでは、ときどき次の2つが混同されます。</p><pre><code class=\"language-text\">契約上使ってよい\n\n技術的に使える\n</code></pre><p>本来使えないPlanなのに、APIエンドポイントへ直接アクセスすると動いてしまう。管理画面では非表示だが、URLを知っていれば開けてしまう。Feature Flagだけで画面を隠し、バックエンドではEntitlementを検証していない。</p><p>こうした実装は、単なるUI上の不具合ではありません。契約違反、情報漏えい、請求漏れ、セキュリティ事故につながる可能性があります。</p><p>オントロジー上でFeatureとEntitlementを分けておくことは、概念整理だけでなく、</p><pre><code class=\"language-text\">どの層で何を確認すべきか\n</code></pre><p>を明確にする効果があります。</p><pre><code class=\"language-text\">フロントエンド\n→ 表示制御\n\nバックエンド\n→ EntitlementとPermissionの最終検証\n\n契約管理\n→ SubscriptionとEntitlementの生成\n\n運用管理\n→ 停止・再開・個別付与\n</code></pre><p>画面にボタンが見えないことは、認可ではありません。同じように、Feature FlagがOFFであることも、契約管理ではありません。</p><hr><h1 id=\"43-%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\">43. 今回の最小関係一覧</h1><!--kg-card-begin: html--><table data-line=\"2925\" class=\"code-line\" dir=\"auto\" style=\"border-collapse: collapse; margin-bottom: 0.7em;\"><thead data-line=\"2925\" class=\"code-line\" dir=\"auto\"><tr data-line=\"2925\" 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=\"2927\" class=\"code-line\" dir=\"auto\"><tr data-line=\"2927\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Plan</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">includesFeature</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Feature</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Planの商品構成にFeatureが含まれる</td></tr><tr data-line=\"2928\" 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);\">Feature</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);\">includedInPlan</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);\">Plan</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);\">FeatureがPlanに含まれる</td></tr><tr data-line=\"2929\" 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);\">hasEntitlement</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);\">Entitlement</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がFeature利用資格を持つ</td></tr><tr data-line=\"2930\" 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);\">Entitlement</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);\">entitledTenant</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);\">Entitlementの対象Tenant</td></tr><tr data-line=\"2931\" 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);\">Entitlement</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);\">entitlesFeature</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);\">Feature</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);\">Entitlementが利用可能にするFeature</td></tr><tr data-line=\"2932\" 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);\">Feature</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);\">hasFeatureEntitlement</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);\">Entitlement</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);\">Featureに対するEntitlement</td></tr><tr data-line=\"2933\" 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);\">Entitlement</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);\">derivedFromSubscription</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);\">Subscription</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);\">Entitlementの契約上の根拠</td></tr><tr data-line=\"2934\" 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);\">Feature</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);\">hasSubFeature</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);\">Feature</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);\">Featureが下位Featureを持つ</td></tr><tr data-line=\"2935\" 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);\">Feature</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);\">subFeatureOf</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);\">Feature</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);\">Featureが上位Featureに属する</td></tr><tr data-line=\"2936\" 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);\">hasEntitledFeature</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);\">Feature</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);\"><code style=\"font-family: Menlo, Monaco, &quot;Courier New&quot;, monospace; color: rgb(140, 140, 140); background-color: rgb(38, 38, 38); padding: 1px 3px; border-radius: 4px; font-size: 1em; line-height: 1.357em;\">hasEntitlement</code>+<code style=\"font-family: Menlo, Monaco, &quot;Courier New&quot;, monospace; color: rgb(140, 140, 140); background-color: rgb(38, 38, 38); padding: 1px 3px; border-radius: 4px; font-size: 1em; line-height: 1.357em;\">entitlesFeature</code>から推論(第31節)</td></tr></tbody></table><!--kg-card-end: html--><p>Entitlementの根拠をたどるために、第1回で定義した次の関係も使います(第18節で再掲しました)。</p><!--kg-card-begin: html--><table data-line=\"2940\" class=\"code-line\" dir=\"auto\" style=\"border-collapse: collapse; margin-bottom: 0.7em;\"><thead data-line=\"2940\" class=\"code-line\" dir=\"auto\"><tr data-line=\"2940\" 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=\"2942\" class=\"code-line\" dir=\"auto\"><tr data-line=\"2942\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Tenant</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">hasSubscription</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Subscription</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">TenantがPlanを契約する</td></tr><tr data-line=\"2943\" 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);\">Subscription</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);\">subscriptionBelongsToTenant</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);\"><code style=\"font-family: Menlo, Monaco, &quot;Courier New&quot;, monospace; color: rgb(140, 140, 140); background-color: rgb(38, 38, 38); padding: 1px 3px; border-radius: 4px; font-size: 1em; line-height: 1.357em;\">hasSubscription</code>の逆</td></tr><tr data-line=\"2944\" 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);\">Subscription</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);\">basedOnPlan</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);\">Plan</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);\">契約が基づくPlan</td></tr></tbody></table><!--kg-card-end: html--><p>主な属性は次のとおりです。</p><!--kg-card-begin: html--><table data-line=\"2948\" class=\"code-line\" dir=\"auto\" style=\"border-collapse: collapse; margin-bottom: 0.7em;\"><thead data-line=\"2948\" class=\"code-line\" dir=\"auto\"><tr data-line=\"2948\" 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=\"2950\" class=\"code-line\" dir=\"auto\"><tr data-line=\"2950\" class=\"code-line\" dir=\"auto\"><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">Feature</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">featureCode</td><td style=\"padding: 5px 10px; border-color: rgba(255, 255, 255, 0.18);\">安定した機能識別子</td></tr><tr data-line=\"2951\" 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);\">Feature</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);\">featureStatus</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);\">機能の提供状態(第40節)</td></tr><tr data-line=\"2952\" 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);\">Entitlement</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);\">entitlementStatus</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=\"2953\" 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);\">Entitlement</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);\">grantType</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=\"2954\" 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);\">Entitlement</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);\">validFrom</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=\"2955\" 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);\">Entitlement</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);\">validUntil</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=\"2956\" 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);\">Entitlement</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);\">grantedAt</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=\"2957\" 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);\">Entitlement</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);\">grantReason</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></tbody></table><!--kg-card-end: html--><hr><h1 id=\"44-%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\">44. 今回のモデルで答えられる質問</h1><h2 id=\"feature%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%B3%AA%E5%95%8F\">Featureに関する質問</h2><pre><code class=\"language-text\">このSaaSには、どのFeatureが存在するか。\n\nAI機能の下位Featureは何か。\n\nこのFeatureは、どのPlanに含まれるか。\n\nEnterprise Planに含まれ、\nStandard Planには含まれないFeatureは何か。\n</code></pre><h2 id=\"entitlement%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%B3%AA%E5%95%8F\">Entitlementに関する質問</h2><pre><code class=\"language-text\">このTenantには、\nどのFeatureのEntitlementがあるか。\n\n現在activeなEntitlementはどれか。\n\nTrialとして付与されているFeatureは何か。\n\nこのEntitlementは、\nどのSubscriptionに基づいているか。\n\n1か月以内に期限切れになるEntitlementはどれか。\n</code></pre><h2 id=\"permission%E3%81%A8%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%81%9F%E8%B3%AA%E5%95%8F\">Permissionと組み合わせた質問</h2><pre><code class=\"language-text\">山田太郎は、\n営業WorkspaceでAI文章生成を利用できるか。\n\nFeatureは利用可能だが、\n操作Permissionを持たないUserは誰か。\n\nPermissionは持っているが、\nTenantにEntitlementがないため\n利用できない機能は何か。\n</code></pre><h2 id=\"%E5%95%86%E5%93%81%E3%83%BB%E5%A5%91%E7%B4%84%E5%B7%AE%E5%88%86%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E8%B3%AA%E5%95%8F\">商品・契約差分に関する質問</h2><pre><code class=\"language-text\">Planには含まれていないが、\n個別に付与されているFeatureは何か。\n\nPlanには含まれるが、\n現在suspendedのFeatureは何か。\n\n同じPlanを契約しているTenant間で、\nEntitlementに差があるFeatureは何か。\n</code></pre><hr><h1 id=\"45-%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\">45. 今回のモデルのまとめ</h1><p>今回の内容を最も単純にまとめると、次のようになります。</p><pre><code class=\"language-text\">Featureは、\nSaaSが提供可能な機能・能力である。\n\nPlanは、\n標準商品としてFeatureを含む。\n\nSubscriptionは、\nTenantによるPlanの個別契約である。\n\nEntitlementは、\n特定Tenantに特定Featureを提供する\n権利・資格・状態である。\n\nPermissionは、\n利用者が操作してよいかを表す。\n\nEntitlementは、\nTenantに機能が提供されているかを表す。\n</code></pre><p>図にすると、次の形です。</p><!--kg-card-begin: html--><pre class=\"mermaid\">\ngraph LR\n    T[Tenant]\n    S[Subscription]\n    P[Plan]\n    E[Entitlement]\n    F[Feature]\n\n    M[Membership]\n    RA[RoleAssignment]\n    R[Role]\n    PM[Permission]\n\n    T --> S\n    S --> P\n    P --> F\n\n    T --> E\n    E --> F\n    E --> S\n\n    M --> T\n    M --> RA\n    RA --> R\n    R --> PM\n\n</pre><!--kg-card-end: html--><p></p><p>そして、利用者がFeatureを操作できるかどうかは、基本的に次の組み合わせで決まります。</p><pre><code class=\"language-text\">Tenant側:\n有効なEntitlementがある\n\nUser側:\n必要なPermissionがある\n</code></pre><p>つまり、</p><pre><code class=\"language-text\">利用可能\n＝\nEntitlement\nかつ\nPermission\n</code></pre><p>です。</p><hr><h1 id=\"46-%E3%81%93%E3%81%93%E3%81%BE%E3%81%A7%E3%81%AEb2b-saas%E3%82%AA%E3%83%B3%E3%83%88%E3%83%AD%E3%82%B8%E3%83%BC\">46. ここまでのB2B SaaSオントロジー</h1><p>第1回から今回までのモデルをまとめると、次のようになります。</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        │       └── Feature\n        │\n        └── Entitlement\n            ├── Feature\n            └── Subscription\n</code></pre><p>より意味を意識して読むと、次のようになります。</p><pre><code class=\"language-text\">現実世界のOrganizationが、\nSaaS上のTenantを利用する。\n\nUserは、\nMembershipを通じてTenantに参加する。\n\nTenantには、\nWorkspaceとTeamがある。\n\nMembershipには、\nScope付きのRoleAssignmentがある。\n\nRoleは、\nPermissionを付与する。\n\nTenantは、\nSubscriptionを通じてPlanを契約する。\n\nPlanは、\n標準商品としてFeatureを含む。\n\nTenantは、\nEntitlementを通じて、\n実際に利用可能なFeatureを持つ。\n</code></pre><hr><h1 id=\"47-%E6%AC%A1%E5%9B%9E%E4%BA%88%E5%91%8A%EF%BC%81\">47. 次回予告！</h1><p>ここまでで、</p><pre><code class=\"language-text\">このTenantに、\nこのFeatureが提供されているか\n</code></pre><p>を表現できるようになりました。</p><p>しかし、Featureが利用可能だとしても、まだ次の問題が残っています。</p><pre><code class=\"language-text\">何回まで使えるのか。\n\n何人まで使えるのか。\n\n何GBまで保存できるのか。\n\nAPIを月に何回実行できるのか。\n\nAI生成を月に何回使えるのか。\n</code></pre><p>FeatureとEntitlementが表すのは、基本的に、</p><pre><code class=\"language-text\">利用できるか、できないか\n</code></pre><p>です。</p><p>しかし、現実のSaaS契約には、</p><pre><code class=\"language-text\">利用できるが、上限がある\n</code></pre><p>という状態が大量にあります。</p><p>その先では、Limit、Quota、Usageなどの概念が必要になりますが、それらはFeatureやEntitlementとは別の問題です。</p><p>まず今回の段階では、次の区別を確実にします。</p><pre><code class=\"language-text\">Feature\n＝ どのような機能が存在するか\n\nEntitlement\n＝ そのTenantに機能が提供されているか\n\nPermission\n＝ その人が機能を操作してよいか\n</code></pre><p>この3つを分けて表現できれば、B2B SaaSの料金プラン、オプション契約、トライアル、β提供、個別開放、機能停止を、かなり自然に扱えるようになります。</p><p>今回も巨大な記事になってしまいましたが、まだまだ続きます！ げんなりしないで、一緒に頑張って学んでいきましょう！</p><p>次回もまた絶対見てくれよな！</p>","url":"https://ghost.tech.anti-pattern.co.jp/saas-ontology-3/","canonical_url":null,"uuid":"6ac60288-08ae-4ea3-bef6-3a2ae5e489b3","page":null,"codeinjection_foot":null,"codeinjection_head":null,"codeinjection_styles":null,"comment_id":"6aaa464fd3fc9f00013c1fda","reading_time":40}},"pageContext":{"slug":"saas-ontology-3"}},
    "staticQueryHashes": ["176528973","2358152166","2561578252","2731221146","4145280475"]}