-
Notifications
You must be signed in to change notification settings - Fork 171
Add HTTP-based PDP authorizer #3315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
|
|
||||||
|
|
@@ -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: | ||||||
|
|
||||||
| - `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) | ||||||
|
||||||
|
|
||||||
| ### 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 | | ||||||
| |-------------|------------|--------| | ||||||
|
||||||
| |-------------|------------|--------| | |
| |-----------|----------|--------| |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
| 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 | ||||
|
||||
| mode: http |
| 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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
There was a problem hiding this comment.
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: httpfield in the pdp section. This field should either be documented here or removed from the examples.