From 5c6bef519e8900d2beec24144163ce8a36cd4809 Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Mon, 21 Sep 2026 14:22:00 -0700 Subject: [PATCH 01/14] feat: added MCP Gateway examples --- README.md | 1 + stores/mcp-gateway/README.md | 53 +++++++ stores/mcp-gateway/mcp-gateway.fga.yaml | 142 ++++++++++++++++++ .../multi-tenant-mcp-gateway-intent.fga.yaml | 113 ++++++++++++++ .../multi-tenant-mcp-gateway.fga.yaml | 96 ++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 stores/mcp-gateway/README.md create mode 100644 stores/mcp-gateway/mcp-gateway.fga.yaml create mode 100644 stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml create mode 100644 stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml diff --git a/README.md b/README.md index bd4c654..cd08dff 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Examples that demonstrate how to model authorization for well-known products and |---------|-------------| | [Modeling Guide](./stores/modeling-guide) | Step-by-step introduction to OpenFGA modeling | | [Multi-Tenant RBAC](./stores/multitenant-rbac) | Multi-tenant role-based access control | +| [MCP Gateway](./stores/mcp-gateway) | Authorization for employee-facing and customer-facing multi-tenant MCP gateways | | [GitHub](./stores/github) | Repository, organization, and team permissions | | [Google Drive](./stores/gdrive) | File, folder, and shared drive permissions | | [Entitlements](./stores/entitlements) | Feature entitlements and plan-based access | diff --git a/stores/mcp-gateway/README.md b/stores/mcp-gateway/README.md new file mode 100644 index 0000000..1c1f0fb --- /dev/null +++ b/stores/mcp-gateway/README.md @@ -0,0 +1,53 @@ +# OpenFGA for MCP Gateways + +## Use Case + +This folder demonstrates three different models for MCP Gateway use cases: + +* **An employee-facing MCP Gateway:** Where you want to control how agents can call MCP servers on behalf of users. The permissions are based on user identity. +* **A customer-facing multi-tenant MCP Gateway:** Where you write your own agents and want to control what the agents can do when interacting with each tenant's tools. Permissions are based on agent identity. +* **An intent-based authorization scenario:** An iteration of the previous one showing how to apply the same ideas for authorizing calls based on inferred intent. + +These examples leverage the **Dynamic Conditions** experimental feature. + +## Employee Authorization + +**Requirements:** + +* **Gateway-level policies:** Allow definition of policies that are well-known to the gateway developer so they can be defined with static conditions. In the example, we add an IP restriction policy. +* **Broad tool access:** Allow access to all read-only or read-write tools per MCP server, user, or role. +* **Specific tool access:** Allow access to specific tools per user or role. +* **Parameter-based access:** Allow calling tools with specific parameter values per user or role. This is implemented using Dynamic Conditions. + +You can view the example configuration [here](mcp-gateway.fga.yaml). + +## Multi-Tenant Agent Authorization + +**Requirements:** + +* Agents operate on behalf of an organization. +* Agents can be granted permissions to call tools, including calls with specific parameter values. + +For example, you can configure that the `triage-bot` can only send Slack messages to `#product-announcements` for the organization `Acme`. + +You can view the example configuration [here](multi-tenant-mcp-gateway.fga.yaml). + +## Intent-Based Authorization + +This builds on the previous example, but agents do not have any permissions persisted in the OpenFGA database. + +**Requirements:** + +* Agents operate on behalf of an organization. +* Agents begin with zero permissions. +* An intent inference engine (out of scope for this example) will infer which tools the agent should call and with which parameters. This information is sent to OpenFGA as contextual tuples. + +Note that even this is applicable to Intent based Authorization, it can be applied to any solution where the granted permissions can be inferred from the context. They can come from an intent-inference engine, or a token that grants the agent fine grained contextual permissions. + +You can view the example configuration [here](multi-tenant-mcp-gateway-intent.fga.yaml). + +## Try It Out + +1. Make sure you have the [FGA CLI](https://github.com/openfga/cli/?tab=readme-ov-file#installation) + +2. In the `mcp-gateway` directory, run `fga model test --tests .fga.yaml` diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml new file mode 100644 index 0000000..b8a9e8d --- /dev/null +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -0,0 +1,142 @@ +name: MCP Gateway for Workforce use cases +model: | + model + schema 1.1 + + type gateway + relations + # Gateway-level policies. + # If you write a user:* tuple, no gateway-level restrictions applied + # You can write any arbitrary policy with a dynamic condition + # This model shows a IP-based policy, abritrarily defined by the Gateway administrator + define allow_policies : [user:*, user:* with in_company_network] + + type user + + type role + relations + define assignee : [user] + + type mcp_server + relations + # Adds the MCP server to the Gateway instance, to inherit its policies + define gateway : [gateway] + # Allows assigning access to all RO or RW tools + define readonly_access : [user, role#assignee] + define readwrite_access : [user, role#assignee] + + define can_call_readonly : readonly_access and allow_policies from gateway + define can_call_readwrite : readwrite_access and allow_policies from gateway + define can_call_any: allow_policies from gateway + + type tool + relations + define mcp_server : [mcp_server] + define is_readonly : [user:*] + define is_readwrite : [user:*] + + define caller : [user, role#assignee, role#assignee with $expression, user with $expression] + + define can_call : (caller and can_call_any from mcp_server) or (is_readonly and can_call_readonly from mcp_server) or (is_readwrite and can_call_readwrite from mcp_server) + + condition in_company_network(user_ip: ipaddress, cidr: string) { + user_ip.in_cidr(cidr) + } + +tuples: + + ### Gateway level tuples + + + # If you write this tuple, there won't be global level policies + #- user: user:* + # relation: allow_policies + # object: gateway:mcp-gateway + # Note, the devloper building the MCP Gatweay will need to allow admins to enter + # these kind of policies + + - user: user:* + relation: allow_policies + object: gateway:mcp-gateway + condition: + name: in_company_network + context: + cidr: '192.168.0.0/24' + + ### MCP configuration + - user: gateway:mcp-gateway + relation: gateway + object: mcp_server:slack + + ## These tuples are created based on MCP server tools definitions + - user: user:* + relation: is_readwrite + object: tool:slack_send_message + + - user: mcp_server:slack + relation: mcp_server + object: tool:slack_send_message + + ### Users/Roles tuples + - user: user:anne + relation: assignee + object: role:devops + + - user: user:bob + relation: assignee + object: role:product + + - user: role:devops#assignee + relation: readwrite_access + object: mcp_server:slack + + # Assignees to the product role are the only ones that can post in product announcements + - user: role:product#assignee + relation: caller + object: tool:slack_send_message + condition: + name: $expression + context: + expression: "channel_name == '#product-announcements'" + parameters: + channel_name: string + +tests: + - name: dynamic-rbac-example + check: + # Anne has access as the devops team has write access + - user: user:anne + object: tool:slack_send_message + context: + channel_name: "#product-announcements" + user_ip: 192.168.0.1 + assertions: + can_call: true + + # Bob has access as he is assigned to the product role + - user: user:bob + object: tool:slack_send_message + context: + channel_name: "#product-announcements" + user_ip: 192.168.0.1 + assertions: + can_call: true + + # Bob does not have access when using another IP + - user: user:bob + object: tool:slack_send_message + context: + channel_name: "#product-announcements" + user_ip: 200.168.0.1 + assertions: + can_call: false + + # Bob does not have access to channels outside the product announcements policy + - user: user:bob + object: tool:slack_send_message + context: + channel_name: "#general" + user_ip: 192.168.0.1 + assertions: + can_call: false + diff --git a/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml b/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml new file mode 100644 index 0000000..65daa95 --- /dev/null +++ b/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml @@ -0,0 +1,113 @@ +name: MCP Gateway for B2B use cases, showing how to use OpenFGA for intent-based authorization +model: | + model + schema 1.1 + + type agent + + type organization + relations + define agent : [agent] + + type tool + relations + define organization : [organization] + define can_call: [agent, agent with $expression] and agent from organization + +tuples: + - user: agent:triage-bot + relation: agent + object: organization:acme + + - user: agent:assistant + relation: agent + object: organization:acme + + # The Slack tools are enabled for the organization + - user: organization:acme + relation: organization + object: tool:acme_slack_send_message + + - user: organization:acme + relation: organization + object: tool:acme_slack_read_channel + + - user: organization:acme + relation: organization + object: tool:acme_slack_search_channels + + - user: organization:acme + relation: organization + object: tool:acme_slack_schedule_message + + # Agents start with zero permissions + +tests: + # We assume that the intent engine inferred the agent needs to find a specific + # Slack channel and post a message there. + # This is sent by the Intent engine as contextual tuples + + - name: test + tuples: + - user: agent:triage-bot + relation: can_call + object: tool:acme_slack_send_message + condition: + name: $expression + context: + expression: "channel_name == '#product-announcements'" + parameters: + channel_name: string + + - user: agent:triage-bot + relation: can_call + object: tool:acme_slack_search_channels + condition: + name: $expression + context: + expression: "query == 'product-announcements'" + parameters: + query: string + - user: agent:triage-bot + + relation: can_call + object: tool:acme_slack_read_channel + + check: + - user: agent:triage-bot + object: tool:acme_slack_search_channels + context: + query: "product-announcements" + assertions: + can_call: true + + - user: agent:triage-bot + object: tool:acme_slack_send_message + context: + channel_name: "#product-announcements" + assertions: + can_call: true + + - user: agent:triage-bot + object: tool:acme_slack_send_message + context: + channel_name: "#general" + assertions: + can_call: false + + - user: agent:triage-bot + object: tool:acme_slack_search_channels + context: + query: "general" + assertions: + can_call: false + + - user: agent:triage-bot + object: tool:acme_slack_read_channel + assertions: + can_call: true + + - user: agent:triage-bot + object: tool:acme_slack_schedule_message + assertions: + can_call: false \ No newline at end of file diff --git a/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml b/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml new file mode 100644 index 0000000..f9793f6 --- /dev/null +++ b/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml @@ -0,0 +1,96 @@ +name: MCP Gateway for B2B use cases +model: | + model + schema 1.1 + + type agent + + type organization + relations + define agent : [agent] + + type tool + relations + define organization : [organization] + define can_call: [agent:*, agent, agent with $expression] and agent from organization + +tuples: + - user: agent:triage-bot + relation: agent + object: organization:acme + + - user: agent:assistant + relation: agent + object: organization:acme + + - user: agent:external + relation: agent + object: organization:globex + + # The Slack tools are enabled for the organization + - user: organization:acme + relation: organization + object: tool:acme_slack_send_message + + - user: organization:acme + relation: organization + object: tool:acme_slack_read_channel + + # Every agent in the acme organization can call acme_slack_read_channel. + - user: agent:* + relation: can_call + object: tool:acme_slack_read_channel + + # The assistant agent can call acme_slack_send_message for every channel: + - user: agent:assistant + relation: can_call + object: tool:acme_slack_send_message + + # The triage-bot agent can only use acme_slack_send_message for product-announcements + - user: agent:triage-bot + relation: can_call + object: tool:acme_slack_send_message + condition: + name: $expression + context: + expression: "channel_name == '#product-announcements'" + parameters: + channel_name: string + +tests: + - name: dynamic-rbac-example + check: + - user: agent:triage-bot + object: tool:acme_slack_send_message + context: + channel_name: "#product-announcements" + assertions: + can_call: true + + - user: agent:assistant + object: tool:acme_slack_send_message + context: + channel_name: "#general" + assertions: + can_call: true + + # An agent from another organization cannot call Acme's tools + - user: agent:external + object: tool:acme_slack_read_channel + assertions: + can_call: false + + - user: agent:triage-bot + object: tool:acme_slack_send_message + context: + channel_name: "#general" + assertions: + can_call: false + + - user: agent:triage-bot + object: tool:acme_slack_read_channel + assertions: + # No need to send the channel name for 'read' operations + # if you are not allowing defining policies for 'read'. + # If you do, you should send it + can_call: true \ No newline at end of file From 3e667fea69af790d64b84fdd6060979638d9e384 Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Mon, 21 Sep 2026 14:31:58 -0700 Subject: [PATCH 02/14] chore: adding experimental flag clarification --- stores/mcp-gateway/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/stores/mcp-gateway/README.md b/stores/mcp-gateway/README.md index 1c1f0fb..da21c3a 100644 --- a/stores/mcp-gateway/README.md +++ b/stores/mcp-gateway/README.md @@ -8,7 +8,11 @@ This folder demonstrates three different models for MCP Gateway use cases: * **A customer-facing multi-tenant MCP Gateway:** Where you write your own agents and want to control what the agents can do when interacting with each tenant's tools. Permissions are based on agent identity. * **An intent-based authorization scenario:** An iteration of the previous one showing how to apply the same ideas for authorizing calls based on inferred intent. -These examples leverage the **Dynamic Conditions** experimental feature. +These examples leverage the **Dynamic Conditions** experimental feature. They can be run with the latest OpenFGA CLI, or by enabling the experimental flag in OpenFGA + +```bash +openfga run --experimentals inline_expressions +``` ## Employee Authorization From f7b72ad9b0cb97953ee46bddb940862e84bdb325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Aguiar?= Date: Tue, 22 Sep 2026 12:28:55 -0300 Subject: [PATCH 03/14] Fix typo in mcp-gateway.fga.yaml comment Corrected a typo in the comment regarding IP-based policy. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- stores/mcp-gateway/mcp-gateway.fga.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index b8a9e8d..29d4cfc 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -8,7 +8,7 @@ model: | # Gateway-level policies. # If you write a user:* tuple, no gateway-level restrictions applied # You can write any arbitrary policy with a dynamic condition - # This model shows a IP-based policy, abritrarily defined by the Gateway administrator + # This model shows an IP-based policy, arbitrarily defined by the Gateway administrator define allow_policies : [user:*, user:* with in_company_network] type user From 87844109d28b56afcc12b68e82fa88dcca3408d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Aguiar?= Date: Tue, 22 Sep 2026 12:29:19 -0300 Subject: [PATCH 04/14] Fix comments in mcp-gateway.fga.yaml Corrected spelling and grammar in comments. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- stores/mcp-gateway/mcp-gateway.fga.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index 29d4cfc..3925e72 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -52,8 +52,8 @@ tuples: #- user: user:* # relation: allow_policies # object: gateway:mcp-gateway - # Note, the devloper building the MCP Gatweay will need to allow admins to enter - # these kind of policies + # Note that the developer building the MCP Gateway will need to allow admins to enter + # these kinds of policies - user: user:* relation: allow_policies From 8d63fab4761c35fd993a2e426084a003f35a6417 Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Tue, 22 Sep 2026 08:36:46 -0700 Subject: [PATCH 05/14] chore: applied feedback --- stores/mcp-gateway/README.md | 10 +++++----- stores/mcp-gateway/mcp-gateway.fga.yaml | 18 +++++++++++++++++- .../multi-tenant-mcp-gateway-intent.fga.yaml | 9 +++++---- .../multi-tenant-mcp-gateway.fga.yaml | 9 +++++---- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/stores/mcp-gateway/README.md b/stores/mcp-gateway/README.md index da21c3a..e58bce4 100644 --- a/stores/mcp-gateway/README.md +++ b/stores/mcp-gateway/README.md @@ -8,7 +8,7 @@ This folder demonstrates three different models for MCP Gateway use cases: * **A customer-facing multi-tenant MCP Gateway:** Where you write your own agents and want to control what the agents can do when interacting with each tenant's tools. Permissions are based on agent identity. * **An intent-based authorization scenario:** An iteration of the previous one showing how to apply the same ideas for authorizing calls based on inferred intent. -These examples leverage the **Dynamic Conditions** experimental feature. They can be run with the latest OpenFGA CLI, or by enabling the experimental flag in OpenFGA +These examples use the **Dynamic Conditions** experimental feature. The FGA CLI enables inline expressions automatically when running model tests. When testing against an OpenFGA server, enable the feature explicitly: ```bash openfga run --experimentals inline_expressions @@ -23,7 +23,7 @@ openfga run --experimentals inline_expressions * **Specific tool access:** Allow access to specific tools per user or role. * **Parameter-based access:** Allow calling tools with specific parameter values per user or role. This is implemented using Dynamic Conditions. -You can view the example configuration [here](mcp-gateway.fga.yaml). +You can view the [mcp-gateway.fga.yaml](mcp-gateway.fga.yaml) example configuration. ## Multi-Tenant Agent Authorization @@ -34,7 +34,7 @@ You can view the example configuration [here](mcp-gateway.fga.yaml). For example, you can configure that the `triage-bot` can only send Slack messages to `#product-announcements` for the organization `Acme`. -You can view the example configuration [here](multi-tenant-mcp-gateway.fga.yaml). +You can view the [multi-tenant-mcp-gateway.fga.yaml](multi-tenant-mcp-gateway.fga.yaml) example configuration. ## Intent-Based Authorization @@ -46,9 +46,9 @@ This builds on the previous example, but agents do not have any permissions pers * Agents begin with zero permissions. * An intent inference engine (out of scope for this example) will infer which tools the agent should call and with which parameters. This information is sent to OpenFGA as contextual tuples. -Note that even this is applicable to Intent based Authorization, it can be applied to any solution where the granted permissions can be inferred from the context. They can come from an intent-inference engine, or a token that grants the agent fine grained contextual permissions. +This pattern also applies to intent-based authorization. Permissions can come from an intent-inference engine or a token that grants the agent fine-grained contextual permissions. -You can view the example configuration [here](multi-tenant-mcp-gateway-intent.fga.yaml). +You can view the [multi-tenant-mcp-gateway-intent.fga.yaml](multi-tenant-mcp-gateway-intent.fga.yaml) example configuration. ## Try It Out diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index 3925e72..32d941c 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -25,7 +25,7 @@ model: | define readonly_access : [user, role#assignee] define readwrite_access : [user, role#assignee] - define can_call_readonly : readonly_access and allow_policies from gateway + define can_call_readonly : (readonly_access or readwrite_access) and allow_policies from gateway define can_call_readwrite : readwrite_access and allow_policies from gateway define can_call_any: allow_policies from gateway @@ -73,10 +73,18 @@ tuples: relation: is_readwrite object: tool:slack_send_message + - user: user:* + relation: is_readonly + object: tool:slack_read_channel + - user: mcp_server:slack relation: mcp_server object: tool:slack_send_message + - user: mcp_server:slack + relation: mcp_server + object: tool:slack_read_channel + ### Users/Roles tuples - user: user:anne relation: assignee @@ -113,6 +121,14 @@ tests: assertions: can_call: true + # Anne's read-write access also grants access to read-only tools + - user: user:anne + object: tool:slack_read_channel + context: + user_ip: 192.168.0.1 + assertions: + can_call: true + # Bob has access as he is assigned to the product role - user: user:bob object: tool:slack_send_message diff --git a/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml b/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml index 65daa95..859a002 100644 --- a/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml +++ b/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml @@ -12,7 +12,8 @@ model: | type tool relations define organization : [organization] - define can_call: [agent, agent with $expression] and agent from organization + define caller: [agent, agent with $expression] + define can_call: caller and agent from organization tuples: - user: agent:triage-bot @@ -50,7 +51,7 @@ tests: - name: test tuples: - user: agent:triage-bot - relation: can_call + relation: caller object: tool:acme_slack_send_message condition: name: $expression @@ -60,7 +61,7 @@ tests: channel_name: string - user: agent:triage-bot - relation: can_call + relation: caller object: tool:acme_slack_search_channels condition: name: $expression @@ -70,7 +71,7 @@ tests: query: string - user: agent:triage-bot - relation: can_call + relation: caller object: tool:acme_slack_read_channel check: diff --git a/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml b/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml index f9793f6..cd913f9 100644 --- a/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml @@ -12,7 +12,8 @@ model: | type tool relations define organization : [organization] - define can_call: [agent:*, agent, agent with $expression] and agent from organization + define caller: [agent:*, agent, agent with $expression] + define can_call: caller and agent from organization tuples: - user: agent:triage-bot @@ -38,17 +39,17 @@ tuples: # Every agent in the acme organization can call acme_slack_read_channel. - user: agent:* - relation: can_call + relation: caller object: tool:acme_slack_read_channel # The assistant agent can call acme_slack_send_message for every channel: - user: agent:assistant - relation: can_call + relation: caller object: tool:acme_slack_send_message # The triage-bot agent can only use acme_slack_send_message for product-announcements - user: agent:triage-bot - relation: can_call + relation: caller object: tool:acme_slack_send_message condition: name: $expression From 96267eaed0f7cf357278c9e06cdde677f59576bc Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Wed, 23 Sep 2026 06:59:14 -0700 Subject: [PATCH 06/14] chore: clarified experimental feature behavior --- stores/mcp-gateway/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/mcp-gateway/README.md b/stores/mcp-gateway/README.md index e58bce4..16377fa 100644 --- a/stores/mcp-gateway/README.md +++ b/stores/mcp-gateway/README.md @@ -8,7 +8,7 @@ This folder demonstrates three different models for MCP Gateway use cases: * **A customer-facing multi-tenant MCP Gateway:** Where you write your own agents and want to control what the agents can do when interacting with each tenant's tools. Permissions are based on agent identity. * **An intent-based authorization scenario:** An iteration of the previous one showing how to apply the same ideas for authorizing calls based on inferred intent. -These examples use the **Dynamic Conditions** experimental feature. The FGA CLI enables inline expressions automatically when running model tests. When testing against an OpenFGA server, enable the feature explicitly: +These examples depend on the experimental **Dynamic Conditions** feature, whose syntax, behavior, API, and tuple format may change. The FGA CLI enables inline expressions automatically when running model tests. When testing against an OpenFGA server, enable the feature explicitly: ```bash openfga run --experimentals inline_expressions From fff668674708f49c91af2ea9aa8985b51e97e6d1 Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Wed, 23 Sep 2026 22:00:02 -0700 Subject: [PATCH 07/14] chore: improved clarity --- stores/mcp-gateway/README.md | 12 ++++---- stores/mcp-gateway/mcp-gateway.fga.yaml | 28 +++++++++---------- .../multi-tenant-mcp-gateway-intent.fga.yaml | 15 ++-------- .../multi-tenant-mcp-gateway.fga.yaml | 12 ++++---- 4 files changed, 31 insertions(+), 36 deletions(-) diff --git a/stores/mcp-gateway/README.md b/stores/mcp-gateway/README.md index 16377fa..4595d64 100644 --- a/stores/mcp-gateway/README.md +++ b/stores/mcp-gateway/README.md @@ -4,11 +4,11 @@ This folder demonstrates three different models for MCP Gateway use cases: -* **An employee-facing MCP Gateway:** Where you want to control how agents can call MCP servers on behalf of users. The permissions are based on user identity. +* **An employee-facing MCP Gateway:** Control which MCP server tools users can call through a gateway. Permissions are based on user identity and role membership. * **A customer-facing multi-tenant MCP Gateway:** Where you write your own agents and want to control what the agents can do when interacting with each tenant's tools. Permissions are based on agent identity. * **An intent-based authorization scenario:** An iteration of the previous one showing how to apply the same ideas for authorizing calls based on inferred intent. -These examples depend on the experimental **Dynamic Conditions** feature, whose syntax, behavior, API, and tuple format may change. The FGA CLI enables inline expressions automatically when running model tests. When testing against an OpenFGA server, enable the feature explicitly: +These examples depend on the experimental **Dynamic Conditions** feature, whose syntax, behavior, API, and tuple format may change. The FGA CLI enables inline expressions automatically when running model tests. Testing with an OpenFGA server requires OpenFGA v1.21.0 or later with the experimental feature enabled explicitly: ```bash openfga run --experimentals inline_expressions @@ -38,20 +38,22 @@ You can view the [multi-tenant-mcp-gateway.fga.yaml](multi-tenant-mcp-gateway.fg ## Intent-Based Authorization -This builds on the previous example, but agents do not have any permissions persisted in the OpenFGA database. +This builds on the previous example. Organization membership and tool ownership are persisted, but tool-call grants are supplied at request time instead of being persisted. **Requirements:** * Agents operate on behalf of an organization. -* Agents begin with zero permissions. +* Agents begin with no persisted tool-call grants. * An intent inference engine (out of scope for this example) will infer which tools the agent should call and with which parameters. This information is sent to OpenFGA as contextual tuples. This pattern also applies to intent-based authorization. Permissions can come from an intent-inference engine or a token that grants the agent fine-grained contextual permissions. +The model test writes its test-local `caller` tuples to a temporary datastore. A production application would instead include those grants as contextual tuples in its authorization check. + You can view the [multi-tenant-mcp-gateway-intent.fga.yaml](multi-tenant-mcp-gateway-intent.fga.yaml) example configuration. ## Try It Out -1. Make sure you have the [FGA CLI](https://github.com/openfga/cli/?tab=readme-ov-file#installation) +1. Make sure you have the [FGA CLI v0.81 or later](https://github.com/openfga/cli/?tab=readme-ov-file#installation) installed. 2. In the `mcp-gateway` directory, run `fga model test --tests .fga.yaml` diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index 32d941c..2aa38ba 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -5,10 +5,9 @@ model: | type gateway relations - # Gateway-level policies. - # If you write a user:* tuple, no gateway-level restrictions applied - # You can write any arbitrary policy with a dynamic condition - # This model shows an IP-based policy, arbitrarily defined by the Gateway administrator + # Gateway-level policies. + # If you write a user:* tuple, no gateway-level restrictions are applied + # This example restricts gateway access by IP using a condition defined in the model define allow_policies : [user:*, user:* with in_company_network] type user @@ -21,13 +20,14 @@ model: | relations # Adds the MCP server to the Gateway instance, to inherit its policies define gateway : [gateway] + # Allows assigning access to all RO or RW tools - define readonly_access : [user, role#assignee] - define readwrite_access : [user, role#assignee] + define readonly_access : [user, role#assignee] + define readwrite_access : [user, role#assignee] define can_call_readonly : (readonly_access or readwrite_access) and allow_policies from gateway define can_call_readwrite : readwrite_access and allow_policies from gateway - define can_call_any: allow_policies from gateway + define passes_gateway_policies: allow_policies from gateway type tool relations @@ -37,7 +37,7 @@ model: | define caller : [user, role#assignee, role#assignee with $expression, user with $expression] - define can_call : (caller and can_call_any from mcp_server) or (is_readonly and can_call_readonly from mcp_server) or (is_readwrite and can_call_readwrite from mcp_server) + define can_call : (caller and passes_gateway_policies from mcp_server) or (is_readonly and can_call_readonly from mcp_server) or (is_readwrite and can_call_readwrite from mcp_server) condition in_company_network(user_ip: ipaddress, cidr: string) { user_ip.in_cidr(cidr) @@ -47,13 +47,10 @@ tuples: ### Gateway level tuples - # If you write this tuple, there won't be global level policies #- user: user:* # relation: allow_policies # object: gateway:mcp-gateway - # Note that the developer building the MCP Gateway will need to allow admins to enter - # these kinds of policies - user: user:* relation: allow_policies @@ -71,7 +68,7 @@ tuples: ## These tuples are created based on MCP server tools definitions - user: user:* relation: is_readwrite - object: tool:slack_send_message + object: tool:slack_send_message - user: user:* relation: is_readonly @@ -98,7 +95,10 @@ tuples: relation: readwrite_access object: mcp_server:slack - # Assignees to the product role are the only ones that can post in product announcements + # Assignees to the product role are the only ones that can post in product announcements + # Note that the developer building the MCP Gateway will need to allow users to enter + # these kinds of policies + - user: role:product#assignee relation: caller object: tool:slack_send_message @@ -138,7 +138,7 @@ tests: assertions: can_call: true - # Bob does not have access when using another IP + # Bob does not have access when using another IP - user: user:bob object: tool:slack_send_message context: diff --git a/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml b/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml index 859a002..ae89826 100644 --- a/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml +++ b/stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml @@ -1,4 +1,4 @@ -name: MCP Gateway for B2B use cases, showing how to use OpenFGA for intent-based authorization +name: Customer-facing multi-tenant MCP Gateway with intent-based authorization model: | model schema 1.1 @@ -7,7 +7,7 @@ model: | type organization relations - define agent : [agent] + define agent : [agent] type tool relations @@ -69,18 +69,14 @@ tests: expression: "query == 'product-announcements'" parameters: query: string - - user: agent:triage-bot - relation: caller - object: tool:acme_slack_read_channel - check: - user: agent:triage-bot object: tool:acme_slack_search_channels context: query: "product-announcements" assertions: - can_call: true + can_call: true - user: agent:triage-bot object: tool:acme_slack_send_message @@ -103,11 +99,6 @@ tests: assertions: can_call: false - - user: agent:triage-bot - object: tool:acme_slack_read_channel - assertions: - can_call: true - - user: agent:triage-bot object: tool:acme_slack_schedule_message assertions: diff --git a/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml b/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml index cd913f9..68170d7 100644 --- a/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/multi-tenant-mcp-gateway.fga.yaml @@ -1,4 +1,4 @@ -name: MCP Gateway for B2B use cases +name: Customer-facing multi-tenant MCP Gateway model: | model schema 1.1 @@ -7,10 +7,11 @@ model: | type organization relations - define agent : [agent] + define agent : [agent] type tool relations + # The tool policies are per-organization define organization : [organization] define caller: [agent:*, agent, agent with $expression] define can_call: caller and agent from organization @@ -61,6 +62,7 @@ tuples: tests: - name: dynamic-rbac-example check: + # triage-bot can post to the channel allowed by its expression - user: agent:triage-bot object: tool:acme_slack_send_message context: @@ -68,6 +70,7 @@ tests: assertions: can_call: true + # assistant has an unconditional grant, so any channel is allowed - user: agent:assistant object: tool:acme_slack_send_message context: @@ -81,6 +84,7 @@ tests: assertions: can_call: false + # triage-bot cannot post to channels outside its expression - user: agent:triage-bot object: tool:acme_slack_send_message context: @@ -88,10 +92,8 @@ tests: assertions: can_call: false + # Channel context isn't needed because no conditional grant exists for this tool - user: agent:triage-bot object: tool:acme_slack_read_channel assertions: - # No need to send the channel name for 'read' operations - # if you are not allowing defining policies for 'read'. - # If you do, you should send it can_call: true \ No newline at end of file From 14600e46d3c3762aa5eec57551b3ee040c4448a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Aguiar?= Date: Thu, 24 Sep 2026 13:02:56 -0300 Subject: [PATCH 08/14] Update stores/mcp-gateway/mcp-gateway.fga.yaml Co-authored-by: Raghd Hamzeh --- stores/mcp-gateway/mcp-gateway.fga.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index 2aa38ba..37560c6 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -129,7 +129,7 @@ tests: assertions: can_call: true - # Bob has access as he is assigned to the product role + # Bob has access to this channel as he is assigned to the product role - user: user:bob object: tool:slack_send_message context: From 939557fded53d54149d759d041ebb0b415a5ca63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Aguiar?= Date: Thu, 24 Sep 2026 13:03:14 -0300 Subject: [PATCH 09/14] Update stores/mcp-gateway/mcp-gateway.fga.yaml Co-authored-by: Raghd Hamzeh --- stores/mcp-gateway/mcp-gateway.fga.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index 37560c6..4f20f24 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -112,7 +112,7 @@ tuples: tests: - name: dynamic-rbac-example check: - # Anne has access as the devops team has write access + # Anne has access as the devops team has write access to all channels - user: user:anne object: tool:slack_send_message context: From 214062de4731395dbab6ace95a4d761738fb686b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Aguiar?= Date: Thu, 24 Sep 2026 13:03:28 -0300 Subject: [PATCH 10/14] Update stores/mcp-gateway/mcp-gateway.fga.yaml Co-authored-by: Raghd Hamzeh --- stores/mcp-gateway/mcp-gateway.fga.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index 4f20f24..59810b8 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -147,7 +147,7 @@ tests: assertions: can_call: false - # Bob does not have access to channels outside the product announcements policy + # Bob does not have access to channels outside the product announcements policy, even when on the correct IP - user: user:bob object: tool:slack_send_message context: From 02164c9a4de682967c5805964b855630ad162dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Aguiar?= Date: Thu, 24 Sep 2026 13:03:44 -0300 Subject: [PATCH 11/14] Update stores/mcp-gateway/mcp-gateway.fga.yaml Co-authored-by: Raghd Hamzeh --- stores/mcp-gateway/mcp-gateway.fga.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index 59810b8..af27efb 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -47,7 +47,7 @@ tuples: ### Gateway level tuples - # If you write this tuple, there won't be global level policies + # If you write this tuple, there won't be any network restriction conditions. #- user: user:* # relation: allow_policies # object: gateway:mcp-gateway From c090bec3d4bbef424b731a22e82f4229caf9949d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Aguiar?= Date: Thu, 24 Sep 2026 13:06:03 -0300 Subject: [PATCH 12/14] Update stores/mcp-gateway/mcp-gateway.fga.yaml Co-authored-by: Raghd Hamzeh --- stores/mcp-gateway/mcp-gateway.fga.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index af27efb..d5b401a 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -25,7 +25,7 @@ model: | define readonly_access : [user, role#assignee] define readwrite_access : [user, role#assignee] - define can_call_readonly : (readonly_access or readwrite_access) and allow_policies from gateway + define can_call_readonly : (readonly_access or readwrite_access) and passes_gateway_policies define can_call_readwrite : readwrite_access and allow_policies from gateway define passes_gateway_policies: allow_policies from gateway From 8452c55cad2778e11c47e4146459d73923c23633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Aguiar?= Date: Thu, 24 Sep 2026 13:06:19 -0300 Subject: [PATCH 13/14] Update stores/mcp-gateway/mcp-gateway.fga.yaml Co-authored-by: Raghd Hamzeh --- stores/mcp-gateway/mcp-gateway.fga.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index d5b401a..d570002 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -26,7 +26,7 @@ model: | define readwrite_access : [user, role#assignee] define can_call_readonly : (readonly_access or readwrite_access) and passes_gateway_policies - define can_call_readwrite : readwrite_access and allow_policies from gateway + define can_call_readwrite : readwrite_access and passes_gateway_policies define passes_gateway_policies: allow_policies from gateway type tool From 2538d5d2f0e04aee86f718628473c2254b58a415 Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Thu, 24 Sep 2026 14:22:28 -0700 Subject: [PATCH 14/14] chore: simplified readonly scenario --- stores/mcp-gateway/mcp-gateway.fga.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/stores/mcp-gateway/mcp-gateway.fga.yaml b/stores/mcp-gateway/mcp-gateway.fga.yaml index d570002..08bc725 100644 --- a/stores/mcp-gateway/mcp-gateway.fga.yaml +++ b/stores/mcp-gateway/mcp-gateway.fga.yaml @@ -18,10 +18,10 @@ model: | type mcp_server relations - # Adds the MCP server to the Gateway instance, to inherit its policies + # Adds the MCP server to the gateway instance so it inherits its policies define gateway : [gateway] - # Allows assigning access to all RO or RW tools + # Allows assigning access to all read-only or read-write tools define readonly_access : [user, role#assignee] define readwrite_access : [user, role#assignee] @@ -32,12 +32,12 @@ model: | type tool relations define mcp_server : [mcp_server] + # Tools without the read-only flag are treated as read-write define is_readonly : [user:*] - define is_readwrite : [user:*] define caller : [user, role#assignee, role#assignee with $expression, user with $expression] - define can_call : (caller and passes_gateway_policies from mcp_server) or (is_readonly and can_call_readonly from mcp_server) or (is_readwrite and can_call_readwrite from mcp_server) + define can_call : (caller and passes_gateway_policies from mcp_server) or (is_readonly and can_call_readonly from mcp_server) or can_call_readwrite from mcp_server condition in_company_network(user_ip: ipaddress, cidr: string) { user_ip.in_cidr(cidr) @@ -65,19 +65,19 @@ tuples: relation: gateway object: mcp_server:slack - ## These tuples are created based on MCP server tools definitions - - user: user:* - relation: is_readwrite - object: tool:slack_send_message + ## These tuples are created from MCP server tool definitions + # slack_read_channel has the 'readOnlyHint' hint in the MCP Tool definition - user: user:* relation: is_readonly object: tool:slack_read_channel + # The Slack MCP server owns the slack_send_message tool - user: mcp_server:slack relation: mcp_server object: tool:slack_send_message + # The Slack MCP server owns the slack_read_channel tool - user: mcp_server:slack relation: mcp_server object: tool:slack_read_channel