-
Notifications
You must be signed in to change notification settings - Fork 4.8k
xds: Implement GrpcService parsing and shared side channels (gRFC A102) #9319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
e0b6447
fef7829
9662c23
979d438
d3b9bc9
7bdfc36
f5b2491
7e9e127
206b758
3519e1d
651d85a
395a24c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ import ( | |
| "google.golang.org/grpc/internal/envconfig" | ||
| "google.golang.org/grpc/internal/grpctest" | ||
| "google.golang.org/grpc/internal/testutils" | ||
| xdscreds "google.golang.org/grpc/internal/xds/credentials" | ||
| "google.golang.org/grpc/xds/bootstrap" | ||
| "google.golang.org/protobuf/testing/protocmp" | ||
| "google.golang.org/protobuf/types/known/structpb" | ||
|
|
@@ -1148,6 +1149,13 @@ func (s) TestGetConfiguration_Federation(t *testing.T) { | |
| Type: "jwt_token_file", | ||
| Config: json.RawMessage("{\n\"jwt_token_file\": \"/var/run/secrets/tokens/istio-token\"\n}"), | ||
| }}, | ||
| // Equal compares the built credentials by identity, so | ||
| // the fixture carries identity-only pairs. | ||
| sideChannelCreds: xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "insecure"}, nil), | ||
| sideCallCreds: []*xdscreds.CallCreds{xdscreds.NewCallCreds(nil, xdscreds.Identity{ | ||
| Type: "jwt_token_file", | ||
| Data: json.RawMessage("{\n\"jwt_token_file\": \"/var/run/secrets/tokens/istio-token\"\n}"), | ||
| }, nil)}, | ||
| }, | ||
| }, | ||
| }, | ||
|
|
@@ -1741,94 +1749,54 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) { | |
| tests := []struct { | ||
| name string | ||
| json string | ||
| want *AllowedGRPCService | ||
| // Fields deliberately excluded from Equal: the selected channel | ||
| // creds and the dial options built from the credentials. | ||
| wantSelectedChannelCredsType string | ||
| wantDialOptions int | ||
| // The identity of the built channel credentials must match the first | ||
| // supported channel-creds entry from the bootstrap JSON. | ||
| wantSelectedChannelCredsType string | ||
| wantSelectedChannelCredsConfig json.RawMessage | ||
| wantSideCallCreds int | ||
| }{ | ||
| { | ||
| name: "insecure_channel_creds", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}]}}`, | ||
| want: &AllowedGRPCService{ | ||
| targetURI: target, | ||
| channelCreds: []ChannelCreds{{Type: "insecure"}}, | ||
| }, | ||
| name: "insecure_channel_creds", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}]}}`, | ||
| wantSelectedChannelCredsType: "insecure", | ||
| wantDialOptions: 1, | ||
| wantSideCallCreds: 0, | ||
| }, | ||
| { | ||
| name: "with_call_creds", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}], "call_creds": [{"type": "jwt_token_file", "config": {"jwt_token_file": "/var/run/secrets/tokens/istio-token"}}]}}`, | ||
| want: &AllowedGRPCService{ | ||
| targetURI: target, | ||
| channelCreds: []ChannelCreds{{Type: "insecure"}}, | ||
| callCredsConfigs: []CallCredsConfig{{ | ||
| Type: "jwt_token_file", | ||
| Config: json.RawMessage(`{"jwt_token_file": "/var/run/secrets/tokens/istio-token"}`), | ||
| }}, | ||
| }, | ||
| name: "with_call_creds", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}], "call_creds": [{"type": "jwt_token_file", "config": {"jwt_token_file": "/var/run/secrets/tokens/istio-token"}}]}}`, | ||
| wantSelectedChannelCredsType: "insecure", | ||
| // One channel-creds dial option plus one per-RPC call-creds | ||
| // option. | ||
| wantDialOptions: 2, | ||
| // One call credential is built for the supported call-creds | ||
| // config. | ||
| wantSideCallCreds: 1, | ||
| }, | ||
| { | ||
| name: "unsupported_call_creds_skipped", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}], "call_creds": [{"type": "unsupported_call_creds_type"}]}}`, | ||
| want: &AllowedGRPCService{ | ||
| targetURI: target, | ||
| channelCreds: []ChannelCreds{{Type: "insecure"}}, | ||
| callCredsConfigs: []CallCredsConfig{{ | ||
| Type: "unsupported_call_creds_type", | ||
| }}, | ||
| }, | ||
| name: "unsupported_call_creds_skipped", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}], "call_creds": [{"type": "unsupported_call_creds_type"}]}}`, | ||
| wantSelectedChannelCredsType: "insecure", | ||
| // Unsupported call-creds types are skipped without error, so | ||
| // only the channel-creds dial option is built. | ||
| wantDialOptions: 1, | ||
| // no call credentials are built. | ||
| wantSideCallCreds: 0, | ||
| }, | ||
| { | ||
| name: "multiple_supported_call_creds", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}], "call_creds": [{"type": "jwt_token_file", "config": {"jwt_token_file": "/tokens/token-one"}}, {"type": "jwt_token_file", "config": {"jwt_token_file": "/tokens/token-two"}}]}}`, | ||
| want: &AllowedGRPCService{ | ||
| targetURI: target, | ||
| channelCreds: []ChannelCreds{{Type: "insecure"}}, | ||
| callCredsConfigs: []CallCredsConfig{ | ||
| { | ||
| Type: "jwt_token_file", | ||
| Config: json.RawMessage(`{"jwt_token_file": "/tokens/token-one"}`), | ||
| }, | ||
| { | ||
| Type: "jwt_token_file", | ||
| Config: json.RawMessage(`{"jwt_token_file": "/tokens/token-two"}`), | ||
| }, | ||
| }, | ||
| }, | ||
| name: "multiple_supported_call_creds", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}], "call_creds": [{"type": "jwt_token_file", "config": {"jwt_token_file": "/tokens/token-one"}}, {"type": "jwt_token_file", "config": {"jwt_token_file": "/tokens/token-two"}}]}}`, | ||
| wantSelectedChannelCredsType: "insecure", | ||
| // One channel-creds dial option plus one per-RPC option for | ||
| // each supported call credential. | ||
| wantDialOptions: 3, | ||
| // One call credential is built for each supported call-creds | ||
| // config. | ||
| wantSideCallCreds: 2, | ||
| }, | ||
| { | ||
| name: "tls_channel_creds", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "tls", "config": {}}]}}`, | ||
| want: &AllowedGRPCService{ | ||
| targetURI: target, | ||
| channelCreds: []ChannelCreds{{Type: "tls", Config: json.RawMessage("{}")}}, | ||
| }, | ||
| wantSelectedChannelCredsType: "tls", | ||
| wantDialOptions: 1, | ||
| name: "tls_channel_creds", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "tls", "config": {}}]}}`, | ||
| wantSelectedChannelCredsType: "tls", | ||
| wantSelectedChannelCredsConfig: json.RawMessage("{}"), | ||
| wantSideCallCreds: 0, | ||
| }, | ||
| { | ||
| name: "skips_unsupported_channel_creds", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "unsupported_cred_type"}, {"type": "insecure"}]}}`, | ||
| want: &AllowedGRPCService{ | ||
| targetURI: target, | ||
| channelCreds: []ChannelCreds{{Type: "unsupported_cred_type"}, {Type: "insecure"}}, | ||
| }, | ||
| name: "skips_unsupported_channel_creds", | ||
| json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "unsupported_cred_type"}, {"type": "insecure"}]}}`, | ||
| wantSelectedChannelCredsType: "insecure", | ||
| wantDialOptions: 1, | ||
| wantSideCallCreds: 0, | ||
| }, | ||
| } | ||
|
|
||
|
|
@@ -1842,14 +1810,19 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) { | |
| if !ok { | ||
| t.Fatalf("AllowedGRPCServices missing key %q", target) | ||
| } | ||
| if !svc.Equal(test.want) { | ||
| t.Errorf("parsed service = %+v, want %+v", svc, test.want) | ||
| if svc.TargetURI() != target { | ||
| t.Errorf("TargetURI() = %q, want %q", svc.TargetURI(), target) | ||
| } | ||
| chanCreds, callCreds := svc.SideChannelCredentials() | ||
| if chanCreds == nil || chanCreds.Bundle() == nil { | ||
| t.Error("SideChannelCredentials() returned no built channel credentials") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Maybe for completeness sake, add a field to the test table which says whether we expect call creds (or the number of call creds to expect) and verify here that call creds were in fact being built with the expected number.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| } | ||
| if got := svc.selectedChannelCreds.Type; got != test.wantSelectedChannelCredsType { | ||
| t.Errorf("selectedChannelCreds.Type = %q, want %q", got, test.wantSelectedChannelCredsType) | ||
| wantIdentity := xdscreds.Identity{Type: test.wantSelectedChannelCredsType, Data: test.wantSelectedChannelCredsConfig} | ||
| if wantChanCreds := xdscreds.NewChannelCreds(nil, wantIdentity, nil); !chanCreds.Equal(wantChanCreds) { | ||
| t.Errorf("SideChannelCredentials() channel credentials = %+v, want identity %+v", chanCreds, wantIdentity) | ||
| } | ||
| if got := len(svc.DialOptions()); got != test.wantDialOptions { | ||
| t.Errorf("len(DialOptions()) = %d, want %d", got, test.wantDialOptions) | ||
| if got := len(callCreds); got != test.wantSideCallCreds { | ||
| t.Errorf("len(SideChannelCredentials() call creds) = %d, want %d", got, test.wantSideCallCreds) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| /* | ||
| * | ||
| * Copyright 2026 gRPC authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| * | ||
| */ | ||
|
|
||
| package credentials | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "google.golang.org/grpc/credentials" | ||
| "google.golang.org/protobuf/proto" | ||
| "google.golang.org/protobuf/types/known/anypb" | ||
|
|
||
| accesstokenpb "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/call_credentials/access_token/v3" | ||
| ) | ||
|
|
||
| const accessTokenCredsTypeURL = "type.googleapis.com/envoy.extensions.grpc_service.call_credentials.access_token.v3.AccessTokenCredentials" | ||
|
|
||
| func init() { | ||
| RegisterCallCredsBuilder(accessTokenCredsTypeURL, func(config *anypb.Any) (credentials.PerRPCCredentials, func(), error) { | ||
| var accessToken accesstokenpb.AccessTokenCredentials | ||
| if err := anypb.UnmarshalTo(config, &accessToken, proto.UnmarshalOptions{}); err != nil { | ||
| return nil, nil, fmt.Errorf("credentials: failed to unmarshal AccessTokenCredentials: %v", err) | ||
| } | ||
| if accessToken.GetToken() == "" { | ||
| return nil, nil, fmt.Errorf("credentials: access token must be non-empty") | ||
| } | ||
| // These credentials hold no resources; the no-op cleanup satisfies | ||
| // the registry contract. | ||
| return &accessTokenCallCreds{token: accessToken.GetToken()}, func() {}, nil | ||
| }) | ||
| } | ||
|
|
||
| // accessTokenCallCreds implements credentials.PerRPCCredentials by attaching | ||
| // a static bearer token to each RPC (gRFC A102). The credentials require | ||
| // transport security: the token is only ever sent on connections that provide | ||
| // privacy and integrity, and RPCs on weaker connections fail. | ||
| type accessTokenCallCreds struct { | ||
| token string | ||
| } | ||
|
|
||
| // GetRequestMetadata returns the token as an authorization header. It fails | ||
| // if the connection does not provide privacy and integrity. | ||
| func (c *accessTokenCallCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) { | ||
| ri, _ := credentials.RequestInfoFromContext(ctx) | ||
| if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil { | ||
| return nil, fmt.Errorf("credentials: unable to transfer access token PerRPCCredentials: %v", err) | ||
| } | ||
| return map[string]string{"authorization": "Bearer " + c.token}, nil | ||
| } | ||
|
|
||
| // RequireTransportSecurity indicates whether the credentials require | ||
| // transport security. | ||
| func (c *accessTokenCallCreds) RequireTransportSecurity() bool { | ||
| return true | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.