mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(terraform): add sensitive auth_value credential field for litellm_mcp_server
Accepting bearer_token/api_key in auth_type validation is not enough on its own: the provider had no way to send the credential, so those values could never work end to end. Add an optional, Sensitive auth_value field that is sent to the LiteLLM API as credentials.auth_value (matching NewMCPServerRequest), and omit the credentials blob entirely when unset. The field is never written back from API responses since the API redacts credentials on read. Signed-off-by: SIDDARTHA REDDY <75976672+SIDDARTHAREDDY8@users.noreply.github.com>
This commit is contained in:
parent
f3cf1cdfef
commit
4178ae8524
5 changed files with 188 additions and 13 deletions
|
|
@ -21,7 +21,8 @@ resource "litellm_mcp_server" "github_server" {
|
|||
description = "GitHub MCP server for repository operations"
|
||||
url = "https://api.github.com/mcp"
|
||||
transport = "http"
|
||||
auth_type = "bearer"
|
||||
auth_type = "bearer_token"
|
||||
auth_value = var.github_token # sensitive variable; never hardcode credentials
|
||||
|
||||
mcp_access_groups = ["dev_team", "devops_team"]
|
||||
}
|
||||
|
|
@ -36,7 +37,7 @@ resource "litellm_mcp_server" "zapier_server" {
|
|||
description = "Zapier MCP server for workflow automation"
|
||||
url = "https://actions.zapier.com/mcp/sk-xxxxx/sse"
|
||||
transport = "sse"
|
||||
auth_type = "bearer"
|
||||
auth_type = "bearer_token"
|
||||
spec_version = "2024-11-05"
|
||||
|
||||
mcp_access_groups = ["automation_team", "marketing_team"]
|
||||
|
|
@ -104,7 +105,7 @@ resource "litellm_mcp_server" "enterprise_api_server" {
|
|||
description = "Enterprise API gateway MCP server"
|
||||
url = "https://api.enterprise.com/mcp/v1"
|
||||
transport = "http"
|
||||
auth_type = "bearer"
|
||||
auth_type = "bearer_token"
|
||||
spec_version = "2024-11-05"
|
||||
|
||||
mcp_access_groups = [
|
||||
|
|
@ -149,7 +150,8 @@ The following arguments are supported:
|
|||
* `alias` - (Optional) Alias for the MCP server. Used for easier reference.
|
||||
* `description` - (Optional) Description of the MCP server.
|
||||
* `spec_version` - (Optional) MCP specification version. Defaults to `2024-11-05`.
|
||||
* `auth_type` - (Optional) Authentication type. Valid values: `none`, `bearer`, `basic`. Defaults to `none`.
|
||||
* `auth_type` - (Optional) Authentication type. Must be a LiteLLM MCPAuth value: `none`, `api_key`, `bearer_token`, `basic`, `authorization`, `oauth2`, `aws_sigv4`, `token`, `oauth2_token_exchange`, `oauth2_id_jag`, `true_passthrough`, `oauth_delegate`. Defaults to `none`. Use `bearer_token` for bearer auth.
|
||||
* `auth_value` - (Optional, Sensitive) Authentication credential sent to the LiteLLM API as `credentials.auth_value` (e.g. the bearer token for `auth_type = "bearer_token"`, the API key for `auth_type = "api_key"`). Hidden from plan output. Only needed when the chosen `auth_type` requires a credential.
|
||||
* `mcp_access_groups` - (Optional) List of access groups that can use this MCP server.
|
||||
* `command` - (Optional) Command to run for stdio transport.
|
||||
* `args` - (Optional) List of arguments for the command (stdio transport only). Do not pass secrets as arguments; args are shown in plans, stored unencrypted in state, and visible in the server's process list.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,24 @@ import (
|
|||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
|
||||
)
|
||||
|
||||
// mcpServerAuthTypes mirrors the LiteLLM API's MCPAuth enum (litellm/types/mcp.py).
|
||||
// Values are sent to the API verbatim, so the provider must only accept the
|
||||
// API's canonical values. Note the API expects "bearer_token", not "bearer".
|
||||
var mcpServerAuthTypes = []string{
|
||||
"none",
|
||||
"api_key",
|
||||
"bearer_token",
|
||||
"basic",
|
||||
"authorization",
|
||||
"oauth2",
|
||||
"aws_sigv4",
|
||||
"token",
|
||||
"oauth2_token_exchange",
|
||||
"oauth2_id_jag",
|
||||
"true_passthrough",
|
||||
"oauth_delegate",
|
||||
}
|
||||
|
||||
func resourceLiteLLMMCPServer() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Create: resourceLiteLLMMCPServerCreate,
|
||||
|
|
@ -53,15 +71,17 @@ func resourceLiteLLMMCPServer() *schema.Resource {
|
|||
Description: "MCP specification version",
|
||||
},
|
||||
"auth_type": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Default: "none",
|
||||
ValidateFunc: validation.StringInSlice([]string{
|
||||
"none",
|
||||
"bearer",
|
||||
"basic",
|
||||
}, false),
|
||||
Description: "Authentication type (none, bearer, basic)",
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Default: "none",
|
||||
ValidateFunc: validation.StringInSlice(mcpServerAuthTypes, false),
|
||||
Description: "Authentication type. Must be a LiteLLM MCPAuth value (none, api_key, bearer_token, basic, authorization, oauth2, aws_sigv4, token, oauth2_token_exchange, oauth2_id_jag, true_passthrough, oauth_delegate). Use bearer_token for bearer auth. Defaults to none",
|
||||
},
|
||||
"auth_value": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
Description: "Authentication credential sent to the LiteLLM API as credentials.auth_value (e.g. the bearer token for auth_type \"bearer_token\", the API key for auth_type \"api_key\"). Sensitive: hidden from plan output. Only needed when the chosen auth_type requires a credential; ignored when unset.",
|
||||
},
|
||||
"mcp_access_groups": {
|
||||
Type: schema.TypeList,
|
||||
|
|
|
|||
138
terraform/provider/litellm/resource_mcp_server_auth_type_test.go
Normal file
138
terraform/provider/litellm/resource_mcp_server_auth_type_test.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package litellm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
)
|
||||
|
||||
// Regression test for https://github.com/BerriAI/litellm/issues/43019.
|
||||
// The provider used to validate auth_type against ["none", "bearer", "basic"],
|
||||
// but the LiteLLM API's MCPAuth enum (litellm/types/mcp.py) expects
|
||||
// "bearer_token" among its canonical values. auth_type="bearer" passed
|
||||
// provider validation and then failed with a 422 from the API, while
|
||||
// auth_type="bearer_token" failed provider plan validation. The provider now
|
||||
// accepts the API's canonical MCPAuth values verbatim.
|
||||
func TestMCPServerAuthTypeValidationAcceptsCanonicalMCPAuthValues(t *testing.T) {
|
||||
vf := resourceLiteLLMMCPServer().Schema["auth_type"].ValidateFunc
|
||||
if vf == nil {
|
||||
t.Fatal("auth_type has no ValidateFunc")
|
||||
}
|
||||
|
||||
valid := []string{
|
||||
"none",
|
||||
"api_key",
|
||||
"bearer_token",
|
||||
"basic",
|
||||
"authorization",
|
||||
"oauth2",
|
||||
"aws_sigv4",
|
||||
"token",
|
||||
"oauth2_token_exchange",
|
||||
"oauth2_id_jag",
|
||||
"true_passthrough",
|
||||
"oauth_delegate",
|
||||
}
|
||||
for _, v := range valid {
|
||||
if _, errs := vf(v, "auth_type"); len(errs) > 0 {
|
||||
t.Errorf("auth_type %q rejected, want accepted: %v", v, errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMCPServerAuthTypeValidationRejectsNonCanonicalValues(t *testing.T) {
|
||||
vf := resourceLiteLLMMCPServer().Schema["auth_type"].ValidateFunc
|
||||
if vf == nil {
|
||||
t.Fatal("auth_type has no ValidateFunc")
|
||||
}
|
||||
|
||||
// "bearer" is not a LiteLLM MCPAuth value; the API returns 422 for it, so
|
||||
// it must fail provider-side validation with a clear error instead of
|
||||
// reaching the API.
|
||||
invalid := []string{"bearer", "Bearer_Token", "BEARER", "jwt", "", " bearer_token"}
|
||||
for _, v := range invalid {
|
||||
if _, errs := vf(v, "auth_type"); len(errs) == 0 {
|
||||
t.Errorf("auth_type %q accepted, want rejected", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The default must remain a valid value so existing configs without an
|
||||
// explicit auth_type still plan cleanly.
|
||||
func TestMCPServerAuthTypeDefaultIsValid(t *testing.T) {
|
||||
r := resourceLiteLLMMCPServer()
|
||||
def := r.Schema["auth_type"].Default.(string)
|
||||
if def != "none" {
|
||||
t.Fatalf("auth_type default = %q, want %q", def, "none")
|
||||
}
|
||||
if _, errs := r.Schema["auth_type"].ValidateFunc(def, "auth_type"); len(errs) > 0 {
|
||||
t.Errorf("auth_type default %q rejected: %v", def, errs)
|
||||
}
|
||||
}
|
||||
|
||||
// auth_value carries the credential for auth types that need one (e.g.
|
||||
// bearer_token, api_key). It must be optional and sensitive: hidden from plan
|
||||
// output and never written back from API responses (the API redacts
|
||||
// credentials on read, same as the sensitive "env" field).
|
||||
func TestMCPServerAuthValueIsOptionalAndSensitive(t *testing.T) {
|
||||
f, ok := resourceLiteLLMMCPServer().Schema["auth_value"]
|
||||
if !ok {
|
||||
t.Fatal("schema has no auth_value field")
|
||||
}
|
||||
if !f.Optional {
|
||||
t.Error("auth_value should be Optional")
|
||||
}
|
||||
if !f.Sensitive {
|
||||
t.Error("auth_value must be Sensitive so the credential never appears in plan output")
|
||||
}
|
||||
}
|
||||
|
||||
// The configured credential must reach the API as credentials.auth_value,
|
||||
// matching the LiteLLM API's NewMCPServerRequest shape.
|
||||
func TestMCPServerBuildRequestSendsAuthValueAsCredentials(t *testing.T) {
|
||||
d := schema.TestResourceDataRaw(t, resourceLiteLLMMCPServer().Schema, map[string]interface{}{
|
||||
"server_name": "s",
|
||||
"url": "https://example.com/mcp",
|
||||
"transport": "http",
|
||||
"auth_type": "bearer_token",
|
||||
"auth_value": "secret-token",
|
||||
})
|
||||
req := buildMCPServerRequest(d)
|
||||
if req.Credentials == nil {
|
||||
t.Fatal("expected Credentials to be set when auth_value is configured")
|
||||
}
|
||||
if req.Credentials.AuthValue != "secret-token" {
|
||||
t.Errorf("Credentials.AuthValue = %q, want %q", req.Credentials.AuthValue, "secret-token")
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal request: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(body), `"credentials":{"auth_value":"secret-token"}`) {
|
||||
t.Errorf("request JSON missing credentials.auth_value, got: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// No credentials blob may be sent when auth_value is unset, so existing
|
||||
// configs and auth_type="none" behave exactly as before.
|
||||
func TestMCPServerBuildRequestOmitsCredentialsWhenAuthValueUnset(t *testing.T) {
|
||||
d := schema.TestResourceDataRaw(t, resourceLiteLLMMCPServer().Schema, map[string]interface{}{
|
||||
"server_name": "s",
|
||||
"url": "https://example.com/mcp",
|
||||
"transport": "http",
|
||||
"auth_type": "none",
|
||||
})
|
||||
req := buildMCPServerRequest(d)
|
||||
if req.Credentials != nil {
|
||||
t.Errorf("expected no Credentials when auth_value is unset, got %+v", req.Credentials)
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal request: %v", err)
|
||||
}
|
||||
if strings.Contains(string(body), "credentials") {
|
||||
t.Errorf("request JSON must not contain credentials when auth_value is unset, got: %s", body)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,13 @@ func buildMCPServerRequest(d *schema.ResourceData) *MCPServerRequest {
|
|||
AuthType: d.Get("auth_type").(string),
|
||||
}
|
||||
|
||||
// Set the credential when one is configured. The API redacts credentials
|
||||
// on read, so auth_value is never written back from responses (same as
|
||||
// the sensitive "env" field): the configured value stays in state.
|
||||
if authValue, ok := d.GetOk("auth_value"); ok {
|
||||
req.Credentials = &MCPCredentials{AuthValue: authValue.(string)}
|
||||
}
|
||||
|
||||
// Set optional fields
|
||||
if alias, ok := d.GetOk("alias"); ok {
|
||||
req.Alias = alias.(string)
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ type MCPServerRequest struct {
|
|||
Transport string `json:"transport"`
|
||||
SpecVersion string `json:"spec_version,omitempty"`
|
||||
AuthType string `json:"auth_type,omitempty"`
|
||||
Credentials *MCPCredentials `json:"credentials,omitempty"`
|
||||
URL string `json:"url"`
|
||||
MCPInfo *MCPInfo `json:"mcp_info,omitempty"`
|
||||
MCPAccessGroups []string `json:"mcp_access_groups,omitempty"`
|
||||
|
|
@ -201,6 +202,13 @@ type MCPServerRequest struct {
|
|||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
// MCPCredentials mirrors litellm.types.mcp.MCPCredentials: the credential
|
||||
// blob accepted by the LiteLLM API's NewMCPServerRequest. Only the fields the
|
||||
// provider sets are modeled here.
|
||||
type MCPCredentials struct {
|
||||
AuthValue string `json:"auth_value,omitempty"`
|
||||
}
|
||||
|
||||
// MCPServerResponse represents a response from the API containing MCP server information.
|
||||
type MCPServerResponse struct {
|
||||
ServerID string `json:"server_id"`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue