pomerium/pkg/policy/criteria/mcp_tool_test.go
Denis Mishin f9e7308f12
mcp: add mcp_tool to ppl (#5662)
## Summary

Adds `mcp_tool` PPL criterion, that matches MCP tool names like 

```yaml
  - from: https://db.localhost.pomerium.io
    to: http://localhost:3000/mcp
    policy:
      allow:
        and:
          - email: 
              in: ["user@pomerium.com"]
          - mcp_tool:
              in: ["list_tables", "read_table", "search_records"]
    mcp: {}
```

## Related issues

Fix
https://linear.app/pomerium/issue/ENG-2393/mcp-authorize-each-incoming-request-to-an-mcp-route

## User Explanation

<!-- How would you explain this change to the user? If this
change doesn't create any user-facing changes, you can leave
this blank. If filled out, add the `docs` label -->

## Checklist

- [x] reference any related issues
- [x] updated unit tests
- [x] add appropriate label (`enhancement`, `bug`, `breaking`,
`dependencies`, `ci`)
- [x] ready for review
2025-06-23 12:43:43 -04:00

71 lines
2 KiB
Go

package criteria
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/pomerium/pomerium/pkg/grpc/databroker"
)
func TestMCPTool(t *testing.T) {
t.Run("ok", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- mcp_tool:
is: list_tables
`, []*databroker.Record{}, Input{MCP: InputMCP{Tool: "list_tables", Method: "tools/call"}})
require.NoError(t, err)
require.Equal(t, A{true, A{ReasonMCPToolOK}, M{}}, res["allow"])
require.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("unauthorized", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- mcp_tool:
is: list_tables
`, []*databroker.Record{}, Input{MCP: InputMCP{Tool: "read_table", Method: "tools/call"}})
require.NoError(t, err)
require.Equal(t, A{false, A{ReasonMCPToolUnauthorized}, M{}}, res["allow"])
require.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("in list", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- mcp_tool:
in: ["list_tables", "read_table"]
`, []*databroker.Record{}, Input{MCP: InputMCP{Tool: "list_tables", Method: "tools/call"}})
require.NoError(t, err)
require.Equal(t, A{true, A{ReasonMCPToolOK}, M{}}, res["allow"])
require.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("not in list", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- mcp_tool:
in: ["list_tables", "read_table"]
`, []*databroker.Record{}, Input{MCP: InputMCP{Tool: "delete_table", Method: "tools/call"}})
require.NoError(t, err)
require.Equal(t, A{false, A{ReasonMCPToolUnauthorized}, M{}}, res["allow"])
require.Equal(t, A{false, A{}}, res["deny"])
})
t.Run("non-tools/call method should pass", func(t *testing.T) {
res, err := evaluate(t, `
allow:
and:
- mcp_tool:
is: list_tables
`, []*databroker.Record{}, Input{MCP: InputMCP{Method: "some/other_method"}})
require.NoError(t, err)
require.Equal(t, A{true, A{ReasonMCPNotAToolCall}, M{}}, res["allow"])
require.Equal(t, A{false, A{}}, res["deny"])
})
}