Skip to content

Commit d8b87e6

Browse files
committed
update
Signed-off-by: Huabing Zhao <[email protected]>
1 parent bc62a62 commit d8b87e6

File tree

4 files changed

+82
-17
lines changed

4 files changed

+82
-17
lines changed

internal/controller/gateway.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strings"
1414
"time"
1515

16+
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
1617
"github.com/go-logr/logr"
1718
"github.com/google/uuid"
1819
appsv1 "k8s.io/api/apps/v1"
@@ -471,6 +472,51 @@ func mcpConfig(mcpRoutes []aigv1a1.MCPRoute) *filterapi.MCPConfig {
471472
mcpRoute.Backends = append(
472473
mcpRoute.Backends, mcpBackend)
473474
}
475+
// Add authorization configuration for the route.
476+
if route.Spec.SecurityPolicy != nil && route.Spec.SecurityPolicy.Authorization != nil {
477+
authorization := route.Spec.SecurityPolicy.Authorization
478+
mcpRoute.Authorization = &filterapi.MCPRouteAuthorization{}
479+
480+
defaultAction := ptr.Deref(authorization.DefaultAction, egv1a1.AuthorizationActionDeny)
481+
if defaultAction == egv1a1.AuthorizationActionAllow {
482+
mcpRoute.Authorization.DefaultAction = filterapi.AuthorizationActionAllow
483+
} else {
484+
mcpRoute.Authorization.DefaultAction = filterapi.AuthorizationActionDeny
485+
}
486+
487+
for _, rule := range authorization.Rules {
488+
action := filterapi.AuthorizationActionDeny
489+
if rule.Action == egv1a1.AuthorizationActionAllow {
490+
action = filterapi.AuthorizationActionAllow
491+
}
492+
493+
scopes := make([]string, len(rule.Source.JWTSource.Scopes))
494+
for i, scope := range rule.Source.JWTSource.Scopes {
495+
scopes[i] = string(scope)
496+
}
497+
498+
tools := make([]filterapi.ToolCall, len(rule.Target.Tools))
499+
for i, tool := range rule.Target.Tools {
500+
tools[i] = filterapi.ToolCall{
501+
BackendName: tool.BackendName,
502+
ToolName: tool.ToolName,
503+
}
504+
}
505+
506+
mcpRule := filterapi.MCPRouteAuthorizationRule{
507+
Action: action,
508+
Source: filterapi.MCPAuthorizationSource{
509+
JWTSource: filterapi.JWTSource{
510+
Scopes: scopes,
511+
},
512+
},
513+
Target: filterapi.MCPAuthorizationTarget{
514+
Tools: tools,
515+
},
516+
}
517+
mcpRoute.Authorization.Rules = append(mcpRoute.Authorization.Rules, mcpRule)
518+
}
519+
}
474520
mc.Routes = append(mc.Routes, mcpRoute)
475521
}
476522
return mc

internal/filterapi/mcpconfig.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ type MCPRouteAuthorization struct {
7070
Rules []MCPRouteAuthorizationRule `json:"rules,omitempty"`
7171

7272
// DefaultAction defines the action to take when no rules match.
73-
// If unset, the default is Deny.
74-
DefaultAction *AuthorizationAction `json:"defaultAction,omitempty"`
73+
DefaultAction AuthorizationAction `json:"defaultAction,omitempty"`
7574
}
7675

7776
// MCPRouteAuthorizationRule defines an authorization rule for MCPRoute based on the MCP authorization spec.

internal/mcpproxy/authorization.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ import (
1212
"strings"
1313

1414
"github.com/golang-jwt/jwt/v5"
15-
"k8s.io/utils/ptr"
1615

1716
"github.com/envoyproxy/ai-gateway/internal/filterapi"
1817
)
1918

2019
func (m *MCPProxy) authorizeRequest(authorization *filterapi.MCPRouteAuthorization, headers http.Header, backendName, toolName string) bool {
21-
defaultAction := ptr.Deref(authorization.DefaultAction, filterapi.AuthorizationActionDeny) == filterapi.AuthorizationActionAllow
20+
defaultAction := authorization.DefaultAction == filterapi.AuthorizationActionAllow
2221

2322
// If there are no rules, return the default action.
2423
if len(authorization.Rules) == 0 {
@@ -115,6 +114,7 @@ func scopesSatisfied(have map[string]struct{}, required []string) bool {
115114
if len(required) == 0 {
116115
return true
117116
}
117+
// All required scopes must be present for authorization to succeed.
118118
for _, scope := range required {
119119
if _, ok := have[scope]; !ok {
120120
return false

internal/mcpproxy/authorization_test.go

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"testing"
1313

1414
"github.com/golang-jwt/jwt/v5"
15-
"k8s.io/utils/ptr"
1615

1716
"github.com/envoyproxy/ai-gateway/internal/filterapi"
1817
)
@@ -42,11 +41,11 @@ func TestAuthorizeRequest(t *testing.T) {
4241
{
4342
name: "matching tool and scope allowed",
4443
auth: &filterapi.MCPRouteAuthorization{
45-
DefaultAction: ptr.To(filterapi.AuthorizationActionDeny),
44+
DefaultAction: filterapi.AuthorizationActionDeny,
4645
Rules: []filterapi.MCPRouteAuthorizationRule{
4746
{
4847
Source: filterapi.MCPAuthorizationSource{
49-
JWTSource: filterapi.JWTSource{Scopes: []string{"read"}},
48+
JWTSource: filterapi.JWTSource{Scopes: []string{"read", "write"}},
5049
},
5150
Target: filterapi.MCPAuthorizationTarget{
5251
Tools: []filterapi.ToolCall{{BackendName: "backend1", ToolName: "tool1"}},
@@ -60,10 +59,31 @@ func TestAuthorizeRequest(t *testing.T) {
6059
toolName: "tool1",
6160
expectAllowed: true,
6261
},
62+
{
63+
name: "matching tool but insufficient scopes not allowed",
64+
auth: &filterapi.MCPRouteAuthorization{
65+
DefaultAction: filterapi.AuthorizationActionDeny,
66+
Rules: []filterapi.MCPRouteAuthorizationRule{
67+
{
68+
Source: filterapi.MCPAuthorizationSource{
69+
JWTSource: filterapi.JWTSource{Scopes: []string{"read", "write"}},
70+
},
71+
Target: filterapi.MCPAuthorizationTarget{
72+
Tools: []filterapi.ToolCall{{BackendName: "backend1", ToolName: "tool1"}},
73+
},
74+
Action: filterapi.AuthorizationActionAllow,
75+
},
76+
},
77+
},
78+
header: "Bearer " + makeToken("read"),
79+
backendName: "backend1",
80+
toolName: "tool1",
81+
expectAllowed: false,
82+
},
6383
{
6484
name: "no matching rule falls back to default deny - tool mismatch",
6585
auth: &filterapi.MCPRouteAuthorization{
66-
DefaultAction: ptr.To(filterapi.AuthorizationActionDeny),
86+
DefaultAction: filterapi.AuthorizationActionDeny,
6787
Rules: []filterapi.MCPRouteAuthorizationRule{
6888
{
6989
Source: filterapi.MCPAuthorizationSource{
@@ -84,7 +104,7 @@ func TestAuthorizeRequest(t *testing.T) {
84104
{
85105
name: "no matching rule falls back to default deny - scope mismatch",
86106
auth: &filterapi.MCPRouteAuthorization{
87-
DefaultAction: ptr.To(filterapi.AuthorizationActionDeny),
107+
DefaultAction: filterapi.AuthorizationActionDeny,
88108
Rules: []filterapi.MCPRouteAuthorizationRule{
89109
{
90110
Source: filterapi.MCPAuthorizationSource{
@@ -105,7 +125,7 @@ func TestAuthorizeRequest(t *testing.T) {
105125
{
106126
name: "matching tool and scope denied",
107127
auth: &filterapi.MCPRouteAuthorization{
108-
DefaultAction: ptr.To(filterapi.AuthorizationActionAllow),
128+
DefaultAction: filterapi.AuthorizationActionAllow,
109129
Rules: []filterapi.MCPRouteAuthorizationRule{
110130
{
111131
Source: filterapi.MCPAuthorizationSource{
@@ -126,7 +146,7 @@ func TestAuthorizeRequest(t *testing.T) {
126146
{
127147
name: "no matching rule falls back to default allow - tool mismatch",
128148
auth: &filterapi.MCPRouteAuthorization{
129-
DefaultAction: ptr.To(filterapi.AuthorizationActionAllow),
149+
DefaultAction: filterapi.AuthorizationActionAllow,
130150
Rules: []filterapi.MCPRouteAuthorizationRule{
131151
{
132152
Source: filterapi.MCPAuthorizationSource{
@@ -147,7 +167,7 @@ func TestAuthorizeRequest(t *testing.T) {
147167
{
148168
name: "no matching rule falls back to default allow - scope mismatch",
149169
auth: &filterapi.MCPRouteAuthorization{
150-
DefaultAction: ptr.To(filterapi.AuthorizationActionAllow),
170+
DefaultAction: filterapi.AuthorizationActionAllow,
151171
Rules: []filterapi.MCPRouteAuthorizationRule{
152172
{
153173
Source: filterapi.MCPAuthorizationSource{
@@ -168,7 +188,7 @@ func TestAuthorizeRequest(t *testing.T) {
168188
{
169189
name: "no rules falls back to default allow",
170190
auth: &filterapi.MCPRouteAuthorization{
171-
DefaultAction: ptr.To(filterapi.AuthorizationActionAllow),
191+
DefaultAction: filterapi.AuthorizationActionAllow,
172192
},
173193
header: "",
174194
backendName: "backend1",
@@ -178,7 +198,7 @@ func TestAuthorizeRequest(t *testing.T) {
178198
{
179199
name: "no rules falls back to default deny",
180200
auth: &filterapi.MCPRouteAuthorization{
181-
DefaultAction: ptr.To(filterapi.AuthorizationActionDeny),
201+
DefaultAction: filterapi.AuthorizationActionDeny,
182202
},
183203
header: "",
184204
backendName: "backend1",
@@ -188,7 +208,7 @@ func TestAuthorizeRequest(t *testing.T) {
188208
{
189209
name: "no bearer token not allowed when rules exist",
190210
auth: &filterapi.MCPRouteAuthorization{
191-
DefaultAction: ptr.To(filterapi.AuthorizationActionAllow),
211+
DefaultAction: filterapi.AuthorizationActionAllow,
192212
Rules: []filterapi.MCPRouteAuthorizationRule{
193213
{
194214
Source: filterapi.MCPAuthorizationSource{
@@ -209,7 +229,7 @@ func TestAuthorizeRequest(t *testing.T) {
209229
{
210230
name: "multiple rules, first match applied - denied",
211231
auth: &filterapi.MCPRouteAuthorization{
212-
DefaultAction: ptr.To(filterapi.AuthorizationActionDeny),
232+
DefaultAction: filterapi.AuthorizationActionDeny,
213233
Rules: []filterapi.MCPRouteAuthorizationRule{
214234
{
215235
Source: filterapi.MCPAuthorizationSource{
@@ -239,7 +259,7 @@ func TestAuthorizeRequest(t *testing.T) {
239259
{
240260
name: "multiple rules, first match applied - allowed",
241261
auth: &filterapi.MCPRouteAuthorization{
242-
DefaultAction: ptr.To(filterapi.AuthorizationActionDeny),
262+
DefaultAction: filterapi.AuthorizationActionDeny,
243263
Rules: []filterapi.MCPRouteAuthorizationRule{
244264
{
245265
Source: filterapi.MCPAuthorizationSource{

0 commit comments

Comments
 (0)