From b84e9b2315169ae192e889de51d8e08e2b3dd7ba Mon Sep 17 00:00:00 2001 From: popagruia Date: Fri, 5 Dec 2025 11:57:17 +0200 Subject: [PATCH 1/3] Fix tag handling from array of strings to array of dicts Signed-off-by: popagruia --- plugins/vault/README.md | 240 +++++++++++++++++++--------------- plugins/vault/vault_plugin.py | 104 +++++++++++++-- 2 files changed, 229 insertions(+), 115 deletions(-) diff --git a/plugins/vault/README.md b/plugins/vault/README.md index 3a4cec414..6ff6f296e 100644 --- a/plugins/vault/README.md +++ b/plugins/vault/README.md @@ -1,144 +1,172 @@ # Vault Plugin -The Vault Plugin provides secure storage and retrieval of secrets, ensuring that sensitive information is protected and managed effectively. It generates bearer tokens based on vault-saved tokens and integrates with gateway metadata and OAuth2 configurations. - -## Quick Start +The Vault plugin generates bearer tokens from vault-saved tokens based on OAuth2 configuration protecting a tool. +It receives a dictionary of secrets and use them to dispatch the authorization token to the server based on rules. ## Features -Replace the Bearer token in a tool invocation based on a header that is send to the agent. Header is matched based on the MCP server metadata - - tag that start with system_tag_prefix - - extract the host of the authentication server from - +- **Tag-based metadata handling**: Supports dict format `{"id": "...", "label": "..."}` + - Supported tags must be created on an MCP server to drive the secret handling: + - system: where system host is the IDP provider for that MCP Server. For example system:github.com or system:mural.com + - AUTH_HEADER:
where header name is the authorization header to be used for this MCP header if a PAT token is send +- **Complex token key format**: Supports secrets send via a header containing a JSON dictionary with keys like `github.com:USER:OAUTH2:TOKEN` or simple `github.com` +- **PAT (Personal Access Token) support**: Use `AUTH_HEADER` tag to specify a custom header to be dispatched to the backend server. +- **OAuth2 token support**: Default bearer token handling for OAuth2 tokens. If no specific rule for PAT the default behavior is to send the secret as Bearer token in Authorization header +- **Flexible configuration**: Falls back to default bearer token behavior when parts are missing -## Installation +## Configuration -1. Copy .env.example .env -2. Enable plugins in `.env` -3. Add the plugin configuration to `plugins/config.yaml`: +### Basic Configuration ```yaml - - name: "VaultPlugin" - kind: "plugins.vault.vault_plugin.Vault" - description: "Vault plugin that based that will generate bearer token based on a vault saved token" - version: "0.0.1" - author: "Adrian Popa" - hooks: ["tool_pre_invoke"] - tags: ["security", "vault", "OAUTH2"] - mode: "permissive" # enforce | permissive | disabled - priority: 10 # Lower number = higher priority (runs first) - conditions: - - prompts: [] # Empty list = apply to all prompts - server_ids: [] # Apply to all servers - tenant_ids: [] # Apply to all tenants - config: - system_tag_prefix: "system" - vault_header_name: "X-Vault-Tokens" - vault_handling: "raw" +vault: + enabled: true + config: + system_tag_prefix: "system" + vault_header_name: "X-Vault-Tokens" + vault_handling: "raw" + system_handling: "tag" + auth_header_tag_prefix: "AUTH_HEADER" +``` + +### Configuration Options + +- **system_tag_prefix**: Prefix for system identification tags (default: `"system"`) +- **vault_header_name**: HTTP header name for vault tokens (default: `"X-Vault-Tokens"`) +- **vault_handling**: Token handling mode (default: `"raw"`). Future version will handle token unwrapping +- **system_handling**: System identification mode (default: `"tag"`) +- **auth_header_tag_prefix**: Prefix for auth header tags (default: `"AUTH_HEADER"`) + +## Token Key Format +The plugin supports complex token keys in the format: + +``` +system[:scope][:token_type][:token_name] ``` -## Configuration Examples +Where: +- **system** (required): The system identifier (e.g., `github.com`, `gitlab.com`) +- **scope** (optional): USER or GROUP (ignored in processing) +- **token_type** (optional): PAT or OAUTH2 +- **token_name** (optional): Name of the token -### Development Environment (Permissive) -```yaml -config: - system_tag_prefix: "system" ### The prefix of the tag that contains the system name - system_handling: "tag" # # Gets the OAUTH2 IDP host from tags. The tag must have the format "system:host" where host is the hostname of the IDP. Use oauth2_config to extract IDP hostname from the OAUTH_CONFIG metadata of the MCP Server template. - vault_header_name: "X-Vault-Tokens" # Name of the header that contains the tokens. - vault_handling: "raw" # Use the token that matches the system as bearer token +### Examples +1. **Simple key**: `github.com` + - Uses default OAuth2 bearer token handling -### Features -- Secure storage of secrets -- Retrieval of secrets with access control -- Integration with gateway metadata and OAuth2 configurations +2. **Full PAT key**: `github.com:USER:PAT:my-token` + - System: `github.com` + - Scope: `USER` (ignored) + - Token type: `PAT` + - Token name: `my-token` + - Checks for `AUTH_HEADER` tag to determine header name -## Available Hooks +3. **OAuth2 key**: `gitlab.com:GROUP:OAUTH2:app-token` + - System: `gitlab.com` + - Scope: `GROUP` (ignored) + - Token type: `OAUTH2` + - Token name: `TOKEN` (default name) + - Uses OAuth2 bearer token handling -The Vault Plugin implement hooks at these lifecycle points: +## Token Type Handling -| Hook | Description | Payload Type | Use Cases | -|------|-------------|--------------|-----------| -| `tool_pre_invoke` | Before tool invocation | `ToolPreInvokePayload` | Access control for OAUTH2 server | +### PAT (Personal Access Token) +When `token_type` is `PAT`: +1. Checks if AUTH_HEADER:header tag exists +2. If found, uses the configured custom header +3. If not found, falls back to `Authorization: Bearer ` +### OAUTH2 -### Plugin Modes +When `token_type` is `OAUTH2` or missing: +- Uses standard `Authorization: Bearer ` header +### Unknown Types -- **`permissive`**: Change headers if possible but allows request to continue +For any other token type: +- Logs a warning +- Falls back to `Authorization: Bearer ` -### Plugin Priority +## Gateway Metadata Tags + +The plugin handles tags in dict format: + +```json +{ + "tags": [ + {"id": "auto-generated-id", "label": "system:github.com"}, + {"id": "another-id", "label": "AUTH_HEADER:X-GitHub-Token"}, + {"id": "third-id", "label": "environment:production"} + ] +} +``` -Lower priority numbers run first (higher priority). Recommended ranges: -- **1-50**: Critical security plugins (access control) -- **51-100**: Content filtering and validation -- **101-200**: Transformations and enhancements -- **201+**: Logging and monitoring +The plugin extracts the `label` field from dict tags (the actual tag value), while `id` is autogenerated. +### Tag Types -### Logging and Monitoring +1. **System Tag**: `system:` + - Identifies which system the token is for + - Example: `system:github.com` + - Required for the plugin to work -```python -def __init__(self, config: PluginConfig): - super().__init__(config) - self.logger.info(f"Initialized {self.name} v{self.version}") +2. **Auth Header Tag**: `AUTH_HEADER:` + - Specifies custom header for PAT tokens + - Example: `AUTH_HEADER:X-GitHub-Token` + - Only used when token type is PAT + - Optional - falls back to Bearer token if not present -async def vault_pre_fetch(self, payload: VaultPreFetchPayload) -> VaultPreFetchPayload: - self.logger.debug(f"Processing vault: {payload.secret_name}") - # ... plugin logic - self.metrics.increment("requests_processed") +## Example Usage + +### Request with Vault Tokens + +```http +POST /api/tools/invoke +X-Vault-Tokens: {"github.com:USER:PAT:my-token": "ghp_xxxxxxxxxxxx", "gitlab.com": "glpat-yyyyyyyy"} ``` +### Gateway with AUTH_HEADER Tag -## Testing -### Create an MCP gateway that is OAUTH2 authenticated: - -```bash -curl -s \ - -X POST \ - -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "github.com", - "url": "https://api.githubcopilot.com/mcp/", - "description": "A new MCP server added with OAuth2 authentication", - "auth_type": "oauth", - "auth_value": { - "client_id": "'$CLIENT_ID'", - "client_secret": "'$CLIENT_SECRET'", - "token_url": "https://github.com/login/oauth/access_token", - "redirect_url": "http://localhost:4444/oauth/callback" - }, - "tags": ["system:github.com"], - "passthrough_headers": ["X-Vault-Tokens"] - }' \ - http://localhost:4444/gateways +If gateway has tags including `AUTH_HEADER:X-GitHub-Token`: +```json +{ + "tags": [ + {"id": "1", "label": "system:github.com"}, + {"id": "2", "label": "AUTH_HEADER:X-GitHub-Token"} + ] +} ``` -The bearer token can be created by running: -```bash - export MCPGATEWAY_BEARER_TOKEN=$(python3 -m mcpgateway.utils.create_jwt_token \ --username admin@example.com --exp 10080 --secret my-test-key) +The plugin will set: +```http +X-GitHub-Token: ghp_xxxxxxxxxxxx ``` -### Invoke a tool sending the tokens in a dictionary -```bash -curl --request POST \ - --url http://localhost:4444/rpc \ - --header "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - --header "Content-Type: application/json" \ - --header "x-vault-tokens: {\"github.com\": \"123\"}" \ - --data '{ - "jsonrpc": "2.0", - "id": 1, - "method": "tools/call", - "params": { - "name": "git.colasdn.top-get-me", - "arguments": { - "timezone": "Asia/Kolkata" - } - } - }' +### Without AUTH_HEADER Tag + +If no `AUTH_HEADER` tag is defined, the plugin will use default Bearer token: +```http +Authorization: Bearer ghp_xxxxxxxxxxxx ``` + +## System Identification + +The plugin supports two modes for identifying the system: + +### TAG Mode (Default) + +Extracts system from gateway tags with the configured prefix: +- Tag: `system:github.com` → System: `github.com` + +### OAUTH2_CONFIG Mode + +Extracts system from the OAuth2 configuration's `token_url`: +- Token URL: `https://github.com/login/oauth/access_token` → System: `github.com` + +## Hook + +- **tool_pre_invoke**: Processes vault tokens before tool invocation diff --git a/plugins/vault/vault_plugin.py b/plugins/vault/vault_plugin.py index 115168419..9c138a504 100644 --- a/plugins/vault/vault_plugin.py +++ b/plugins/vault/vault_plugin.py @@ -67,12 +67,14 @@ class VaultConfig(BaseModel): vault_header_name: HTTP header name for vault tokens. vault_handling: Vault token handling mode. system_handling: System identification mode. + auth_header_tag_prefix: Prefix for auth header tags (e.g., "AUTH_HEADER"). """ system_tag_prefix: str = "system" vault_header_name: str = "X-Vault-Tokens" vault_handling: VaultHandling = VaultHandling.RAW system_handling: SystemHandling = SystemHandling.TAG + auth_header_tag_prefix: str = "AUTH_HEADER" class Vault(Plugin): @@ -91,6 +93,22 @@ def __init__(self, config: PluginConfig): except Exception: self._sconfig = VaultConfig() + def _parse_vault_token_key(self, key: str) -> tuple[str, str | None, str | None, str | None]: + """Parse vault token key in format: system[:scope][:token_type][:token_name]. + + Args: + key: Token key to parse (e.g., "github.com:USER:OAUTH2:TOKEN" or "github.com"). + + Returns: + Tuple of (system, scope, token_type, token_name). Missing parts are None. + """ + parts = key.split(":") + system = parts[0] if len(parts) > 0 else key + scope = parts[1] if len(parts) > 1 else None + token_type = parts[2] if len(parts) > 2 else None + token_name = parts[3] if len(parts) > 3 else None + return system, scope, token_type, token_name + async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: """Generate bearer tokens from vault-saved tokens before tool invocation. @@ -107,12 +125,33 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo gateway_metadata = context.global_context.metadata["gateway"] system_key: str | None = None + auth_header: str | None = None + if self._sconfig.system_handling == SystemHandling.TAG: - system_tag = next((tag for tag in gateway_metadata.tags if tag.startswith(self._sconfig.system_tag_prefix)), None) - match_exp = self._sconfig.system_tag_prefix + ":" - if system_tag and system_tag.startswith(match_exp): - system_key = system_tag.split(match_exp)[1] + # Extract tags from dict format {"id": "...", "label": "..."} + normalized_tags: list[str] = [] + for tag in gateway_metadata.tags: + if isinstance(tag, dict): + # Use 'label' field (the actual tag value) + tag_value = str(tag.get("label", "")) + if tag_value: + normalized_tags.append(tag_value) + elif hasattr(tag, "label"): + normalized_tags.append(str(getattr(tag, "label"))) + + # Find system tag with the configured prefix + system_prefix = self._sconfig.system_tag_prefix + ":" + system_tag = next((tag for tag in normalized_tags if tag.startswith(system_prefix)), None) + if system_tag: + system_key = system_tag.split(system_prefix)[1] logger.info(f"Using vault system from GW tags: {system_key}") + + # Find auth header tag with the configured prefix (e.g., "AUTH_HEADER:X-GitHub-Token") + auth_header_prefix = self._sconfig.auth_header_tag_prefix + ":" + auth_header_tag = next((tag for tag in normalized_tags if tag.startswith(auth_header_prefix)), None) + if auth_header_tag: + auth_header = auth_header_tag.split(auth_header_prefix)[1] + logger.info(f"Found AUTH_HEADER tag: {auth_header}") elif self._sconfig.system_handling == SystemHandling.OAUTH2_CONFIG: gen = get_db() @@ -151,13 +190,60 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo vault_handling = self._sconfig.vault_handling + # Try to find matching token in vault_tokens + # First try exact match with system_key + token_value: str | None = None + token_key_used: str | None = None + if system_key in vault_tokens: - if vault_handling == VaultHandling.RAW: - logger.info(f"Set Bearer token for system tag: {system_key}") - bearer_token: str = str(vault_tokens[system_key]) - headers["Authorization"] = f"Bearer {bearer_token}" + token_value = str(vault_tokens[system_key]) + token_key_used = str(system_key) + logger.info(f"Found exact match for system key: {system_key}") + else: + # Try to find a key that starts with system_key (complex key format) + for key in vault_tokens.keys(): + parsed_system, scope, token_type, token_name = self._parse_vault_token_key(key) + if parsed_system == system_key: + token_value = vault_tokens[key] + token_key_used = key + logger.info(f"Found matching token with complex key: {key} (system: {parsed_system}, scope: {scope}, type: {token_type}, name: {token_name})") + break + + if token_value and token_key_used: + # Parse the token key to determine handling + parsed_system, scope, token_type, token_name = self._parse_vault_token_key(token_key_used) + + # Determine how to handle the token based on token_type and AUTH_HEADER tag + if token_type == "PAT": + # Handle Personal Access Token + logger.info(f"Processing PAT token for system: {parsed_system}") + + # Check if AUTH_HEADER tag is defined + if auth_header: + logger.info(f"Using AUTH_HEADER tag for {parsed_system}: header={auth_header}") + headers[auth_header] = str(token_value) + modified = True + else: + # No AUTH_HEADER tag, use default Bearer token + logger.info(f"No AUTH_HEADER tag found for {parsed_system}, using Bearer token") + headers["Authorization"] = f"Bearer {token_value}" + modified = True + elif token_type == "OAUTH2" or token_type is None: + # Handle OAuth2 token or default behavior (when token_type is missing) + if vault_handling == VaultHandling.RAW: + logger.info(f"Set Bearer token for system: {parsed_system}") + headers["Authorization"] = f"Bearer {token_value}" + modified = True + else: + # Unknown token type, use default behavior + logger.warning(f"Unknown token type '{token_type}', using default Bearer token") + if vault_handling == VaultHandling.RAW: + headers["Authorization"] = f"Bearer {token_value}" + modified = True + + # Remove vault header after processing + if modified and self._sconfig.vault_header_name in headers: del headers[self._sconfig.vault_header_name] - modified = True payload.headers = HttpHeaderPayload(root=headers) From 8458bf509885924631583424c54a7c8453812348 Mon Sep 17 00:00:00 2001 From: popagruia Date: Fri, 5 Dec 2025 13:13:13 +0200 Subject: [PATCH 2/3] Fix flake 8 issues Signed-off-by: popagruia --- plugins/vault/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/vault/README.md b/plugins/vault/README.md index 6ff6f296e..4c62c2d21 100644 --- a/plugins/vault/README.md +++ b/plugins/vault/README.md @@ -1,6 +1,6 @@ # Vault Plugin -The Vault plugin generates bearer tokens from vault-saved tokens based on OAuth2 configuration protecting a tool. +The Vault plugin generates bearer tokens from vault-saved tokens based on OAuth2 configuration protecting a tool. It receives a dictionary of secrets and use them to dispatch the authorization token to the server based on rules. ## Features @@ -76,7 +76,7 @@ Where: ### PAT (Personal Access Token) When `token_type` is `PAT`: -1. Checks if AUTH_HEADER:header tag exists +1. Checks if AUTH_HEADER:header tag exists 2. If found, uses the configured custom header 3. If not found, falls back to `Authorization: Bearer ` From 060e875cd5e694eb8e85d1b91756c9ed6bdaf435 Mon Sep 17 00:00:00 2001 From: popagruia Date: Fri, 5 Dec 2025 13:20:49 +0200 Subject: [PATCH 3/3] Fix flake 8 issues Signed-off-by: popagruia --- plugins/vault/vault_plugin.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/vault/vault_plugin.py b/plugins/vault/vault_plugin.py index 9c138a504..99e962432 100644 --- a/plugins/vault/vault_plugin.py +++ b/plugins/vault/vault_plugin.py @@ -126,7 +126,6 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo system_key: str | None = None auth_header: str | None = None - if self._sconfig.system_handling == SystemHandling.TAG: # Extract tags from dict format {"id": "...", "label": "..."} normalized_tags: list[str] = [] @@ -138,14 +137,12 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo normalized_tags.append(tag_value) elif hasattr(tag, "label"): normalized_tags.append(str(getattr(tag, "label"))) - # Find system tag with the configured prefix system_prefix = self._sconfig.system_tag_prefix + ":" system_tag = next((tag for tag in normalized_tags if tag.startswith(system_prefix)), None) if system_tag: system_key = system_tag.split(system_prefix)[1] logger.info(f"Using vault system from GW tags: {system_key}") - # Find auth header tag with the configured prefix (e.g., "AUTH_HEADER:X-GitHub-Token") auth_header_prefix = self._sconfig.auth_header_tag_prefix + ":" auth_header_tag = next((tag for tag in normalized_tags if tag.startswith(auth_header_prefix)), None) @@ -194,7 +191,6 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo # First try exact match with system_key token_value: str | None = None token_key_used: str | None = None - if system_key in vault_tokens: token_value = str(vault_tokens[system_key]) token_key_used = str(system_key) @@ -212,12 +208,10 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo if token_value and token_key_used: # Parse the token key to determine handling parsed_system, scope, token_type, token_name = self._parse_vault_token_key(token_key_used) - # Determine how to handle the token based on token_type and AUTH_HEADER tag if token_type == "PAT": # Handle Personal Access Token logger.info(f"Processing PAT token for system: {parsed_system}") - # Check if AUTH_HEADER tag is defined if auth_header: logger.info(f"Using AUTH_HEADER tag for {parsed_system}: header={auth_header}")