Skip to content

Commit 39a15f6

Browse files
xds: implement cluster metadata parsing for GCP Authentication filter (gRFC A83) (#9044)
This PR implements the xDS Cluster Metadata parsing logic as specified in [gRFC A83](https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md#xds-cluster-metadata). This allows the xDS client to extract and validate cluster metadata to configure the audience used by GCP Authentication filter. ### Changes - **GCP Authn Support**: Added `audienceConverter` to parse `envoy.extensions.filters.http.gcp_authn.v3.Audience` protos. - **CDS Integration**: Updated `validateClusterAndConstructClusterUpdate` to invoke metadata validation. - **Validation**: Added strict validation for the `url` field; an empty URL in the audience metadata will now result in a NACK of the Cluster resource. - **Environment Variable**: Metadata parsing for GCP Authn is guarded by the `GCPAuthenticationFilterEnabled` environment variable. RELEASE NOTES: N/A
1 parent e80524d commit 39a15f6

7 files changed

Lines changed: 382 additions & 12 deletions

File tree

internal/envconfig/xds.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,9 @@ var (
8989
// filtered and prefix-propagated to the LRS server. For more details, see:
9090
// https://github.com/grpc/proposal/blob/master/A85-lrs-custom-metrics-changes.md
9191
XDSORCAToLRSPropEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_ORCA_LRS_PROPAGATION", false)
92+
93+
// GCPAuthenticationFilterEnabled enables the xDS GCP Authentication
94+
// filter. For more details, see:
95+
// https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md
96+
GCPAuthenticationFilterEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER", false)
9297
)

internal/xds/xdsclient/xdsresource/metadata.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,20 @@ import (
2121
"fmt"
2222
"net/netip"
2323

24-
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
2524
"google.golang.org/grpc/internal/envconfig"
2625
"google.golang.org/protobuf/types/known/anypb"
26+
27+
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
28+
v3gcpauthnpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3"
2729
)
2830

2931
func init() {
3032
if envconfig.XDSHTTPConnectEnabled {
3133
registerMetadataConverter("type.googleapis.com/envoy.config.core.v3.Address", proxyAddressConvertor{})
3234
}
35+
if envconfig.GCPAuthenticationFilterEnabled {
36+
registerMetadataConverter("type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience", audienceConverter{})
37+
}
3338
}
3439

3540
var (
@@ -100,3 +105,28 @@ func (proxyAddressConvertor) convert(anyProto *anypb.Any) (any, error) {
100105
}
101106
return ProxyAddressMetadataValue{Address: parseAddress(socketaddress)}, nil
102107
}
108+
109+
// AudienceMetadataValue holds the audience parsed from the
110+
// envoy.extensions.filters.http.gcp_authn.v3.Audience proto message, as
111+
// specified in gRFC A83.
112+
type AudienceMetadataValue struct {
113+
// Audience is the URL of the receiving service that performs token
114+
// authentication.
115+
Audience string
116+
}
117+
118+
// audienceConverter implements the metadataConverter interface to
119+
// handle the conversion of envoy.extensions.filters.http.gcp_authn.v3.Audience
120+
// protobuf messages into an internal representation.
121+
type audienceConverter struct{}
122+
123+
func (audienceConverter) convert(anyProto *anypb.Any) (any, error) {
124+
audienceProto := &v3gcpauthnpb.Audience{}
125+
if err := anyProto.UnmarshalTo(audienceProto); err != nil {
126+
return nil, fmt.Errorf("failed to unmarshal the envoy.extensions.filters.http.gcp_authn.v3.Audience resource from Any proto: %v", err)
127+
}
128+
if audienceProto.GetUrl() == "" {
129+
return nil, fmt.Errorf("empty url field in audience metadata")
130+
}
131+
return AudienceMetadataValue{Audience: audienceProto.GetUrl()}, nil
132+
}

internal/xds/xdsclient/xdsresource/metadata_test.go

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@ package xdsresource
1919
import (
2020
"testing"
2121

22-
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
2322
"github.com/google/go-cmp/cmp"
2423
"google.golang.org/grpc/internal/testutils"
24+
"google.golang.org/protobuf/types/known/anypb"
25+
26+
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
27+
v3gcpauthnpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/gcp_authn/v3"
2528
)
2629

27-
const proxyAddressTypeURL = "type.googleapis.com/envoy.config.core.v3.Address"
30+
const (
31+
proxyAddressTypeURL = "type.googleapis.com/envoy.config.core.v3.Address"
32+
audienceTypeURL = "type.googleapis.com/envoy.extensions.filters.http.gcp_authn.v3.Audience"
33+
)
2834

2935
func setupProxyAddressConverter(t *testing.T) {
3036
registerMetadataConverter(proxyAddressTypeURL, proxyAddressConvertor{})
@@ -33,6 +39,13 @@ func setupProxyAddressConverter(t *testing.T) {
3339
})
3440
}
3541

42+
func setupAudienceConverter(t *testing.T) {
43+
registerMetadataConverter(audienceTypeURL, audienceConverter{})
44+
t.Cleanup(func() {
45+
unregisterMetadataConverterForTesting(audienceTypeURL)
46+
})
47+
}
48+
3649
func (s) TestProxyAddressConverterSuccess(t *testing.T) {
3750
setupProxyAddressConverter(t)
3851
converter := metadataConverterForType(proxyAddressTypeURL)
@@ -45,7 +58,7 @@ func (s) TestProxyAddressConverterSuccess(t *testing.T) {
4558
want ProxyAddressMetadataValue
4659
}{
4760
{
48-
name: "valid IPv4 address and port",
61+
name: "valid_IPv4_address_and_port",
4962
addr: &v3corepb.Address{
5063
Address: &v3corepb.Address_SocketAddress{
5164
SocketAddress: &v3corepb.SocketAddress{
@@ -61,7 +74,7 @@ func (s) TestProxyAddressConverterSuccess(t *testing.T) {
6174
},
6275
},
6376
{
64-
name: "valid full IPv6 address and port",
77+
name: "valid_full_IPv6_address_and_port",
6578
addr: &v3corepb.Address{
6679
Address: &v3corepb.Address_SocketAddress{
6780
SocketAddress: &v3corepb.SocketAddress{
@@ -77,7 +90,7 @@ func (s) TestProxyAddressConverterSuccess(t *testing.T) {
7790
},
7891
},
7992
{
80-
name: "valid shortened IPv6 address",
93+
name: "valid_shortened_IPv6_address",
8194
addr: &v3corepb.Address{
8295
Address: &v3corepb.Address_SocketAddress{
8396
SocketAddress: &v3corepb.SocketAddress{
@@ -93,7 +106,7 @@ func (s) TestProxyAddressConverterSuccess(t *testing.T) {
93106
},
94107
},
95108
{
96-
name: "valid link-local IPv6 address",
109+
name: "valid_link-local_IPv6_address",
97110
addr: &v3corepb.Address{
98111
Address: &v3corepb.Address_SocketAddress{
99112
SocketAddress: &v3corepb.SocketAddress{
@@ -109,7 +122,7 @@ func (s) TestProxyAddressConverterSuccess(t *testing.T) {
109122
},
110123
},
111124
{
112-
name: "valid IPv4-mapped IPv6 address",
125+
name: "valid_IPv4-mapped_IPv6_address",
113126
addr: &v3corepb.Address{
114127
Address: &v3corepb.Address_SocketAddress{
115128
SocketAddress: &v3corepb.SocketAddress{
@@ -152,7 +165,7 @@ func (s) TestProxyAddressConverterFailure(t *testing.T) {
152165
wantErr string
153166
}{
154167
{
155-
name: "invalid address",
168+
name: "invalid_address",
156169
addr: &v3corepb.Address{
157170
Address: &v3corepb.Address_SocketAddress{
158171
SocketAddress: &v3corepb.SocketAddress{
@@ -163,14 +176,14 @@ func (s) TestProxyAddressConverterFailure(t *testing.T) {
163176
wantErr: "address field is not a valid IPv4 or IPv6 address: \"invalid-ip\"",
164177
},
165178
{
166-
name: "missing socket_address",
179+
name: "missing_socket_address",
167180
addr: &v3corepb.Address{
168181
// No SocketAddress field set.
169182
},
170183
wantErr: "no socket_address field in metadata",
171184
},
172185
{
173-
name: "address is not a socket address",
186+
name: "address_is_not_a_socket_address",
174187
addr: &v3corepb.Address{
175188
Address: &v3corepb.Address_EnvoyInternalAddress{
176189
EnvoyInternalAddress: &v3corepb.EnvoyInternalAddress{
@@ -183,7 +196,7 @@ func (s) TestProxyAddressConverterFailure(t *testing.T) {
183196
wantErr: "no socket_address field in metadata",
184197
},
185198
{
186-
name: "port value not set",
199+
name: "port_value_not_set",
187200
addr: &v3corepb.Address{
188201
Address: &v3corepb.Address_SocketAddress{
189202
SocketAddress: &v3corepb.SocketAddress{
@@ -206,3 +219,47 @@ func (s) TestProxyAddressConverterFailure(t *testing.T) {
206219
})
207220
}
208221
}
222+
223+
func (s) TestAudienceConverterSuccess(t *testing.T) {
224+
setupAudienceConverter(t)
225+
converter := metadataConverterForType(audienceTypeURL)
226+
if converter == nil {
227+
t.Fatalf("Converter for %q not found in registry", audienceTypeURL)
228+
}
229+
tests := []struct {
230+
name string
231+
audience *anypb.Any
232+
want AudienceMetadataValue
233+
wantErr string
234+
}{
235+
{
236+
name: "valid_audience",
237+
audience: testutils.MarshalAny(t, &v3gcpauthnpb.Audience{Url: "https://example.com"}),
238+
want: AudienceMetadataValue{Audience: "https://example.com"},
239+
},
240+
{
241+
name: "empty_audience",
242+
audience: testutils.MarshalAny(t, &v3gcpauthnpb.Audience{Url: ""}),
243+
want: AudienceMetadataValue{Audience: ""},
244+
wantErr: "empty url field in audience metadata",
245+
},
246+
}
247+
248+
for _, tt := range tests {
249+
t.Run(tt.name, func(t *testing.T) {
250+
got, err := converter.convert(tt.audience)
251+
if tt.wantErr != "" {
252+
if err == nil || err.Error() != tt.wantErr {
253+
t.Errorf("convert() got error = %v, wantErr = %q", err, tt.wantErr)
254+
}
255+
return
256+
}
257+
if err != nil {
258+
t.Fatalf("convert() failed with error: %v", err)
259+
}
260+
if diff := cmp.Diff(tt.want, got); diff != "" {
261+
t.Errorf("convert(%s) returned unexpected diff (-want +got):\n%s", tt.audience.GetTypeUrl(), diff)
262+
}
263+
})
264+
}
265+
}

internal/xds/xdsclient/xdsresource/type_cds.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ type ClusterUpdate struct {
8383
// LRSReportEndpointMetrics specifies the subset of ORCA metrics that
8484
// should be propagated to the LRS server.
8585
LRSReportEndpointMetrics *LRSReportEndpointMetricsConfig
86+
87+
// Metadata contains the metadata from the cluster resource.
88+
Metadata map[string]any
8689
}
8790

8891
// LRSReportEndpointMetricsConfig holds the configuration for propagating ORCA

internal/xds/xdsclient/xdsresource/unmarshal_cds.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,14 @@ func validateClusterAndConstructClusterUpdate(cluster *v3clusterpb.Cluster, serv
215215
}
216216
}
217217

218+
var metadata map[string]any
219+
if envconfig.GCPAuthenticationFilterEnabled {
220+
var err error
221+
if metadata, err = validateAndConstructMetadata(cluster.GetMetadata()); err != nil {
222+
return ClusterUpdate{}, err
223+
}
224+
}
225+
218226
ret := ClusterUpdate{
219227
ClusterName: cluster.GetName(),
220228
SecurityCfg: sc,
@@ -223,6 +231,7 @@ func validateClusterAndConstructClusterUpdate(cluster *v3clusterpb.Cluster, serv
223231
OutlierDetection: od,
224232
TelemetryLabels: telemetryLabels,
225233
LRSReportEndpointMetrics: lrsReportEndpointMetrics,
234+
Metadata: metadata,
226235
}
227236

228237
if lrs := cluster.GetLrsServer(); lrs != nil {

0 commit comments

Comments
 (0)