diff --git a/docs/toolhive/concepts/cedar-policies.mdx b/docs/toolhive/concepts/cedar-policies.mdx index 8bc4263b..30dd57fe 100644 --- a/docs/toolhive/concepts/cedar-policies.mdx +++ b/docs/toolhive/concepts/cedar-policies.mdx @@ -4,14 +4,16 @@ description: Writing and configuring Cedar policies for MCP server authorization. --- -This document provides detailed guidance on writing and configuring Cedar -policies for MCP server authorization. You'll learn how to create effective -policies, configure authorization settings, and troubleshoot common issues. +Cedar policies control which authenticated clients can access which tools, +prompts, and resources on your MCP servers. ToolHive evaluates these policies on +every request, denying anything not explicitly permitted. :::info For the conceptual overview of authentication and authorization, see -[Authentication and authorization framework](./auth-framework.mdx). +[Authentication and authorization framework](./auth-framework.mdx). For the +complete dictionary of entity types, actions, and attributes, see +[Authorization policy reference](../reference/authz-policy-reference.mdx). ::: @@ -50,7 +52,6 @@ The operation being performed on an MCP feature: - `Action::"call_tool"`: Call a tool - `Action::"get_prompt"`: Get a prompt - `Action::"read_resource"`: Read a resource - - `Action::"list_tools"`: List available tools ### Resource @@ -107,12 +108,13 @@ cedar: - `cedar`: The Cedar-specific configuration - `policies`: An array of Cedar policy strings - `entities_json`: A JSON string representing Cedar entities + - `group_claim_name`: Optional custom JWT claim name for group membership (for + example, `https://example.com/groups`) ## Writing effective policies -Understanding how to write Cedar policies is crucial for securing your MCP -servers effectively. This section provides practical guidance for creating -policies that match your security requirements. +This section covers common policy patterns, from simple tool-level permits to +role-based and attribute-based access control. ### Basic policy patterns @@ -149,6 +151,38 @@ permit(principal, action == Action::"call_tool", resource) when { This policy allows clients with the "admin" role to call any tool. RBAC is effective when you have well-defined roles in your organization. +### Group-based access control + +If your identity provider includes group claims in JWT tokens (for example, +`groups`, `roles`, or `cognito:groups`), ToolHive automatically creates +`THVGroup` entities that you can use with Cedar's `in` operator: + +```text +permit( + principal in THVGroup::"engineering", + action == Action::"call_tool", + resource +); +``` + +This policy allows any member of the "engineering" group to call any tool. +Group-based policies are useful when your identity provider manages group +memberships centrally. + +You can combine group membership with other conditions: + +```text +permit( + principal in THVGroup::"data-science", + action == Action::"call_tool", + resource == Tool::"query_database" +); +``` + +For details on how groups are resolved from JWT claims, see +[Group membership](../reference/authz-policy-reference.mdx#group-membership) in +the policy reference. + ### Attribute-based access control (ABAC) ABAC policies use multiple attributes to make fine-grained decisions: @@ -164,6 +198,162 @@ This policy allows data analysts to access sensitive data, but only if their clearance level is sufficient. ABAC provides the most flexibility for complex security requirements. +## Tool annotation policies + +MCP servers can declare behavioral hints on their tools using +[annotations](https://modelcontextprotocol.io/docs/specification/2025-06-18/server/tools#annotations). +ToolHive makes these annotations available as resource attributes during +`tools/call` authorization, letting you write policies based on what a tool +**does** rather than what it's **named**. + +Not all MCP servers set all annotation fields. Always use Cedar's `has` operator +to check for an annotation before accessing it, otherwise a missing attribute +causes a Cedar evaluation error that ToolHive treats as a deny. For the full +list of annotation attributes and detailed `has` operator guidance, see +[Tool annotation attributes](../reference/authz-policy-reference.mdx#tool-annotation-attributes). + +### Annotation policy examples + +#### Allow non-destructive, closed-world tools + +This pattern is useful when you want to allow tools that are both safe to run +and operate within a controlled environment: + +```text +permit( + principal, + action == Action::"call_tool", + resource +) when { + resource has destructiveHint && resource.destructiveHint == false && + resource has openWorldHint && resource.openWorldHint == false +}; +``` + +#### Block destructive tools for non-admin users + +```text +forbid( + principal, + action == Action::"call_tool", + resource +) when { + resource has destructiveHint && resource.destructiveHint == true && + !(principal.claim_roles.contains("admin")) +}; +``` + +## Real-world policy profiles + +These profiles represent common authorization patterns. They progress from most +restrictive to least restrictive. + +### Observe profile (read-only) + +Allow reading prompts and resources, but block all tool calls: + +```yaml title="authz-observe.yaml" +version: '1.0' +type: cedarv1 +cedar: + policies: + - 'permit(principal, action == Action::"get_prompt", resource);' + - 'permit(principal, action == Action::"read_resource", resource);' + entities_json: '[]' +``` + +This profile is useful for monitoring or auditing scenarios where clients need +access to prompts and data resources without executing any tools. + +:::note + +Because `tools/list` responses are +[filtered](../reference/authz-policy-reference.mdx#list-operation-filtering) +based on `call_tool` policies, tools won't appear in list responses under this +profile. Prompts and resources appear normally because `get_prompt` and +`read_resource` policies are present. + +::: + +### Safe tools profile + +Extend the observe profile to also allow tool calls for tools that MCP servers +have annotated as safe. This allows read-only tools and non-destructive +closed-world tools, while blocking everything else: + +```yaml title="authz-safe-tools.yaml" +version: '1.0' +type: cedarv1 +cedar: + policies: + # Prompt and resource access + - 'permit(principal, action == Action::"get_prompt", resource);' + - 'permit(principal, action == Action::"read_resource", resource);' + # Read-only tools + - >- + permit(principal, action == Action::"call_tool", resource) when { resource + has readOnlyHint && resource.readOnlyHint == true }; + # Non-destructive AND closed-world tools + - >- + permit(principal, action == Action::"call_tool", resource) when { resource + has destructiveHint && resource.destructiveHint == false && resource has + openWorldHint && resource.openWorldHint == false }; + entities_json: '[]' +``` + +:::tip + +Tools that omit all annotation attributes are denied under this profile, +preserving a conservative default-deny posture. Only tools that explicitly +declare safe annotations are allowed. + +::: + +### Tool allowlist profile + +Allow only specific, named tools. This is the most explicit approach and doesn't +depend on MCP servers setting annotations correctly: + +```yaml title="authz-allowlist.yaml" +version: '1.0' +type: cedarv1 +cedar: + policies: + - 'permit(principal, action == Action::"get_prompt", resource);' + - 'permit(principal, action == Action::"read_resource", resource);' + - 'permit(principal, action == Action::"call_tool", resource == + Tool::"search_code");' + - 'permit(principal, action == Action::"call_tool", resource == + Tool::"read_file");' + - 'permit(principal, action == Action::"call_tool", resource == + Tool::"list_repos");' + entities_json: '[]' +``` + +### RBAC with annotation guardrails + +Combine role-based access with annotation checks. Admins get full access, while +regular users are restricted to safe tools: + +```yaml title="authz-rbac-annotations.yaml" +version: '1.0' +type: cedarv1 +cedar: + policies: + # Everyone can read prompts and resources + - 'permit(principal, action == Action::"get_prompt", resource);' + - 'permit(principal, action == Action::"read_resource", resource);' + # Admins can call any tool + - >- + permit(principal, action == Action::"call_tool", resource) when { + principal.claim_roles.contains("admin") }; + # Non-admins can only call read-only tools + - >- + permit(principal, action == Action::"call_tool", resource) when { resource + has readOnlyHint && resource.readOnlyHint == true }; + entities_json: '[]' +``` + ## Working with JWT claims JWT claims from your identity provider become available in policies with a @@ -214,9 +404,10 @@ how you can control access based on request parameters. ## List operations and filtering -List operations (`tools/list`, `prompts/list`, `resources/list`) work -differently from other operations. They're always allowed, but the response is -automatically filtered based on what the user can actually access: +List operations (`tools/list`, `prompts/list`, `resources/list`) bypass +request-level authorization entirely. ToolHive forwards the list request to the +MCP server, then automatically filters the response based on what the caller is +authorized to access: - `tools/list` shows only tools the user can call (based on `call_tool` policies) @@ -266,38 +457,29 @@ permissions and then trying to restrict them. also make your policy set harder to understand. In most cases, the default deny behavior is sufficient. +**Guard annotation access with `has`:** Always use `resource has ` before +accessing annotation attributes. Many MCP servers only set some annotations, and +unguarded access causes evaluation errors that result in a deny. + **Test your policies:** Always test policies with real requests to ensure they work as expected. Pay special attention to edge cases and error conditions. ## Advanced policy examples -### Combining JWT claims and tool arguments - -You can combine JWT claims and tool arguments in your policies to create more -sophisticated authorization rules: - -```text -permit(principal, action == Action::"call_tool", resource == Tool::"sensitive_data") when { - principal.claim_roles.contains("data_analyst") && - resource.arg_data_level <= principal.claim_clearance_level -}; -``` - -This policy allows clients with the "data_analyst" role to access the -sensitive_data tool, but only if their clearance level (from JWT claims) is -sufficient for the requested data level (from tool arguments). - ### Multi-tenant environments -In multi-tenant environments, you can use policies to isolate tenants: +In multi-tenant environments, you can use custom entity attributes in +`entities_json` to isolate tenants: ```text -permit(principal, action, resource) when { - principal.claim_tenant_id == resource.tenant_id +permit(principal, action == Action::"call_tool", resource) when { + resource.tenant_id == principal.claim_tenant_id }; ``` -This ensures that clients can only access resources belonging to their tenant. +This ensures that clients can only access tools belonging to their tenant. You +must define the `tenant_id` attribute on each tool entity in `entities_json` for +this pattern to work. ### Data sensitivity levels @@ -311,29 +493,18 @@ permit(principal, action == Action::"call_tool", resource == Tool::"data_access" This ensures that clients can only access data within their clearance level. -### Geographic restrictions - -For geographically restricted resources: - -```text -permit(principal, action == Action::"call_tool", resource == Tool::"geo_restricted") when { - principal.claim_location in ["US", "Canada", "Mexico"] -}; -``` - -This restricts access based on the client's location. - -### Time-based access +### Argument-scoped access -For resources that should only be accessible during certain hours: +Restrict a tool to specific argument values: ```text -permit(principal, action == Action::"call_tool", resource == Tool::"business_hours") when { - context.current_hour >= 9 && context.current_hour <= 17 +permit(principal, action == Action::"call_tool", resource == Tool::"calculator") when { + resource.arg_operation == "add" || resource.arg_operation == "subtract" }; ``` -This restricts access to business hours only. +This permits calling the calculator tool, but only for the "add" and "subtract" +operations. ## Entity attributes @@ -368,6 +539,26 @@ This configuration defines a custom entity for the weather tool with an `owner` attribute set to `user123`. The policy allows clients to call tools only if they own them. +For the complete list of built-in attributes available on each entity type, see +the [Authorization policy reference](../reference/authz-policy-reference.mdx). + +## Next steps + +- Look up every available entity type, action, and attribute in the + [Authorization policy reference](../reference/authz-policy-reference.mdx) +- Set up authentication and authorization for + [CLI-managed MCP servers](../guides-cli/auth.mdx) or + [Kubernetes-deployed MCP servers](../guides-k8s/auth-k8s.mdx) +- Follow the end-to-end + [Role-based authorization with Okta](../integrations/okta.mdx) tutorial + +## Related information + +- [Authentication and authorization framework](./auth-framework.mdx) -- + Conceptual overview of ToolHive's auth architecture +- [Cedar documentation](https://docs.cedarpolicy.com/) -- Official Cedar policy + language reference + ## Troubleshooting policies When policies don't work as expected, follow this systematic approach: @@ -378,9 +569,13 @@ When policies don't work as expected, follow this systematic approach: valid Cedar syntax. 2. **Verify entity matching:** Confirm that the principal, action, and resource in your policies match the actual values in the request. -3. **Test conditions:** Check that any conditions in your policies are satisfied +3. **Check `has` guards:** If your policy references annotation attributes + (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), + ensure you're using `resource has ` before accessing them. A missing + attribute causes an evaluation error, which ToolHive treats as a deny. +4. **Test conditions:** Check that any conditions in your policies are satisfied by the request context. -4. **Remember default deny:** If no policy explicitly permits the request, it +5. **Remember default deny:** If no policy explicitly permits the request, it will be denied. ### JWT claims are not available @@ -400,12 +595,27 @@ When policies don't work as expected, follow this systematic approach: `arg_` prefix. 3. **Verify argument names:** Confirm that the argument names in your policies match those in the actual requests. - -## Related information - -- For the conceptual overview, see - [Authentication and authorization framework](./auth-framework.mdx) -- For detailed Cedar policy syntax, see - [Cedar documentation](https://docs.cedarpolicy.com/) -- For a practical example of Cedar policies enforcing role-based access control, - see [Role-based authorization with Okta](../integrations/okta.mdx) +4. **Check argument types:** Complex arguments (objects, arrays) are not + available directly. Instead, check for `arg__present == true`. + +### Tool annotations are not available + +1. **Check MCP server support:** Not all MCP servers set annotation hints on + their tools. Check the server's `tools/list` response to see which + annotations are present. +2. **Use `has` guards:** Always check `resource has readOnlyHint` before + accessing `resource.readOnlyHint`. A missing annotation attribute is not the + same as `false` -- it simply doesn't exist. +3. **Verify annotation source:** Annotations come from the MCP server's + `tools/list` response, not from the client's `tools/call` request. If you + don't see annotations, the MCP server may not be setting them. + +### Groups are not working + +1. **Check JWT claims:** Verify that your JWT token contains a group claim + (`groups`, `roles`, or `cognito:groups`). +2. **Configure custom claim name:** If your identity provider uses a + non-standard claim name, set `group_claim_name` in the Cedar configuration. +3. **Use correct syntax:** Use `principal in THVGroup::"group-name"` rather than + `principal.claim_groups.contains("group-name")`. Both evaluate correctly, but + the `in` syntax is the idiomatic Cedar approach for group membership. diff --git a/docs/toolhive/guides-cli/auth.mdx b/docs/toolhive/guides-cli/auth.mdx index b1517c15..ae0a5ab4 100644 --- a/docs/toolhive/guides-cli/auth.mdx +++ b/docs/toolhive/guides-cli/auth.mdx @@ -156,9 +156,12 @@ denied with a 403 Forbidden response. - [Authentication and authorization framework](../concepts/auth-framework.mdx) for conceptual understanding -- [Cedar policies](../concepts/cedar-policies.mdx) and the - [Cedar documentation](https://docs.cedarpolicy.com/) for detailed policy - syntax +- [Cedar policies](../concepts/cedar-policies.mdx) for policy patterns and + examples +- [Authorization policy reference](../reference/authz-policy-reference.mdx) for + the complete dictionary of entity types, actions, and attributes +- [Cedar documentation](https://docs.cedarpolicy.com/) for the Cedar policy + language specification ## Troubleshooting diff --git a/docs/toolhive/guides-k8s/auth-k8s.mdx b/docs/toolhive/guides-k8s/auth-k8s.mdx index f88a4f2e..cab520ef 100644 --- a/docs/toolhive/guides-k8s/auth-k8s.mdx +++ b/docs/toolhive/guides-k8s/auth-k8s.mdx @@ -775,8 +775,11 @@ kubectl logs -n toolhive-system -l app.kubernetes.io/name=weather-server-k8s [Embedded authorization server](../concepts/auth-framework.mdx#embedded-authorization-server) - For a similar configuration pattern using token exchange, see [Configure token exchange](./token-exchange-k8s.mdx) -- For detailed Cedar policy syntax, see - [Cedar policies](../concepts/cedar-policies.mdx) and the +- For policy patterns and examples, see + [Cedar policies](../concepts/cedar-policies.mdx) +- For the complete dictionary of entity types, actions, and attributes, see + [Authorization policy reference](../reference/authz-policy-reference.mdx) +- For the Cedar policy language specification, see [Cedar documentation](https://docs.cedarpolicy.com/) - For a complete end-to-end example with Okta OIDC and role-based access control, see [Role-based authorization with Okta](../integrations/okta.mdx) diff --git a/docs/toolhive/reference/authz-policy-reference.mdx b/docs/toolhive/reference/authz-policy-reference.mdx new file mode 100644 index 00000000..4f976b0d --- /dev/null +++ b/docs/toolhive/reference/authz-policy-reference.mdx @@ -0,0 +1,496 @@ +--- +title: Authorization policy reference +description: + Complete reference for Cedar entity types, actions, attributes, and + annotations available when writing ToolHive authorization policies. +--- + +This page lists the Cedar entity types, actions, attributes, and annotations +available when writing authorization policies for ToolHive MCP servers. It also +covers group membership and the HTTP PDP model for external policy decision +points. + +For conceptual guidance and practical examples, see +[Cedar policies](../concepts/cedar-policies.mdx). + +## Cedar entity types + +Every Cedar authorization request involves three entity types: a principal, an +action, and a resource. ToolHive maps MCP concepts to these Cedar entities +automatically. + +| Entity type | Format | Description | +| ------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Client` | `Client::""` | The authenticated user, identified by the `sub` claim from the access token | +| `Action` | `Action::""` | The MCP operation being performed | +| `Tool` | `Tool::""` | A tool resource (used for `tools/call`) | +| `Prompt` | `Prompt::""` | A prompt resource (used for `prompts/get`) | +| `Resource` | `Resource::""` | A data resource (used for `resources/read`). The URI is [sanitized](#resource-uri-sanitization) for Cedar compatibility | +| `FeatureType` | `FeatureType::""` | A feature category entity. Values: `tool`, `prompt`, `resource`. Not currently used for authorization; list operations are handled via [response filtering](#list-operation-filtering) | +| `THVGroup` | `THVGroup::""` | A group membership entity. Used with Cedar's `in` operator for [group-based policies](#group-membership) | + +## Cedar actions + +ToolHive maps MCP methods to Cedar actions. Each action corresponds to a +specific MCP operation. + +### Actions that require authorization + +These actions are evaluated against your Cedar policies: + +| Action | MCP method | Description | +| ------------------------- | ---------------- | ----------------------------- | +| `Action::"call_tool"` | `tools/call` | Call a specific tool | +| `Action::"get_prompt"` | `prompts/get` | Retrieve a specific prompt | +| `Action::"read_resource"` | `resources/read` | Read a specific data resource | + +### List operations + +List methods (`tools/list`, `prompts/list`, `resources/list`) bypass +request-level authorization entirely. ToolHive allows the list request through +and filters the response to include only items the caller is authorized to +access using the individual-access actions above. See +[List operation filtering](#list-operation-filtering) for details. + +### Always-allowed MCP methods + +These MCP methods bypass authorization entirely. You cannot write policies to +restrict them: + +| MCP method | Purpose | +| --------------------- | ---------------------------------- | +| `initialize` | Protocol initialization handshake | +| `ping` | Health check | +| `features/list` | Capability discovery | +| `roots/list` | Root directory discovery | +| `logging/setLevel` | Client logging preference | +| `completion/complete` | Argument auto-completion | +| `notifications/*` | All server-to-client notifications | + +### Denied-by-default MCP methods + +These MCP methods are not in the authorization map and are always denied. They +require new authorization features before they can be enabled: + +- `elicitation/create` -- User input prompting +- `sampling/createMessage` -- LLM text generation +- `tasks/list`, `tasks/get`, `tasks/cancel`, `tasks/result` -- Task management + +## Principal attributes + +The principal entity (`Client::`) receives all JWT claims from the access token +with a `claim_` prefix. Any claim in the token becomes an attribute you can +reference in policies. + +### Common principal attributes + +| Attribute | Source | Cedar type | Description | +| -------------- | ------------ | -------------- | ----------------------------------------------------------------- | +| `claim_sub` | JWT `sub` | String | Subject identifier (also used as the entity ID) | +| `claim_name` | JWT `name` | String | Display name | +| `claim_email` | JWT `email` | String | Email address | +| `claim_roles` | JWT `roles` | Set of Strings | Role memberships | +| `claim_groups` | JWT `groups` | Set of Strings | Group memberships | +| `claim_role` | JWT `role` | String | Single role (some identity providers use this instead of `roles`) | +| `claim_` | JWT `` | Varies | Any other JWT claim | + +:::tip + +The exact attributes available depend on your identity provider and token +configuration. Check your access token's claims to see what's available. Every +claim becomes a `claim_`-prefixed attribute automatically. + +::: + +### Claim type mapping + +| JWT claim type | Cedar type | +| -------------------- | ------------------------------------------- | +| String | `String` | +| Boolean | `Bool` | +| Integer | `Long` | +| Float | `Decimal` | +| Array of strings | `Set` of `String` values | +| Array of mixed types | `Set` (each element converted individually) | + +## Resource attributes + +Resource attributes vary depending on the type of MCP operation. Each operation +type provides a different set of attributes on the resource entity. + +### Tool call attributes (`tools/call`) + +When a client calls a tool, the resource entity (`Tool::`) has these attributes: + +| Attribute | Type | Description | +| ----------------- | ------ | ------------------------------------------------------------------------------- | +| `name` | String | The tool name | +| `operation` | String | Always `"call"` | +| `feature` | String | Always `"tool"` | +| `readOnlyHint` | Bool | From [tool annotations](#tool-annotation-attributes), if the MCP server sets it | +| `destructiveHint` | Bool | From tool annotations, if set | +| `idempotentHint` | Bool | From tool annotations, if set | +| `openWorldHint` | Bool | From tool annotations, if set | +| `arg_` | Varies | Tool argument values (see [argument preprocessing](#argument-preprocessing)) | + +### Prompt get attributes (`prompts/get`) + +When a client retrieves a prompt, the resource entity (`Prompt::`) has these +attributes: + +| Attribute | Type | Description | +| ----------- | ------ | ---------------------- | +| `name` | String | The prompt name | +| `operation` | String | Always `"get"` | +| `feature` | String | Always `"prompt"` | +| `arg_` | Varies | Prompt argument values | + +### Resource read attributes (`resources/read`) + +When a client reads a data resource, the resource entity (`Resource::`) has +these attributes: + +| Attribute | Type | Description | +| ----------- | ------ | ----------------------------------------- | +| `name` | String | The sanitized URI (same as the entity ID) | +| `uri` | String | The original, unsanitized resource URI | +| `operation` | String | Always `"read"` | +| `feature` | String | Always `"resource"` | +| `arg_` | Varies | Request argument values | + +### Feature list attributes (list operations) + +:::info[Not currently used] + +The `FeatureType` entity and its attributes are defined in the Cedar authorizer +but are not evaluated during normal request processing. List operations bypass +request-level authorization and use +[response filtering](#list-operation-filtering) instead. This section is +included for completeness. + +::: + +When a client lists tools, prompts, or resources, the `FeatureType::` entity has +these attributes: + +| Attribute | Type | Description | +| ----------- | ------ | ------------------------------------------------------- | +| `name` | String | The feature type (same as the entity ID) | +| `type` | String | The feature type: `"tool"`, `"prompt"`, or `"resource"` | +| `operation` | String | Always `"list"` | +| `feature` | String | The feature type | + +## Tool annotation attributes + +MCP servers can declare behavioral hints on their tools through +[annotations](https://modelcontextprotocol.io/docs/specification/2025-06-18/server/tools#annotations). +ToolHive caches these annotations from `tools/list` responses and makes them +available as resource attributes during `tools/call` authorization. + +| Attribute | Type | Meaning when `true` | Meaning when `false` | +| ----------------- | ---- | -------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| `readOnlyHint` | Bool | The tool only reads data; it does not modify anything | The tool may modify data | +| `destructiveHint` | Bool | The tool may perform destructive or irreversible operations | The tool's modifications are non-destructive or reversible | +| `idempotentHint` | Bool | Calling the tool multiple times with the same arguments produces the same result | Repeated calls may have different effects | +| `openWorldHint` | Bool | The tool interacts with external systems outside the MCP server's control | The tool operates only within a closed, controlled environment | + +:::warning[Annotations may be absent] + +Not all MCP servers set all annotation fields. An annotation attribute is only +present on the resource entity when the MCP server explicitly sets it. If a tool +omits an annotation, that attribute does not exist on the entity. + +Always use Cedar's `has` operator to check for the presence of an annotation +before accessing its value. Without `has`, accessing a missing attribute causes +a Cedar evaluation error, which ToolHive treats as a deny. + +::: + +### The `has` operator + +The `has` operator is essential for writing safe annotation-based policies. It +checks whether an attribute exists on an entity before you try to read it: + +```text +// Safe: checks existence before access +permit( + principal, + action == Action::"call_tool", + resource +) when { + resource has readOnlyHint && resource.readOnlyHint == true +}; +``` + +```text +// Unsafe: fails with an evaluation error if readOnlyHint is absent +permit( + principal, + action == Action::"call_tool", + resource +) when { + resource.readOnlyHint == true +}; +``` + +### Trust boundary + +Annotations are sourced exclusively from the MCP server's `tools/list` response, +not from the client's `tools/call` request. This prevents a malicious client +from setting `readOnlyHint: true` on a destructive tool to bypass +annotation-based policies. + +## Context attributes + +The Cedar context record contains a merged copy of all JWT claims and tool +arguments. This gives you an alternative way to reference these values in +policies. Context attributes use the same prefixes as entity attributes: + +| Prefix | Source | Example | +| ------------- | --------------------- | -------------------------------------------- | +| `claim_` | JWT claims | `context.claim_email == "admin@example.com"` | +| `arg_` | Tool/prompt arguments | `context.arg_location == "New York"` | + +You can use either entity attributes or context attributes in your policies. +Both contain the same values: + +```text +// These are equivalent: +principal.claim_roles.contains("admin") +context.claim_roles.contains("admin") + +// These are also equivalent: +resource.arg_location == "New York" +context.arg_location == "New York" +``` + +## Group membership + +ToolHive automatically extracts group claims from JWT tokens and creates +`THVGroup` parent entities for the principal. This lets you write group-based +policies using Cedar's `in` operator. + +### How groups are resolved + +ToolHive checks the following JWT claim names in order and uses the first one +found: + +1. Custom claim name (if configured via `group_claim_name` in Cedar config) +2. `groups` -- Microsoft Entra ID, Okta, Auth0, PingIdentity +3. `roles` -- Keycloak +4. `cognito:groups` -- AWS Cognito + +The claim value must be an array of strings. Each string becomes a `THVGroup` +entity, and the principal is added as a child of each group. + +### Group policy examples + +```text +// Allow members of the "engineering" group to call any tool +permit( + principal in THVGroup::"engineering", + action == Action::"call_tool", + resource +); +``` + +```text +// Allow only the "platform" group to read infrastructure resources +permit( + principal in THVGroup::"platform", + action == Action::"read_resource", + resource +); +``` + +### Configuring a custom group claim + +If your identity provider uses a non-standard claim name for groups (for +example, Auth0 namespaced claims), configure it in the Cedar authorization +config: + +```yaml title="authz-config.yaml" +version: '1.0' +type: cedarv1 +cedar: + group_claim_name: 'https://example.com/groups' + policies: + - 'permit(principal in THVGroup::"admins", action, resource);' + entities_json: '[]' +``` + +## Argument preprocessing + +Tool and prompt arguments are converted to Cedar-compatible types with an `arg_` +prefix. The conversion rules depend on the argument's Go type: + +| Argument type | Cedar attribute | Cedar type | Example | +| ----------------------- | ------------------- | -------------------- | ------------------------------------- | +| String | `arg_` | String | `resource.arg_location == "NYC"` | +| Boolean | `arg_` | Bool | `resource.arg_verbose == true` | +| Integer | `arg_` | Long | `resource.arg_limit == 10` | +| Float | `arg_` | Decimal | `resource.arg_threshold == 0.95` | +| Complex (object, array) | `arg__present` | Bool (always `true`) | `resource.arg_config_present == true` | + +Complex argument types (objects, nested arrays) cannot be represented directly +in Cedar. Instead, ToolHive creates a boolean `arg__present` attribute set +to `true`, which lets you check whether the argument was provided without +inspecting its value. + +## Resource URI sanitization + +For `resources/read` operations, the resource URI is sanitized to create a valid +Cedar entity ID. The following characters are replaced with underscores (`_`): +`:`, `/`, `\`, `?`, `&`, `=`, `#`, `.`, and ` ` (space). + +For example, the URI `file:///data/config.json` becomes the entity ID +`Resource::"file____data_config_json"`. + +To write policies against resource URIs, use the unsanitized `uri` attribute +instead of matching the entity ID directly: + +```text +// Use the uri attribute for readable policies +permit( + principal, + action == Action::"read_resource", + resource +) when { + resource.uri == "file:///data/config.json" +}; +``` + +## List operation filtering + +List operations (`tools/list`, `prompts/list`, `resources/list`) bypass +request-level authorization entirely. ToolHive forwards the list request to the +MCP server, then filters the response to include only items the caller is +authorized to access. + +For each item in the list response, ToolHive runs a policy check using the +corresponding individual-access action: + +| List method | Per-item check uses | +| ---------------- | -------------------------------------------------------------------- | +| `tools/list` | `Action::"call_tool"` against each `Tool::""` | +| `prompts/list` | `Action::"get_prompt"` against each `Prompt::""` | +| `resources/list` | `Action::"read_resource"` against each `Resource::""` | + +This means you don't need separate list policies. Your `call_tool`, +`get_prompt`, and `read_resource` policies automatically control what appears in +list responses. For resources, the per-item check uses the +[sanitized](#resource-uri-sanitization) entity ID, while the original URI +remains available via the `resource.uri` attribute. + +:::note + +Because list responses are filtered using `call_tool`, `get_prompt`, and +`read_resource` policies, an item only appears in a list response when the +corresponding individual-access policy permits it. For example, if no +`call_tool` policy permits a given tool, that tool won't appear in `tools/list` +responses. + +::: + +## Custom static entities + +You can define custom entities with arbitrary attributes using the +`entities_json` field in your Cedar configuration. These entities are merged +with the dynamically created entities at evaluation time. This lets you attach +metadata to tools, prompts, or resources that you can reference in policies. + +```yaml title="authz-config.yaml" +version: '1.0' +type: cedarv1 +cedar: + policies: + - | + permit( + principal, + action == Action::"call_tool", + resource + ) when { + resource.owner == principal.claim_sub + }; + entities_json: | + [ + { + "uid": "Tool::weather", + "attrs": { + "owner": "user123", + "department": "engineering" + } + }, + { + "uid": "Tool::billing", + "attrs": { + "owner": "finance-bot", + "department": "finance" + } + } + ] +``` + +:::warning + +Static entity attributes are merged with dynamic attributes at evaluation time. +The dynamic attributes (`name`, `operation`, `feature`, and any `arg_` or +annotation attributes) always take precedence over static ones. Don't define +static attributes using reserved names. + +::: + +## HTTP PDP PORC mapping + +The HTTP PDP authorizer (`httpv1`) maps MCP requests to a PORC +(Principal-Operation-Resource-Context) model for external policy decision +points. + +### PORC fields + +| Field | Format | Example | +| ----------------------------------------- | -------------------------------------------------- | --------------------------------- | +| `principal.sub` | JWT `sub` claim | `"user@example.com"` | +| `principal.roles` or `principal.mroles` | Depends on [claim mapper](#http-pdp-claim-mappers) | `["developer"]` | +| `principal.groups` or `principal.mgroups` | Depends on claim mapper | `["engineering"]` | +| `principal.scopes` | JWT `scope` or `scopes` | `["read", "write"]` | +| `operation` | `mcp::` | `"mcp:tool:call"` | +| `resource` | `mrn:mcp:::` | `"mrn:mcp:myserver:tool:weather"` | + +### PORC context fields + +Context fields are optional and controlled by the `context` configuration: + +| Field | Config required | Description | +| ----------------------------------------- | ----------------------------- | --------------------- | +| `context.mcp.feature` | `include_operation: true` | MCP feature type | +| `context.mcp.operation` | `include_operation: true` | MCP operation type | +| `context.mcp.resource_id` | `include_operation: true` | Resource identifier | +| `context.mcp.args.` | `include_args: true` | Tool/prompt arguments | +| `context.mcp.annotations.readOnlyHint` | Automatic for tool operations | Tool annotation hint | +| `context.mcp.annotations.destructiveHint` | Automatic for tool operations | Tool annotation hint | +| `context.mcp.annotations.idempotentHint` | Automatic for tool operations | Tool annotation hint | +| `context.mcp.annotations.openWorldHint` | Automatic for tool operations | Tool annotation hint | + +### HTTP PDP claim mappers + +| Mapper | Config value | Principal fields | Compatible with | +| ------------- | --------------------------- | ------------------------------------------------------------------ | ------------------------------------------- | +| MPE | `claim_mapping: "mpe"` | `sub`, `mroles`, `mgroups`, `scopes`, `mclearance`, `mannotations` | Manetu PolicyEngine | +| Standard OIDC | `claim_mapping: "standard"` | `sub`, `roles`, `groups`, `scopes` | Generic PDPs expecting standard OIDC claims | + +## Next steps + +- Learn practical policy patterns and profiles in + [Cedar policies](../concepts/cedar-policies.mdx) +- Set up authorization for [CLI-managed MCP servers](../guides-cli/auth.mdx) or + [Kubernetes-deployed MCP servers](../guides-k8s/auth-k8s.mdx) +- Follow the end-to-end + [Role-based authorization with Okta](../integrations/okta.mdx) tutorial + +## Related information + +- [Authentication and authorization](../concepts/auth-framework.mdx) -- Overview + of the authentication and authorization framework +- [Cedar documentation](https://docs.cedarpolicy.com/) -- Official Cedar policy + language reference diff --git a/sidebars.ts b/sidebars.ts index 8a9ca36f..0a91c59c 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -285,6 +285,7 @@ const sidebars: SidebarsConfig = { 'toolhive/reference/client-compatibility', 'toolhive/reference/index', + 'toolhive/reference/authz-policy-reference', 'toolhive/faq', 'toolhive/enterprise', 'toolhive/support',