Skip to content

Commit d230e3c

Browse files
fix(controller): honor ReferenceGrantTo.Name in cross-namespace validation
Per Gateway API spec, `ReferenceGrantTo.Name` is optional. When set, the grant must apply only to that named target; when unset, it acts as a wildcard for the group/kind. The controller was ignoring the field entirely, so a name-scoped grant like to: - group: aigateway.envoyproxy.io kind: AIServiceBackend name: safe-backend silently allowed AIGatewayRoutes to reference *any* AIServiceBackend of that group/kind in the target namespace — not what the grant author meant. In practice this widens cross-namespace permission surface beyond what the policy declared. Thread `targetName` through validateReference → isReferenceGrantValid → matchesTo so the name comparison happens at the leaf. Wildcard behaviour (name unset) is preserved. Regression test added covering all three cases: name-unset wildcard, name-matches (allowed), name-mismatch (denied). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d10579c commit d230e3c

2 files changed

Lines changed: 56 additions & 16 deletions

File tree

internal/controller/referencegrant.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func (v *referenceGrantValidator) validateReference(
9595
// Check if any ReferenceGrant allows this cross-namespace reference.
9696
for i := range referenceGrants.Items {
9797
grant := &referenceGrants.Items[i]
98-
if v.isReferenceGrantValid(grant, routeNamespace, targetGroup, targetKind) {
98+
if v.isReferenceGrantValid(grant, routeNamespace, targetGroup, targetKind, gwapiv1b1.ObjectName(targetName)) {
9999
return nil
100100
}
101101
}
@@ -109,12 +109,13 @@ func (v *referenceGrantValidator) validateReference(
109109
}
110110

111111
// isReferenceGrantValid checks if a ReferenceGrant allows an AIGatewayRoute to reference the
112-
// target resource identified by targetGroup/targetKind.
112+
// target resource identified by targetGroup/targetKind/targetName.
113113
func (v *referenceGrantValidator) isReferenceGrantValid(
114114
grant *gwapiv1b1.ReferenceGrant,
115115
fromNamespace string,
116116
targetGroup gwapiv1b1.Group,
117117
targetKind gwapiv1b1.Kind,
118+
targetName gwapiv1b1.ObjectName,
118119
) bool {
119120
// Check if the grant allows references from the route's namespace.
120121
fromAllowed := false
@@ -131,7 +132,7 @@ func (v *referenceGrantValidator) isReferenceGrantValid(
131132

132133
// Check if the grant allows references to the target resource.
133134
for _, to := range grant.Spec.To {
134-
if v.matchesTo(&to, targetGroup, targetKind) {
135+
if v.matchesTo(&to, targetGroup, targetKind, targetName) {
135136
return true
136137
}
137138
}
@@ -159,22 +160,31 @@ func (v *referenceGrantValidator) matchesFrom(from *gwapiv1b1.ReferenceGrantFrom
159160
return true
160161
}
161162

162-
// matchesTo checks if a ReferenceGrantTo matches the target resource identified by targetGroup/targetKind.
163-
func (v *referenceGrantValidator) matchesTo(to *gwapiv1b1.ReferenceGrantTo, targetGroup gwapiv1b1.Group, targetKind gwapiv1b1.Kind) bool {
164-
// Check group
163+
// matchesTo checks if a ReferenceGrantTo matches the target resource identified by
164+
// targetGroup/targetKind/targetName.
165+
//
166+
// Per the Gateway API spec, ReferenceGrantTo.Name is optional. When set, the grant is scoped
167+
// to exactly that named resource; when unset, it acts as a wildcard covering every resource of
168+
// the group/kind. Previously we ignored the name field entirely, which quietly promoted any
169+
// name-scoped grant into a wildcard and let an AIGatewayRoute reference *any* backend of the
170+
// same kind in the target namespace — not what the grant author meant.
171+
//
172+
// https://gateway-api.sigs.k8s.io/api-types/referencegrant/
173+
func (v *referenceGrantValidator) matchesTo(
174+
to *gwapiv1b1.ReferenceGrantTo,
175+
targetGroup gwapiv1b1.Group,
176+
targetKind gwapiv1b1.Kind,
177+
targetName gwapiv1b1.ObjectName,
178+
) bool {
165179
if to.Group != targetGroup {
166180
return false
167181
}
168-
169-
// Check kind
170182
if to.Kind != targetKind {
171183
return false
172184
}
173-
174-
// If a specific name is specified, we would need to check it here,
175-
// but ReferenceGrant typically doesn't specify individual resource names
176-
// (that's handled by the Name field which is optional in the spec)
177-
// For now, we only check group and kind as per Gateway API spec
178-
185+
// Name is optional in ReferenceGrantTo. Unset == wildcard; set == exact match required.
186+
if to.Name != nil && *to.Name != targetName {
187+
return false
188+
}
179189
return true
180190
}

internal/controller/referencegrant_test.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,36 @@ func TestReferenceGrantValidator_MatchesFrom_WrongNamespace(t *testing.T) {
442442
require.False(t, result, "should return false for wrong namespace")
443443
}
444444

445+
// TestReferenceGrantValidator_MatchesTo_Name locks in the name-filter behaviour: when
446+
// ReferenceGrantTo.Name is set, the grant must apply only to the named resource; when unset,
447+
// it acts as a wildcard for the group/kind.
448+
func TestReferenceGrantValidator_MatchesTo_Name(t *testing.T) {
449+
scheme := runtime.NewScheme()
450+
_ = gwapiv1b1.Install(scheme)
451+
_ = aigv1a1.AddToScheme(scheme)
452+
453+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
454+
validator := newReferenceGrantValidator(fakeClient)
455+
456+
t.Run("name unset → wildcard, matches any name", func(t *testing.T) {
457+
to := &gwapiv1b1.ReferenceGrantTo{Group: aiServiceBackendGroup, Kind: aiServiceBackendKind}
458+
require.True(t, validator.matchesTo(to, aiServiceBackendGroup, aiServiceBackendKind, "any-backend"))
459+
})
460+
461+
t.Run("name set and matches → allowed", func(t *testing.T) {
462+
named := gwapiv1b1.ObjectName("target-backend")
463+
to := &gwapiv1b1.ReferenceGrantTo{Group: aiServiceBackendGroup, Kind: aiServiceBackendKind, Name: &named}
464+
require.True(t, validator.matchesTo(to, aiServiceBackendGroup, aiServiceBackendKind, "target-backend"))
465+
})
466+
467+
t.Run("name set but differs → denied", func(t *testing.T) {
468+
named := gwapiv1b1.ObjectName("target-backend")
469+
to := &gwapiv1b1.ReferenceGrantTo{Group: aiServiceBackendGroup, Kind: aiServiceBackendKind, Name: &named}
470+
require.False(t, validator.matchesTo(to, aiServiceBackendGroup, aiServiceBackendKind, "other-backend"),
471+
"name-scoped grant must not promote to a wildcard")
472+
})
473+
}
474+
445475
// TestReferenceGrantValidator_MatchesTo_WrongGroup tests matchesTo with wrong group
446476
func TestReferenceGrantValidator_MatchesTo_WrongGroup(t *testing.T) {
447477
scheme := runtime.NewScheme()
@@ -456,7 +486,7 @@ func TestReferenceGrantValidator_MatchesTo_WrongGroup(t *testing.T) {
456486
Kind: aiServiceBackendKind,
457487
}
458488

459-
result := validator.matchesTo(to, aiServiceBackendGroup, aiServiceBackendKind)
489+
result := validator.matchesTo(to, aiServiceBackendGroup, aiServiceBackendKind, "target-backend")
460490
require.False(t, result, "should return false for wrong group")
461491
}
462492

@@ -474,7 +504,7 @@ func TestReferenceGrantValidator_MatchesTo_WrongKind(t *testing.T) {
474504
Kind: "WrongKind",
475505
}
476506

477-
result := validator.matchesTo(to, aiServiceBackendGroup, aiServiceBackendKind)
507+
result := validator.matchesTo(to, aiServiceBackendGroup, aiServiceBackendKind, "target-backend")
478508
require.False(t, result, "should return false for wrong kind")
479509
}
480510

0 commit comments

Comments
 (0)