Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
59 changes: 59 additions & 0 deletions stores/mcp-gateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# OpenFGA for MCP Gateways

## Use Case

This folder demonstrates three different models for MCP Gateway use cases:

* **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. Testing with an OpenFGA server requires OpenFGA v1.21.0 or later with the experimental feature enabled explicitly:
Comment thread
aaguiarz marked this conversation as resolved.
Comment thread
rhamzeh marked this conversation as resolved.

```bash
openfga run --experimentals inline_expressions
```

## 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 [mcp-gateway.fga.yaml](mcp-gateway.fga.yaml) example configuration.

## 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 [multi-tenant-mcp-gateway.fga.yaml](multi-tenant-mcp-gateway.fga.yaml) example configuration.

## Intent-Based Authorization

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 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 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 <store-file>.fga.yaml`
Comment thread
aaguiarz marked this conversation as resolved.
158 changes: 158 additions & 0 deletions stores/mcp-gateway/mcp-gateway.fga.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: MCP Gateway for Workforce use cases
Comment thread
rhamzeh marked this conversation as resolved.
model: |
model
schema 1.1

type gateway
relations
# 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

type role
relations
define assignee : [user]

type mcp_server
relations
# Adds the MCP server to the gateway instance so it inherits its policies
define gateway : [gateway]

# Allows assigning access to all read-only or read-write tools
define readonly_access : [user, role#assignee]
Comment thread
rhamzeh marked this conversation as resolved.
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 passes_gateway_policies
define passes_gateway_policies: allow_policies from gateway

type tool
relations
define mcp_server : [mcp_server]
# Tools without the read-only flag are treated as read-write
define is_readonly : [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 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 any network restriction conditions.
#- user: user:*
# relation: allow_policies
# object: gateway:mcp-gateway

- 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 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

### 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
# 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
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 to all channels
- user: user:anne
object: tool:slack_send_message
context:
channel_name: "#product-announcements"
user_ip: 192.168.0.1
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 to this channel 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
Comment thread
rhamzeh marked this conversation as resolved.
- 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, even when on the correct IP
- user: user:bob
object: tool:slack_send_message
context:
channel_name: "#general"
user_ip: 192.168.0.1
assertions:
can_call: false

105 changes: 105 additions & 0 deletions stores/mcp-gateway/multi-tenant-mcp-gateway-intent.fga.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Customer-facing multi-tenant MCP Gateway with intent-based authorization
Comment thread
rhamzeh marked this conversation as resolved.
model: |
model
schema 1.1

type agent

type organization
relations
define agent : [agent]

type tool
relations
define organization : [organization]
define caller: [agent, agent with $expression]
define can_call: caller 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
Comment thread
aaguiarz marked this conversation as resolved.

- name: test
tuples:
- user: agent:triage-bot
Comment thread
rhamzeh marked this conversation as resolved.
relation: caller
object: tool:acme_slack_send_message
condition:
name: $expression
context:
expression: "channel_name == '#product-announcements'"
parameters:
channel_name: string

- user: agent:triage-bot
relation: caller
object: tool:acme_slack_search_channels
condition:
name: $expression
context:
expression: "query == 'product-announcements'"
parameters:
query: string

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_schedule_message
assertions:
can_call: false
Loading
Loading