66package mcpproxy
77
88import (
9- "encoding/json"
109 "errors"
1110 "fmt"
1211 "log/slog"
1312 "net/http"
14- "regexp"
1513 "strings"
1614
1715 "github.com/golang-jwt/jwt/v5"
16+ "github.com/google/cel-go/cel"
17+ "github.com/google/cel-go/common/types"
1818 "k8s.io/apimachinery/pkg/util/sets"
1919
2020 "github.com/envoyproxy/ai-gateway/internal/filterapi"
2121)
2222
23+ type compiledAuthorization struct {
24+ ResourceMetadataURL string
25+ Rules []compiledAuthorizationRule
26+ }
27+
28+ type compiledAuthorizationRule struct {
29+ Source filterapi.MCPAuthorizationSource
30+ Target []compiledToolCall
31+ }
32+
33+ type compiledToolCall struct {
34+ BackendName string
35+ ToolName string
36+ Expression string
37+ program cel.Program
38+ }
39+
40+ // compileAuthorization compiles the MCPRouteAuthorization into a compiledAuthorization for efficient CEL evaluation.
41+ func compileAuthorization (auth * filterapi.MCPRouteAuthorization ) (* compiledAuthorization , error ) {
42+ if auth == nil {
43+ return nil , nil
44+ }
45+
46+ env , err := cel .NewEnv (
47+ cel .Variable ("args" , cel .DynType ),
48+ cel .OptionalTypes (),
49+ )
50+ if err != nil {
51+ return nil , fmt .Errorf ("failed to create CEL environment: %w" , err )
52+ }
53+
54+ compiled := & compiledAuthorization {
55+ ResourceMetadataURL : auth .ResourceMetadataURL ,
56+ }
57+
58+ for _ , rule := range auth .Rules {
59+ cr := compiledAuthorizationRule {
60+ Source : rule .Source ,
61+ }
62+ for _ , tool := range rule .Target .Tools {
63+ ct := compiledToolCall {
64+ BackendName : tool .BackendName ,
65+ ToolName : tool .ToolName ,
66+ }
67+ if tool .Arguments != nil && strings .TrimSpace (* tool .Arguments ) != "" {
68+ expr := strings .TrimSpace (* tool .Arguments )
69+ ast , issues := env .Compile (expr )
70+ if issues != nil && issues .Err () != nil {
71+ return nil , fmt .Errorf ("failed to compile arguments CEL for tool %s/%s: %w" , tool .BackendName , tool .ToolName , issues .Err ())
72+ }
73+ program , err := env .Program (ast , cel .CostLimit (10000 ))
74+ if err != nil {
75+ return nil , fmt .Errorf ("failed to build arguments CEL program for tool %s/%s: %w" , tool .BackendName , tool .ToolName , err )
76+ }
77+ ct .Expression = expr
78+ ct .program = program
79+ }
80+ cr .Target = append (cr .Target , ct )
81+ }
82+ compiled .Rules = append (compiled .Rules , cr )
83+ }
84+
85+ return compiled , nil
86+ }
87+
2388// authorizeRequest authorizes the request based on the given MCPRouteAuthorization configuration.
2489
25- func (m * MCPProxy ) authorizeRequest (authorization * filterapi. MCPRouteAuthorization , headers http.Header , backendName , toolName string , argments any ) (bool , []string ) {
90+ func (m * MCPProxy ) authorizeRequest (authorization * compiledAuthorization , headers http.Header , backendName , toolName string , arguments any ) (bool , []string ) {
2691 if authorization == nil {
2792 return true , nil
2893 }
@@ -48,17 +113,11 @@ func (m *MCPProxy) authorizeRequest(authorization *filterapi.MCPRouteAuthorizati
48113 return false , nil
49114 }
50115
51- scopeSet := sets .New [ string ] (extractScopes (claims )... )
116+ scopeSet := sets .New (extractScopes (claims )... )
52117 var missingScopesForChallenge []string
53118
54119 for _ , rule := range authorization .Rules {
55- var args map [string ]any
56- if argments != nil {
57- if cast , ok := argments .(map [string ]any ); ok {
58- args = cast
59- }
60- }
61- if ! m .toolMatches (filterapi.ToolCall {BackendName : backendName , ToolName : toolName }, rule .Target .Tools , args ) {
120+ if ! m .toolMatches (backendName , toolName , rule .Target , arguments ) {
62121 continue
63122 }
64123
@@ -117,53 +176,36 @@ func extractScopes(claims jwt.MapClaims) []string {
117176 }
118177}
119178
120- func (m * MCPProxy ) toolMatches (target filterapi. ToolCall , tools []filterapi. ToolCall , args map [ string ] any ) bool {
179+ func (m * MCPProxy ) toolMatches (backendName , toolName string , tools []compiledToolCall , args any ) bool {
121180 if len (tools ) == 0 {
122181 return true
123182 }
124183
125184 for _ , t := range tools {
126- if t .BackendName != target . BackendName || t .ToolName != target . ToolName {
185+ if t .BackendName != backendName || t .ToolName != toolName {
127186 continue
128187 }
129- if len ( t . Arguments ) == 0 {
188+ if t . program == nil {
130189 return true
131190 }
132- if args == nil {
133- return false
191+
192+ result , _ , err := t .program .Eval (map [string ]any {"args" : args })
193+ if err != nil {
194+ m .l .Error ("failed to evaluate arguments CEL" , slog .String ("backend" , t .BackendName ), slog .String ("tool" , t .ToolName ), slog .String ("error" , err .Error ()))
195+ continue
134196 }
135- allMatch := true
136- for key , pattern := range t .Arguments {
137- rawVal , ok := args [key ]
138- if ! ok {
139- allMatch = false
140- break
141- }
142- re , err := regexp .Compile (pattern )
143- if err != nil {
144- m .l .Error ("invalid argument regex pattern" , slog .String ("pattern" , pattern ), slog .String ("error" , err .Error ()))
145- allMatch = false
146- break
147- }
148- var data []byte
149- if s , ok := rawVal .(string ); ok {
150- data = []byte (s )
151- } else {
152- jsonVal , err := json .Marshal (rawVal )
153- if err != nil {
154- m .l .Error ("failed to marshal argument value to json" , slog .String ("key" , key ), slog .String ("error" , err .Error ()))
155- allMatch = false
156- break
157- }
158- data = jsonVal
197+
198+ switch v := result .Value ().(type ) {
199+ case bool :
200+ if v {
201+ return true
159202 }
160- if ! re . Match ( data ) {
161- allMatch = false
162- break
203+ case types. Bool :
204+ if bool ( v ) {
205+ return true
163206 }
164- }
165- if allMatch {
166- return true
207+ default :
208+ m .l .Error ("arguments CEL did not return a boolean" , slog .String ("backend" , t .BackendName ), slog .String ("tool" , t .ToolName ), slog .String ("expression" , t .Expression ))
167209 }
168210 }
169211 // If no matching tool entry or no arguments matched, fail.
0 commit comments