Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 32 additions & 20 deletions internal/xds/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"google.golang.org/grpc/credentials/tls/certprovider"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/xds/grpcservice/creds"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is reasonable for the bootstrap package to have a dependency on the something inside of the grpcservice package. It wouldn't be reasonable for this package to have a dependency on the grpcservice package as well.

"google.golang.org/grpc/xds/bootstrap"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
Expand Down Expand Up @@ -132,12 +133,13 @@ type AllowedGRPCService struct {
// callCredsConfigs is the list of call-credential configs from the
// bootstrap JSON. Kept for Equal and MarshalJSON.
callCredsConfigs []CallCredsConfig
// selectedChannelCreds is the first channel-creds entry whose type the
// client supports; it is the one used to build the side channel.
selectedChannelCreds ChannelCreds
// dialOptions are built from the selected channel and call credentials
// and passed to grpc.NewClient when creating the side channel.
dialOptions []grpc.DialOption
// sideChannelCreds is the credentials bundle built from the first
// channel-creds entry whose type the client supports, paired with its
// identity.
sideChannelCreds *creds.ChannelCreds
// sideCallCreds are the call credentials built from the supported
// call-creds configs, paired with their identities, preserving order.
sideCallCreds []*creds.CallCreds
// cleanups release resources (credential bundles, file watchers) built
// for this service; run when the owning Config is no longer needed.
cleanups []func()
Expand All @@ -148,10 +150,13 @@ func (a *AllowedGRPCService) TargetURI() string {
return a.targetURI
}

// DialOptions returns the dial options built from this service's selected
// channel and call credentials, for use when creating the side channel.
func (a *AllowedGRPCService) DialOptions() []grpc.DialOption {
return a.dialOptions
// SideChannelCredentials returns the channel and call credentials configured
// for this service, paired with their identities, for use when creating the
// side channel to it. The returned credentials are owned by the bootstrap
// config: their cleanups are nil, and the underlying resources are released
// via Cleanups when the config is no longer needed.
func (a *AllowedGRPCService) SideChannelCredentials() (*creds.ChannelCreds, []*creds.CallCreds) {
return a.sideChannelCreds, a.sideCallCreds
}

// Cleanups returns cleanups to run when the service is no longer needed.
Expand Down Expand Up @@ -237,8 +242,10 @@ func (a *AllowedGRPCService) UnmarshalJSON(data []byte) (err error) {
}
}()

var credsDialOption grpc.DialOption
var selectedChannelCreds ChannelCreds
// The built credentials are paired with their (JSON) identities but the
// pairs carry no cleanups: the resources built here are owned by the
// bootstrap config and released via the cleanups collected below.
var sideChannelCreds *creds.ChannelCreds
for _, cc := range jsonS.ChannelCreds {
c := bootstrap.GetChannelCredentials(cc.Type)
if c == nil {
Expand All @@ -248,19 +255,18 @@ func (a *AllowedGRPCService) UnmarshalJSON(data []byte) (err error) {
if err != nil {
return fmt.Errorf("xds: failed to build credentials bundle from bootstrap for allowed grpc service: type %q, err: %v", cc.Type, err)
}
selectedChannelCreds = cc
credsDialOption = grpc.WithCredentialsBundle(bundle)
sideChannelCreds = creds.NewChannelCreds(bundle, creds.NewJSONIdentity(cc.Type, cc.Config), nil)
cleanups = append(cleanups, cancel)
break
}

// If no channel-creds type in the list was supported, credsDialOption is
// If no channel-creds type in the list was supported, sideChannelCreds is
// still nil after the loop; that is a validation error.
if credsDialOption == nil {
if sideChannelCreds == nil {
return fmt.Errorf("xds: no supported channel credentials found for allowed grpc service in config:\n%s", string(data))
}
dialOptions := []grpc.DialOption{credsDialOption}

var sideCallCreds []*creds.CallCreds
for _, cfg := range jsonS.CallCredsConfigs {
c := bootstrap.GetCallCredentials(cfg.Type)
if c == nil {
Expand All @@ -270,14 +276,14 @@ func (a *AllowedGRPCService) UnmarshalJSON(data []byte) (err error) {
if err != nil {
return fmt.Errorf("xds: failed to build call credentials from bootstrap for allowed grpc service: type %q, err: %v", cfg.Type, err)
}
dialOptions = append(dialOptions, grpc.WithPerRPCCredentials(callCreds))
sideCallCreds = append(sideCallCreds, creds.NewCallCreds(callCreds, creds.NewJSONIdentity(cfg.Type, cfg.Config), nil))
cleanups = append(cleanups, cancel)
}

a.channelCreds = jsonS.ChannelCreds
a.callCredsConfigs = jsonS.CallCredsConfigs
a.selectedChannelCreds = selectedChannelCreds
a.dialOptions = dialOptions
a.sideChannelCreds = sideChannelCreds
a.sideCallCreds = sideCallCreds
a.cleanups = cleanups
return nil
}
Expand Down Expand Up @@ -643,6 +649,12 @@ func (c *Config) AllowedGRPCServices() AllowedGRPCServices {
return c.allowedGRPCServices
}

// AllowedGRPCService returns the allowed gRPC service configured for the
// given target URI, or nil if there is none.
func (c *Config) AllowedGRPCService(targetURI string) *AllowedGRPCService {
return c.allowedGRPCServices[targetURI]
}

// XDSServers returns the top-level list of management servers to connect to,
// ordered by priority.
func (c *Config) XDSServers() ServerConfigs {
Expand Down
51 changes: 33 additions & 18 deletions internal/xds/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/xds/grpcservice/creds"
"google.golang.org/grpc/xds/bootstrap"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/structpb"
Expand Down Expand Up @@ -1742,10 +1743,10 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) {
name string
json string
want *AllowedGRPCService
// Fields deliberately excluded from Equal: the selected channel
// creds and the dial options built from the credentials.
// The built credentials are deliberately excluded from Equal; verify
// them via SideChannelCredentials instead.
wantSelectedChannelCredsType string
wantDialOptions int
wantSideCallCreds int
}{
{
name: "insecure_channel_creds",
Expand All @@ -1755,7 +1756,7 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) {
channelCreds: []ChannelCreds{{Type: "insecure"}},
},
wantSelectedChannelCredsType: "insecure",
wantDialOptions: 1,
wantSideCallCreds: 0,
},
{
name: "with_call_creds",
Expand All @@ -1769,9 +1770,9 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) {
}},
},
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",
Expand All @@ -1785,8 +1786,8 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) {
},
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",
Expand All @@ -1806,9 +1807,9 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) {
},
},
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",
Expand All @@ -1818,7 +1819,7 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) {
channelCreds: []ChannelCreds{{Type: "tls", Config: json.RawMessage("{}")}},
},
wantSelectedChannelCredsType: "tls",
wantDialOptions: 1,
wantSideCallCreds: 0,
},
{
name: "skips_unsupported_channel_creds",
Expand All @@ -1828,7 +1829,7 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) {
channelCreds: []ChannelCreds{{Type: "unsupported_cred_type"}, {Type: "insecure"}},
},
wantSelectedChannelCredsType: "insecure",
wantDialOptions: 1,
wantSideCallCreds: 0,
},
}

Expand All @@ -1845,11 +1846,25 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) {
if !svc.Equal(test.want) {
t.Errorf("parsed service = %+v, want %+v", svc, test.want)
}
if got := svc.selectedChannelCreds.Type; got != test.wantSelectedChannelCredsType {
t.Errorf("selectedChannelCreds.Type = %q, want %q", got, test.wantSelectedChannelCredsType)
chanCreds, callCreds := svc.SideChannelCredentials()
if chanCreds == nil || chanCreds.Bundle() == nil {
t.Error("SideChannelCredentials() returned no built channel credentials")
}
if got := len(svc.DialOptions()); got != test.wantDialOptions {
t.Errorf("len(DialOptions()) = %d, want %d", got, test.wantDialOptions)
// The identity of the built channel credentials must match the
// first supported channel-creds entry from the bootstrap JSON.
var wantConfig json.RawMessage
for _, cc := range test.want.channelCreds {
if cc.Type == test.wantSelectedChannelCredsType {
wantConfig = cc.Config
break
}
}
wantIdentity := creds.NewChannelCreds(nil, creds.NewJSONIdentity(test.wantSelectedChannelCredsType, wantConfig), nil)
if !chanCreds.Equal(wantIdentity) {
t.Errorf("SideChannelCredentials() channel credentials identity = %+v, want type %q", chanCreds, test.wantSelectedChannelCredsType)
}
if got := len(callCreds); got != test.wantSideCallCreds {
t.Errorf("len(SideChannelCredentials() call creds) = %d, want %d", got, test.wantSideCallCreds)
}
})
}
Expand Down
71 changes: 71 additions & 0 deletions internal/xds/grpcservice/accesstokencreds/call_creds.go
Comment thread
easwars marked this conversation as resolved.
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 accesstokencreds implements static access token CallCredentials for
// xDS-configured side channels, as specified in gRFC A102.
package accesstokencreds

import (
"context"
"encoding/json"
"fmt"

"google.golang.org/grpc/credentials"
)

// NewCallCredentials returns call credentials that attach a static bearer
// token to outgoing RPCs. The config must be a JSON object of the form
// {"token": <non-empty string>}.
//
// The credentials require transport security: the token is only ever sent on
// connections that provide privacy and integrity, and RPCs on weaker
// connections fail.
func NewCallCredentials(configJSON json.RawMessage) (credentials.PerRPCCredentials, error) {
var cfg struct {
Token string `json:"token"`
}
if err := json.Unmarshal(configJSON, &cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal access token call credentials config: %v", err)
}
if cfg.Token == "" {
return nil, fmt.Errorf("token is required in access token call credentials config")
}
return &callCreds{token: cfg.Token}, nil
}

// callCreds implements credentials.PerRPCCredentials by attaching a static
// bearer token to each RPC.
type callCreds struct {
token string
}

// GetRequestMetadata returns the token as an authorization header. It fails
// if the connection does not provide privacy and integrity.
func (c *callCreds) 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("unable to transfer access token PerRPCCredentials: %v", err)
}
return map[string]string{"authorization": "Bearer " + c.token}, nil
}

// RequireTransportSecurity indicates whether the credentials requires
// transport security.
func (c *callCreds) RequireTransportSecurity() bool {
return true
}
Loading
Loading