Skip to content
Open
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
162 changes: 157 additions & 5 deletions docs/authz.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ The authorization framework consists of the following components:

### Available authorizers

Currently, ToolHive provides the following authorizer implementation:
ToolHive provides the following authorizer implementations:

| Type | Description |
|------|-------------|
| Type | Description |
|------|--------------------------------------------------------------------------------------------------|
| `cedarv1` | Authorization using [Cedar](https://www.cedarpolicy.com/), a policy language developed by Amazon |
| `httpv1` | Authorization using an external HTTP-based Policy Decision Point (PDP) with PORC model |

The framework is designed to support additional authorizers in the future (e.g.,
OPA, Casbin, or custom implementations).
The framework is designed to support additional authorizers (e.g., OPA, Casbin,
or custom implementations).

## How it works

Expand Down Expand Up @@ -368,6 +369,157 @@ This means that `forbid` policies take precedence over `permit` policies.

---

## HTTP PDP authorizer (`httpv1`)

The HTTP PDP authorizer provides authorization using an external HTTP-based Policy
Decision Point (PDP). This is a general-purpose authorizer that can work with
any PDP server that implements the PORC (Principal-Operation-Resource-Context)
decision endpoint.

### HTTP PDP configuration

The authorizer connects to a remote PDP server via HTTP. This allows you to
share a single PDP across multiple services or run the PDP as a sidecar service.

#### YAML format

```yaml
version: "1.0"
type: httpv1
pdp:
http:
url: "http://localhost:9000"
timeout: 30 # Optional, timeout in seconds (default: 30)
insecure_skip_verify: false # Optional, skip TLS verification (default: false)
```

#### JSON format

```json
{
"version": "1.0",
"type": "httpv1",
"pdp": {
"http": {
"url": "http://localhost:9000",
"timeout": 30,
"insecure_skip_verify": false
}
}
}
```

The configuration fields are:

Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation inconsistency: The configuration documentation at lines 413-416 mentions three fields, but the example at line 13 shows an undocumented mode: http field in the pdp section. This field should either be documented here or removed from the examples.

Suggested change
- `pdp.http.mode`: The mode/transport used to communicate with the PDP (for example, `http`; default: `http`)

Copilot uses AI. Check for mistakes.
- `pdp.http.url`: The base URL of the PDP server (required)
- `pdp.http.timeout`: HTTP request timeout in seconds (default: 30)
- `pdp.http.insecure_skip_verify`: Skip TLS certificate verification (default: false)
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nolint directive for gosec correctly identifies that InsecureSkipVerify is a security concern. However, the configuration documentation should include a stronger warning that this option should never be used in production environments. Consider adding a security warning in the documentation similar to "WARNING: Only use insecure_skip_verify for local testing. Never enable this in production as it disables TLS certificate validation and makes the connection vulnerable to man-in-the-middle attacks."

Copilot uses AI. Check for mistakes.

### Context configuration

The context configuration controls what MCP-specific information is included in
the PORC `context` object. By default, no MCP context is included. You can enable
specific context fields based on your policy requirements.

```yaml
version: "1.0"
type: httpv1
pdp:
http:
url: "http://localhost:9000"
context:
include_args: true # Include tool/prompt arguments in context.mcp.args
include_operation: true # Include feature, operation, and resource_id in context.mcp
```

The context configuration fields are:

- `pdp.context.include_args`: When `true`, includes tool/prompt arguments in
`context.mcp.args`. Default is `false`.
- `pdp.context.include_operation`: When `true`, includes MCP operation metadata
(`feature`, `operation`, `resource_id`) in `context.mcp`. Default is `false`.

**Important**: If your policies reference `input.context.mcp.*` fields (such as
`input.context.mcp.resource_id` for determining public tools/resources), you must
enable the corresponding context option. Otherwise, those fields will not be
present in the PORC and your policies will not work as expected.

### PORC mapping

The HTTP PDP authorizer uses the PORC (Principal-Operation-Resource-Context)
model for authorization decisions. ToolHive automatically maps MCP requests to
PORC:

| MCP Concept | PORC Field | Format |
|-------------|------------|--------|
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent formatting: The table has inconsistent column widths compared to the table at lines 33-36. While this may render correctly, maintaining consistent formatting improves readability of the markdown source. Consider aligning the dashes to match the column widths like in the earlier table.

Suggested change
|-------------|------------|--------|
|-----------|----------|--------|

Copilot uses AI. Check for mistakes.
| Client identity | `principal.sub` | From JWT `sub` claim |
| Roles | `principal.mroles` | From JWT `roles` or `mroles` claim |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why mroles or mgroups instead of roles/groups?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MPE is derived from our platform product, and our platform product had a scheme where any Manetu-specific claims added to the JWT had an 'm' prefix (mroles, mclearance, mannotations, etc). It probably made more sense when this was an MPE-specific patch, but now that it's moving in a "it's generic HTTP" direction, it stands out more. Not sure what the best solution is, with the current MPE they need those prefixes to work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe that this needs to be more abstract, and we could add some mappers between the specific implementations and the generic settings

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's what I am thinking: originally, this PR was written to add "mpev1" to the config. This was generalized to 'httpv1' in recognition that much of it is applicable to many contexts. However, maybe I took it too far in the generalization.

So, what I am thinking is:

  • most of the current logic for http-based authorizers remains as a reusable substrate
  • we have an 'mpev1' type that uses that substrate but does the MPE specific mapping and can have a natural organization to the documentation (e.g. m prefixes needed, operation mandatory, etc).
  • other systems may come along and also share the http-based authorizers code, but put whatever mappings, etc, they need in place in a similar way.

thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like that could works:

We could use a ClaimMapper interface with type-specific implementations:

  • MPEClaimMapper for type: mpev1 - handles m-prefixed claims (mroles, mgroups, mannotations)
  • OIDCClaimMapper for future type: oidcv1 - uses standard OIDC claims (roles, groups)

The mapper would be injected into PORCBuilder based on the authorizer type. This keeps the shared HTTP
infrastructure (client, PORC builder, context config) generic while allowing each PDP type to have its
own claim mapping conventions.

| Groups | `principal.mgroups` | From JWT `groups` or `mgroups` claim |
| Scopes | `principal.scopes` | From JWT `scope` or `scopes` claim |
| MCP operation | `operation` | `mcp:<feature>:<operation>` (e.g., `mcp:tool:call`) |
| MCP resource | `resource` | `mrn:mcp:<server>:<feature>:<id>` (e.g., `mrn:mcp:myserver:tool:weather`) |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add some comment explaining this format, some link to documentation, etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do (we have an explainer here: https://manetu.github.io/policyengine/concepts/mrn). Ill add this to the docs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should add: As you will see in the note here: https://manetu.github.io/policyengine/concepts/mrn#mrn-format

The MRN format is optional. All we really care about is unique strings. We do encourage good organization, and the MRN scheme is an example of how to accomplish that. Perhaps what I can do is come up with a generic way to talk about the organizational properties without using "mrn" in the cited examples

| MCP feature | `context.mcp.feature` | The MCP feature type - requires `include_operation: true` |
| MCP operation type | `context.mcp.operation` | The MCP operation - requires `include_operation: true` |
| MCP resource ID | `context.mcp.resource_id` | The resource identifier - requires `include_operation: true` |
| Tool arguments | `context.mcp.args` | Tool/prompt arguments - requires `include_args: true` |

### Example PORC expression

When a client calls the `weather` tool with `location: "New York"`, and both
`include_operation` and `include_args` are enabled, the resulting PORC expression
looks like:

```json
{
"principal": {
"sub": "[email protected]",
"mroles": ["developer"],
"mgroups": ["engineering"],
"scopes": ["read", "write"]
},
"operation": "mcp:tool:call",
"resource": "mrn:mcp:myserver:tool:weather",
"context": {
"mcp": {
"feature": "tool",
"operation": "call",
"resource_id": "weather",
"args": { "location": "New York" }
}
}
}
```

If no context options are enabled (the default), the `context` object will be empty.

### PDP API contract

The HTTP PDP authorizer expects the PDP server to implement the following endpoint:

**POST /decision**

Request body: A JSON PORC object (see example above)

Response body:
```json
{
"allow": true
}
```

The `allow` field should be `true` to permit the request, or `false` to deny it.

### Compatible PDP servers

The HTTP PDP authorizer is compatible with any PDP server that implements the
PORC-based decision endpoint, including:

- [Manetu PolicyEngine (MPE)](https://manetu.github.io/policyengine) - A policy
engine built on OPA with multi-phase evaluation
- Custom PDP implementations that follow the API contract above

---

## Implementing a custom authorizer

The authorization framework is designed to be extensible. You can implement your
Expand Down
17 changes: 17 additions & 0 deletions examples/authz-httpv1-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# HTTP PDP Authorization Configuration
#
# This example shows how to configure ToolHive to use an HTTP-based
# Policy Decision Point (PDP) for authorization. This is compatible
# with any PDP that implements the PORC-based decision endpoint.
#
# Start your PDP server (e.g., on port 9000), then start ToolHive with:
# thv run --authz-config authz-httpv1-config.yaml ...
#
version: "1.0"
type: httpv1
pdp:
mode: http
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example configuration shows mode: http field at line 13, but this field does not appear in the ConfigOptions struct definition in config.go. Either the field should be added to the struct and its purpose documented, or it should be removed from the example if it's not used.

Suggested change
mode: http

Copilot uses AI. Check for mistakes.
http:
url: "http://localhost:9000"
timeout: 30 # Request timeout in seconds (default: 30)
insecure_skip_verify: false # Skip TLS certificate verification (default: false)
2 changes: 2 additions & 0 deletions pkg/authz/authorizers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ package authz
import (
// Import Cedar authorizer to register it
_ "github.com/stacklok/toolhive/pkg/authz/authorizers/cedar"
// Import HTTP PDP authorizer to register it
_ "github.com/stacklok/toolhive/pkg/authz/authorizers/http"
)
88 changes: 88 additions & 0 deletions pkg/authz/authorizers/http/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Package http provides authorization using HTTP-based Policy Decision Points (PDPs).
package http

import (
"encoding/json"
"fmt"
)

// ConfigType is the configuration type identifier for HTTP-based PDP authorization.
const ConfigType = "httpv1"

// Config represents the complete authorization configuration file structure
// for HTTP-based PDP authorization. This includes the common version/type fields
// plus the PDP-specific "pdp" field.
type Config struct {
Version string `json:"version"`
Type string `json:"type"`
Options *ConfigOptions `json:"pdp"`
}

// ConfigOptions represents the HTTP PDP authorization configuration options.
type ConfigOptions struct {
// HTTP contains the HTTP connection configuration.
HTTP *ConnectionConfig `json:"http,omitempty" yaml:"http,omitempty"`

// Context configures what context information is included in the PORC.
// By default, no MCP context is included in the PORC.
Context *ContextConfig `json:"context,omitempty" yaml:"context,omitempty"`
}

// ContextConfig configures what context information is included in the PORC.
// All options default to false, meaning no MCP context is included by default.
type ContextConfig struct {
// IncludeArgs enables inclusion of tool/prompt arguments in context.mcp.args.
// Default is false.
IncludeArgs bool `json:"include_args,omitempty" yaml:"include_args,omitempty"`

// IncludeOperation enables inclusion of MCP operation metadata in context.mcp:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what will happen if we add some context.mcp, but we do not include operation? will it break? is that covered?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

operation is a "mandatory phase", so if its missing, the access control decision will be a DENY and the audit-trail will reflect that at least part of the reason for the DENY was the missing operation. There's some more info available here:

https://manetu.github.io/policyengine/concepts/policy-conjunction#phase-requirements

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be documented as well?

// feature, operation, and resource_id fields.
// Default is false.
IncludeOperation bool `json:"include_operation,omitempty" yaml:"include_operation,omitempty"`
}

// ConnectionConfig contains configuration for the HTTP connection to the PDP.
type ConnectionConfig struct {
// URL is the base URL of the PDP server (e.g., "http://localhost:9000").
URL string `json:"url" yaml:"url"`

// Timeout is the HTTP request timeout in seconds. Default is 30.
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"`

// InsecureSkipVerify skips TLS certificate verification. Use only for testing.
InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty" yaml:"insecure_skip_verify,omitempty"`
}

// parseConfig parses the raw JSON configuration into a Config struct.
func parseConfig(rawConfig json.RawMessage) (*Config, error) {
var config Config
if err := json.Unmarshal(rawConfig, &config); err != nil {
return nil, fmt.Errorf("failed to parse HTTP PDP configuration: %w", err)
}
return &config, nil
}

// Validate validates the HTTP PDP configuration options.
func (c *ConfigOptions) Validate() error {
if c == nil {
return fmt.Errorf("pdp configuration is required (missing 'pdp' field)")
}

// Validate HTTP configuration
if c.HTTP == nil {
return fmt.Errorf("http configuration is required")
}
if c.HTTP.URL == "" {
return fmt.Errorf("http.url is required")
}

return nil
}

// GetContextConfig returns the context configuration, or a default empty config if nil.
func (c *ConfigOptions) GetContextConfig() ContextConfig {
if c.Context == nil {
return ContextConfig{}
}
return *c.Context
}
Loading
Loading