diff --git a/authorize/evaluator/evaluator.go b/authorize/evaluator/evaluator.go index 378b703f3..0b6642ad0 100644 --- a/authorize/evaluator/evaluator.go +++ b/authorize/evaluator/evaluator.go @@ -4,7 +4,6 @@ package evaluator import ( "context" "encoding/base64" - "encoding/json" "encoding/pem" "fmt" "net/http" @@ -135,12 +134,6 @@ type RequestSession struct { ID string `json:"id"` } -// RequestMCP is the MCP field in the request. -type RequestMCP struct { - Tool string `json:"tool,omitempty"` - Method string `json:"method,omitempty"` -} - // Result is the result of evaluation. type Result struct { Allow RuleResult @@ -477,36 +470,3 @@ func carryOverJWTAssertion(dst http.Header, src map[string]string) { dst.Add(jwtForKey, jwtFor) } } - -// RequestMCPFromCheckRequest populates a RequestMCP from an Envoy CheckRequest proto for MCP routes. -func RequestMCPFromCheckRequest( - in *envoy_service_auth_v3.CheckRequest, -) (RequestMCP, bool) { - var mcpReq RequestMCP - - body := in.GetAttributes().GetRequest().GetHttp().GetBody() - if body == "" { - return mcpReq, false - } - - var jsonRPCReq struct { - Method string `json:"method"` - Params map[string]any `json:"params,omitempty"` - } - - if err := json.Unmarshal([]byte(body), &jsonRPCReq); err != nil { - return mcpReq, false - } - - mcpReq.Method = jsonRPCReq.Method - - if jsonRPCReq.Method == "tools/call" { - if name, exists := jsonRPCReq.Params["name"]; exists { - if toolName, ok := name.(string); ok { - mcpReq.Tool = toolName - } - } - } - - return mcpReq, true -} diff --git a/authorize/evaluator/evaluator_test.go b/authorize/evaluator/evaluator_test.go index 9f9e30766..843274811 100644 --- a/authorize/evaluator/evaluator_test.go +++ b/authorize/evaluator/evaluator_test.go @@ -689,8 +689,10 @@ func TestEvaluator(t *testing.T) { URL: "https://from.example.com", }, MCP: RequestMCP{ - Tool: "tool_name", Method: "tools/call", + ToolCall: &RequestMCPToolCall{ + Name: "tool_name", + }, }, }) require.NoError(t, err) diff --git a/authorize/evaluator/mcp.go b/authorize/evaluator/mcp.go new file mode 100644 index 000000000..5f13ccb60 --- /dev/null +++ b/authorize/evaluator/mcp.go @@ -0,0 +1,53 @@ +package evaluator + +import ( + "encoding/json" + + envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" +) + +// RequestMCP is the MCP field in the request. +type RequestMCP struct { + Method string `json:"method,omitempty"` + ToolCall *RequestMCPToolCall `json:"tool_call,omitempty"` +} + +// RequestMCPToolCall represents a tool call within an MCP request. +type RequestMCPToolCall struct { + Name string `json:"name"` + Arguments map[string]any `json:"arguments"` +} + +// RequestMCPFromCheckRequest populates a RequestMCP from an Envoy CheckRequest proto for MCP routes. +func RequestMCPFromCheckRequest( + in *envoy_service_auth_v3.CheckRequest, +) (RequestMCP, bool) { + var mcpReq RequestMCP + + body := in.GetAttributes().GetRequest().GetHttp().GetBody() + if body == "" { + return mcpReq, false + } + + var jsonRPCReq struct { + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` + } + + if err := json.Unmarshal([]byte(body), &jsonRPCReq); err != nil { + return mcpReq, false + } + + mcpReq.Method = jsonRPCReq.Method + + if jsonRPCReq.Method == "tools/call" { + var toolCall RequestMCPToolCall + err := json.Unmarshal(jsonRPCReq.Params, &toolCall) + if err != nil { + return mcpReq, false + } + mcpReq.ToolCall = &toolCall + } + + return mcpReq, true +} diff --git a/authorize/grpc.go b/authorize/grpc.go index 815c65d4c..3143ae5e8 100644 --- a/authorize/grpc.go +++ b/authorize/grpc.go @@ -9,6 +9,8 @@ import ( "strings" envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "go.opentelemetry.io/otel/attribute" + oteltrace "go.opentelemetry.io/otel/trace" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -37,18 +39,24 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v3.CheckRe ctx = a.withQuerierForCheckRequest(ctx) state := a.state.Load() + mcpEnabled := a.currentConfig.Load().Options.IsRuntimeFlagSet(config.RuntimeFlagMCP) // convert the incoming envoy-style http request into a go-style http request hreq := getHTTPRequestFromCheckRequest(in) requestID := requestid.FromHTTPHeader(hreq.Header) ctx = requestid.WithValue(ctx, requestID) - req, err := a.getEvaluatorRequestFromCheckRequest(ctx, in) + req, err := a.getEvaluatorRequestFromCheckRequest(ctx, in, mcpEnabled) if err != nil { log.Ctx(ctx).Error().Err(err).Str("request-id", requestID).Msg("error building evaluator request") return nil, err } + // Add MCP information to trace if available + if mcpEnabled { + updateSpanWithMCPInfo(span, req.MCP) + } + // load the session s, err := a.loadSession(ctx, hreq, req) if errors.Is(err, sessions.ErrInvalidSession) { @@ -68,7 +76,7 @@ func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v3.CheckRe // For MCP routes that only require authentication (not full authorization), // if we have a valid session, allow the request without running policy evaluation // as policy for MCP may contain check for i.e. tool calls that are not relevant at this stage. - if a.currentConfig.Load().Options.IsRuntimeFlagSet(config.RuntimeFlagMCP) { + if mcpEnabled { if req.Policy.IsMCPServer() && strings.HasPrefix(hreq.URL.Path, mcp.DefaultPrefix) { if s != nil { return a.requireLoginResponse(ctx, in, req) @@ -196,6 +204,7 @@ func (a *Authorize) getMCPSession( func (a *Authorize) getEvaluatorRequestFromCheckRequest( ctx context.Context, in *envoy_service_auth_v3.CheckRequest, + mcpEnabled bool, ) (*evaluator.Request, error) { attrs := in.GetAttributes() req := &evaluator.Request{ @@ -206,7 +215,7 @@ func (a *Authorize) getEvaluatorRequestFromCheckRequest( } req.Policy = a.getMatchingPolicy(req.EnvoyRouteID) - if req.Policy.IsMCPServer() { + if mcpEnabled && req.Policy.IsMCPServer() { var ok bool req.MCP, ok = evaluator.RequestMCPFromCheckRequest(in) if !ok { @@ -214,13 +223,6 @@ func (a *Authorize) getEvaluatorRequestFromCheckRequest( Str("request-id", requestid.FromContext(ctx)). Str("route_id", req.EnvoyRouteID). Msg("failed to parse MCP request from check request") - } else { - log.Ctx(ctx).Debug(). - Str("request-id", requestid.FromContext(ctx)). - Str("route_id", req.EnvoyRouteID). - Str("mcp_tool", req.MCP.Tool). - Str("mcp_method", req.MCP.Method). - Msg("authorize request from check request") } } @@ -280,3 +282,13 @@ func getCheckRequestHeaders(req *envoy_service_auth_v3.CheckRequest) map[string] } return hdrs } + +func updateSpanWithMCPInfo(span oteltrace.Span, mcp evaluator.RequestMCP) { + if mcp.Method == "" { + return + } + span.SetAttributes(attribute.String("mcp.method", mcp.Method)) + if tc := mcp.ToolCall; tc != nil { + span.SetAttributes(attribute.String("mcp.tool", tc.Name)) + } +} diff --git a/authorize/grpc_test.go b/authorize/grpc_test.go index 7fb00c82f..e3d81741b 100644 --- a/authorize/grpc_test.go +++ b/authorize/grpc_test.go @@ -87,6 +87,7 @@ func Test_getEvaluatorRequest(t *testing.T) { }, }, }, + false, // mcp disabled ) require.NoError(t, err) expect := &evaluator.Request{ @@ -144,7 +145,7 @@ func Test_getEvaluatorRequestWithPortInHostHeader(t *testing.T) { }, }, }, - }) + }, false) // mcp disabled require.NoError(t, err) expect := &evaluator.Request{ Policy: &a.currentConfig.Load().Options.Policies[0], @@ -168,6 +169,53 @@ func Test_getEvaluatorRequestWithPortInHostHeader(t *testing.T) { assert.Equal(t, expect, actual) } +func Test_MCP_TraceAttributes(t *testing.T) { + t.Parallel() + + // Test MCP request parsing + mcpBody := `{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"database_query","arguments":{"query":"SELECT * FROM users","limit":10}}}` + + req := &envoy_service_auth_v3.CheckRequest{ + Attributes: &envoy_service_auth_v3.AttributeContext{ + Request: &envoy_service_auth_v3.AttributeContext_Request{ + Http: &envoy_service_auth_v3.AttributeContext_HttpRequest{ + Body: mcpBody, + }, + }, + }, + } + + mcpReq, ok := evaluator.RequestMCPFromCheckRequest(req) + require.True(t, ok, "should successfully parse MCP request") + + assert.Equal(t, "tools/call", mcpReq.Method) + require.NotNil(t, mcpReq.ToolCall) + assert.Equal(t, "database_query", mcpReq.ToolCall.Name) + assert.NotNil(t, mcpReq.ToolCall.Arguments) + assert.Equal(t, "SELECT * FROM users", mcpReq.ToolCall.Arguments["query"]) + assert.Equal(t, float64(10), mcpReq.ToolCall.Arguments["limit"]) + + // Test non-tools/call method + mcpBodyList := `{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}` + req.Attributes.Request.Http.Body = mcpBodyList + + mcpReq, ok = evaluator.RequestMCPFromCheckRequest(req) + require.True(t, ok, "should successfully parse MCP list request") + + assert.Equal(t, "tools/list", mcpReq.Method) + assert.Nil(t, mcpReq.ToolCall) + + // Test invalid JSON + req.Attributes.Request.Http.Body = `invalid json` + mcpReq, ok = evaluator.RequestMCPFromCheckRequest(req) + assert.False(t, ok, "should fail to parse invalid JSON") + + // Test empty body + req.Attributes.Request.Http.Body = "" + mcpReq, ok = evaluator.RequestMCPFromCheckRequest(req) + assert.False(t, ok, "should fail to parse empty body") +} + type mockDataBrokerServiceClient struct { databroker.DataBrokerServiceClient diff --git a/authorize/log.go b/authorize/log.go index 68008af61..4605b597a 100644 --- a/authorize/log.go +++ b/authorize/log.go @@ -184,6 +184,21 @@ func populateLogEvent( return evt case log.AuthorizeLogFieldIP: return evt.Str(string(field), req.HTTP.IP) + case log.AuthorizeLogFieldMCPMethod: + if method := req.MCP.Method; method != "" { + return evt.Str(string(field), req.MCP.Method) + } + return evt + case log.AuthorizeLogFieldMCPTool: + if req.MCP.ToolCall != nil { + return evt.Str(string(field), req.MCP.ToolCall.Name) + } + return evt + case log.AuthorizeLogFieldMCPToolParameters: + if req.MCP.ToolCall != nil && req.MCP.ToolCall.Arguments != nil { + return evt.Interface(string(field), req.MCP.ToolCall.Arguments) + } + return evt case log.AuthorizeLogFieldMethod: return evt.Str(string(field), req.HTTP.Method) case log.AuthorizeLogFieldPath: diff --git a/authorize/log_test.go b/authorize/log_test.go index 4c4975cb3..5694cc53d 100644 --- a/authorize/log_test.go +++ b/authorize/log_test.go @@ -31,6 +31,13 @@ func Test_populateLogEvent(t *testing.T) { Headers: map[string]string{"X-Request-Id": "CHECK-REQUEST-ID"}, IP: "127.0.0.1", }, + MCP: evaluator.RequestMCP{ + Method: "tools/call", + ToolCall: &evaluator.RequestMCPToolCall{ + Name: "list_tables", + Arguments: map[string]interface{}{"database": "test", "schema": "public"}, + }, + }, EnvoyRouteChecksum: 1234, EnvoyRouteID: "ROUTE-ID", Policy: &config.Policy{ @@ -79,6 +86,9 @@ func Test_populateLogEvent(t *testing.T) { {log.AuthorizeLogFieldImpersonateSessionID, s, `{"impersonate-session-id":"IMPERSONATE-SESSION-ID"}`}, {log.AuthorizeLogFieldImpersonateUserID, s, `{"impersonate-user-id":"IMPERSONATE-USER-ID"}`}, {log.AuthorizeLogFieldIP, s, `{"ip":"127.0.0.1"}`}, + {log.AuthorizeLogFieldMCPMethod, s, `{"mcp-method":"tools/call"}`}, + {log.AuthorizeLogFieldMCPTool, s, `{"mcp-tool":"list_tables"}`}, + {log.AuthorizeLogFieldMCPToolParameters, s, `{"mcp-tool-parameters":{"database":"test","schema":"public"}}`}, {log.AuthorizeLogFieldMethod, s, `{"method":"GET"}`}, {log.AuthorizeLogFieldPath, s, `{"path":"/some/path"}`}, {log.AuthorizeLogFieldQuery, s, `{"query":"a=b"}`}, @@ -105,3 +115,80 @@ func Test_populateLogEvent(t *testing.T) { }) } } + +// Test_MCP_LogFields tests that MCP-specific log fields are properly populated +func Test_MCP_LogFields(t *testing.T) { + t.Parallel() + + ctx := t.Context() + ctx = requestid.WithValue(ctx, "MCP-REQUEST-ID") + + // Test with a tools/call request + req := &evaluator.Request{ + MCP: evaluator.RequestMCP{ + Method: "tools/call", + ToolCall: &evaluator.RequestMCPToolCall{ + Name: "database_query", + Arguments: map[string]interface{}{ + "query": "SELECT * FROM users", + "limit": 100, + "format": "json", + }, + }, + }, + } + + var buf bytes.Buffer + logger := zerolog.New(&buf) + + // Test MCP method field + evt := logger.Log() + evt = populateLogEvent(ctx, log.AuthorizeLogFieldMCPMethod, evt, req, nil, nil, nil, nil) + evt.Send() + assert.Contains(t, buf.String(), `"mcp-method":"tools/call"`) + buf.Reset() + + // Test MCP tool field + evt = logger.Log() + evt = populateLogEvent(ctx, log.AuthorizeLogFieldMCPTool, evt, req, nil, nil, nil, nil) + evt.Send() + assert.Contains(t, buf.String(), `"mcp-tool":"database_query"`) + buf.Reset() + + // Test MCP tool parameters field + evt = logger.Log() + evt = populateLogEvent(ctx, log.AuthorizeLogFieldMCPToolParameters, evt, req, nil, nil, nil, nil) + evt.Send() + assert.Contains(t, buf.String(), `"mcp-tool-parameters":`) + assert.Contains(t, buf.String(), `"query":"SELECT * FROM users"`) + assert.Contains(t, buf.String(), `"limit":100`) + assert.Contains(t, buf.String(), `"format":"json"`) + buf.Reset() + + // Test with a non-tools/call request (no tool or parameters) + req.MCP = evaluator.RequestMCP{ + Method: "tools/list", + } + + evt = logger.Log() + evt = populateLogEvent(ctx, log.AuthorizeLogFieldMCPMethod, evt, req, nil, nil, nil, nil) + evt.Send() + assert.Contains(t, buf.String(), `"mcp-method":"tools/list"`) + buf.Reset() + + evt = logger.Log() + evt = populateLogEvent(ctx, log.AuthorizeLogFieldMCPTool, evt, req, nil, nil, nil, nil) + evt.Send() + // Should not contain the field when ToolCall is nil + assert.NotContains(t, buf.String(), `"mcp-tool"`) + buf.Reset() + + // Test with empty MCP data + req.MCP = evaluator.RequestMCP{} + + evt = logger.Log() + evt = populateLogEvent(ctx, log.AuthorizeLogFieldMCPToolParameters, evt, req, nil, nil, nil, nil) + evt.Send() + // Should not contain the field when parameters are nil + assert.NotContains(t, buf.String(), `"mcp-tool-parameters"`) +} diff --git a/config/envoyconfig/clusters.go b/config/envoyconfig/clusters.go index 36c273b42..e5ab0d863 100644 --- a/config/envoyconfig/clusters.go +++ b/config/envoyconfig/clusters.go @@ -123,6 +123,11 @@ func (b *Builder) BuildClusters(ctx context.Context, cfg *config.Config) ([]*env return clusters, nil } +var defaultTCPKeepalive = &envoy_config_core_v3.TcpKeepalive{ + KeepaliveTime: wrapperspb.UInt32(15), + KeepaliveInterval: wrapperspb.UInt32(15), +} + func (b *Builder) buildInternalCluster( ctx context.Context, cfg *config.Config, @@ -134,12 +139,8 @@ func (b *Builder) buildInternalCluster( cluster := newDefaultEnvoyClusterConfig() cluster.DnsLookupFamily = config.GetEnvoyDNSLookupFamily(cfg.Options.DNSLookupFamily) // Match the Go standard library default TCP keepalive settings. - const keepaliveTimeSeconds = 15 cluster.UpstreamConnectionOptions = &envoy_config_cluster_v3.UpstreamConnectionOptions{ - TcpKeepalive: &envoy_config_core_v3.TcpKeepalive{ - KeepaliveTime: wrapperspb.UInt32(keepaliveTimeSeconds), - KeepaliveInterval: wrapperspb.UInt32(keepaliveTimeSeconds), - }, + TcpKeepalive: defaultTCPKeepalive, } var endpoints []Endpoint for _, dst := range dsts { diff --git a/go.mod b/go.mod index d16043c83..3cdeffcb0 100644 --- a/go.mod +++ b/go.mod @@ -93,10 +93,10 @@ require ( go.uber.org/automaxprocs v1.6.0 go.uber.org/mock v0.5.2 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.38.0 - golang.org/x/net v0.40.0 + golang.org/x/crypto v0.39.0 + golang.org/x/net v0.41.0 golang.org/x/oauth2 v0.30.0 - golang.org/x/sync v0.14.0 + golang.org/x/sync v0.15.0 golang.org/x/sys v0.33.0 golang.org/x/time v0.11.0 google.golang.org/api v0.235.0 @@ -267,9 +267,9 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/tools v0.32.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 4787200b5..4d3d85882 100644 --- a/go.sum +++ b/go.sum @@ -820,8 +820,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -856,8 +856,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -897,8 +897,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -921,8 +921,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1000,8 +1000,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1052,8 +1052,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/log/authorize.go b/internal/log/authorize.go index 5065af112..4d92518e9 100644 --- a/internal/log/authorize.go +++ b/internal/log/authorize.go @@ -22,6 +22,9 @@ const ( AuthorizeLogFieldImpersonateSessionID AuthorizeLogField = "impersonate-session-id" AuthorizeLogFieldImpersonateUserID AuthorizeLogField = "impersonate-user-id" AuthorizeLogFieldIP AuthorizeLogField = "ip" + AuthorizeLogFieldMCPMethod AuthorizeLogField = "mcp-method" + AuthorizeLogFieldMCPTool AuthorizeLogField = "mcp-tool" + AuthorizeLogFieldMCPToolParameters AuthorizeLogField = "mcp-tool-parameters" AuthorizeLogFieldMethod AuthorizeLogField = "method" AuthorizeLogFieldPath AuthorizeLogField = "path" AuthorizeLogFieldQuery AuthorizeLogField = "query" @@ -72,6 +75,9 @@ var authorizeLogFieldLookup = map[AuthorizeLogField]struct{}{ AuthorizeLogFieldImpersonateSessionID: {}, AuthorizeLogFieldImpersonateUserID: {}, AuthorizeLogFieldIP: {}, + AuthorizeLogFieldMCPMethod: {}, + AuthorizeLogFieldMCPTool: {}, + AuthorizeLogFieldMCPToolParameters: {}, AuthorizeLogFieldMethod: {}, AuthorizeLogFieldPath: {}, AuthorizeLogFieldQuery: {}, diff --git a/pkg/policy/criteria/criteria_test.go b/pkg/policy/criteria/criteria_test.go index de1e39c7e..a4be31d4a 100644 --- a/pkg/policy/criteria/criteria_test.go +++ b/pkg/policy/criteria/criteria_test.go @@ -45,9 +45,14 @@ type ( ID string `json:"id"` } InputMCP struct { - Tool string `json:"tool"` - Method string `json:"method"` + Method string `json:"method,omitempty"` + ToolCall *InputMCPToolCall `json:"tool_call,omitempty"` } + + InputMCPToolCall struct { + Name string `json:"name"` + } + ClientCertificateInfo struct { Presented bool `json:"presented"` Leaf string `json:"leaf"` diff --git a/pkg/policy/criteria/mcp_tool.go b/pkg/policy/criteria/mcp_tool.go index c7a9f7be5..a16f2bed7 100644 --- a/pkg/policy/criteria/mcp_tool.go +++ b/pkg/policy/criteria/mcp_tool.go @@ -32,7 +32,7 @@ func (c mcpToolCriterion) GenerateRule(_ string, data parser.Value) (*ast.Rule, ast.MustParseExpr(`input.mcp.method == "tools/call"`), }, } - toolRef := ast.RefTerm(ast.VarTerm("input"), ast.VarTerm("mcp"), ast.VarTerm("tool")) + toolRef := ast.RefTerm(ast.VarTerm("input"), ast.VarTerm("mcp"), ast.VarTerm("tool_call"), ast.VarTerm("name")) err := matchString(&r2.Body, toolRef, data) if err != nil { return nil, nil, err diff --git a/pkg/policy/criteria/mcp_tool_test.go b/pkg/policy/criteria/mcp_tool_test.go index 4b716c1eb..365818332 100644 --- a/pkg/policy/criteria/mcp_tool_test.go +++ b/pkg/policy/criteria/mcp_tool_test.go @@ -15,7 +15,7 @@ allow: and: - mcp_tool: is: list_tables -`, []*databroker.Record{}, Input{MCP: InputMCP{Tool: "list_tables", Method: "tools/call"}}) +`, []*databroker.Record{}, Input{MCP: InputMCP{Method: "tools/call", ToolCall: &InputMCPToolCall{Name: "list_tables"}}}) require.NoError(t, err) require.Equal(t, A{true, A{ReasonMCPToolOK}, M{}}, res["allow"]) require.Equal(t, A{false, A{}}, res["deny"]) @@ -27,7 +27,7 @@ allow: and: - mcp_tool: is: list_tables -`, []*databroker.Record{}, Input{MCP: InputMCP{Tool: "read_table", Method: "tools/call"}}) +`, []*databroker.Record{}, Input{MCP: InputMCP{Method: "tools/call", ToolCall: &InputMCPToolCall{Name: "read_table"}}}) require.NoError(t, err) require.Equal(t, A{false, A{ReasonMCPToolUnauthorized}, M{}}, res["allow"]) require.Equal(t, A{false, A{}}, res["deny"]) @@ -39,7 +39,7 @@ allow: and: - mcp_tool: in: ["list_tables", "read_table"] -`, []*databroker.Record{}, Input{MCP: InputMCP{Tool: "list_tables", Method: "tools/call"}}) +`, []*databroker.Record{}, Input{MCP: InputMCP{Method: "tools/call", ToolCall: &InputMCPToolCall{Name: "list_tables"}}}) require.NoError(t, err) require.Equal(t, A{true, A{ReasonMCPToolOK}, M{}}, res["allow"]) require.Equal(t, A{false, A{}}, res["deny"]) @@ -51,7 +51,7 @@ allow: and: - mcp_tool: in: ["list_tables", "read_table"] -`, []*databroker.Record{}, Input{MCP: InputMCP{Tool: "delete_table", Method: "tools/call"}}) +`, []*databroker.Record{}, Input{MCP: InputMCP{Method: "tools/call", ToolCall: &InputMCPToolCall{Name: "delete_table"}}}) require.NoError(t, err) require.Equal(t, A{false, A{ReasonMCPToolUnauthorized}, M{}}, res["allow"]) require.Equal(t, A{false, A{}}, res["deny"])