Skip to content

Commit d75cfcc

Browse files
committed
filter wiring for A102
1 parent e020cf6 commit d75cfcc

20 files changed

Lines changed: 857 additions & 223 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
*
3+
* Copyright 2026 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
// Package accesstokencreds implements static access token CallCredentials for
20+
// xDS-configured side channels, as specified in gRFC A102.
21+
package accesstokencreds
22+
23+
import (
24+
"context"
25+
"encoding/json"
26+
"fmt"
27+
28+
"google.golang.org/grpc/credentials"
29+
)
30+
31+
// NewCallCredentials returns call credentials that attach a static bearer
32+
// token to outgoing RPCs. The config must be a JSON object of the form
33+
// {"token": <non-empty string>}.
34+
//
35+
// The caller is expected to invoke the cancel function when they are done
36+
// using the returned call creds. This cancel function is idempotent.
37+
func NewCallCredentials(configJSON json.RawMessage) (credentials.PerRPCCredentials, func(), error) {
38+
var cfg struct {
39+
Token string `json:"token"`
40+
}
41+
emptyFn := func() {}
42+
43+
if err := json.Unmarshal(configJSON, &cfg); err != nil {
44+
return nil, emptyFn, fmt.Errorf("failed to unmarshal access token call credentials config: %v", err)
45+
}
46+
if cfg.Token == "" {
47+
return nil, emptyFn, fmt.Errorf("token is required in access token call credentials config")
48+
}
49+
return &callCreds{token: cfg.Token}, emptyFn, nil
50+
}
51+
52+
// callCreds implements credentials.PerRPCCredentials by attaching a static
53+
// bearer token to each RPC.
54+
type callCreds struct {
55+
token string
56+
}
57+
58+
// GetRequestMetadata returns the token as an authorization header, but only
59+
// when the connection provides privacy and integrity. On weaker connections
60+
// the token is withheld without failing the RPC, as per gRFC A102.
61+
func (c *callCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
62+
ri, ok := credentials.RequestInfoFromContext(ctx)
63+
if !ok || credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity) != nil {
64+
return nil, nil
65+
}
66+
return map[string]string{"authorization": "Bearer " + c.token}, nil
67+
}
68+
69+
// RequireTransportSecurity returns false. The credentials may be used on any
70+
// connection, but GetRequestMetadata withholds the token on connections that
71+
// do not provide privacy and integrity.
72+
func (c *callCreds) RequireTransportSecurity() bool {
73+
return false
74+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
*
3+
* Copyright 2026 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package accesstokencreds
20+
21+
import (
22+
"context"
23+
"encoding/json"
24+
"testing"
25+
"time"
26+
27+
"google.golang.org/grpc/credentials"
28+
"google.golang.org/grpc/internal/grpctest"
29+
)
30+
31+
type s struct {
32+
grpctest.Tester
33+
}
34+
35+
func Test(t *testing.T) {
36+
grpctest.RunSubTests(t, s{})
37+
}
38+
39+
func (s) TestNewCallCredentialsWithInvalidConfig(t *testing.T) {
40+
tests := []struct {
41+
name string
42+
config string
43+
}{
44+
{
45+
name: "not_an_object",
46+
config: `""`,
47+
},
48+
{
49+
name: "empty_config",
50+
config: `{}`,
51+
},
52+
{
53+
name: "empty_token",
54+
config: `{"token": ""}`,
55+
},
56+
}
57+
58+
for _, tt := range tests {
59+
t.Run(tt.name, func(t *testing.T) {
60+
callCreds, cleanup, err := NewCallCredentials(json.RawMessage(tt.config))
61+
if err == nil {
62+
t.Fatalf("NewCallCredentials(%s): got nil, want error", tt.config)
63+
}
64+
if callCreds != nil {
65+
t.Errorf("NewCallCredentials(%s): returned non-nil call credentials", tt.config)
66+
}
67+
if cleanup == nil {
68+
t.Errorf("NewCallCredentials(%s): returned nil cleanup function", tt.config)
69+
}
70+
})
71+
}
72+
}
73+
74+
// Tests that the token is attached as a bearer authorization header on
75+
// connections providing privacy and integrity, and is withheld without error
76+
// on weaker connections.
77+
func (s) TestGetRequestMetadata(t *testing.T) {
78+
const config = `{"token": "test-token"}`
79+
callCreds, cleanup, err := NewCallCredentials(json.RawMessage(config))
80+
if err != nil {
81+
t.Fatalf("NewCallCredentials(%s) failed: %v", config, err)
82+
}
83+
defer cleanup()
84+
85+
if callCreds.RequireTransportSecurity() {
86+
t.Error("RequireTransportSecurity() = true, want false")
87+
}
88+
89+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
90+
defer cancel()
91+
92+
// The token must be attached on a connection with privacy and integrity.
93+
secureCtx := credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
94+
AuthInfo: &testAuthInfo{secLevel: credentials.PrivacyAndIntegrity},
95+
})
96+
md, err := callCreds.GetRequestMetadata(secureCtx)
97+
if err != nil {
98+
t.Fatalf("GetRequestMetadata() on a secure connection failed: %v", err)
99+
}
100+
if got, want := md["authorization"], "Bearer test-token"; got != want {
101+
t.Fatalf("GetRequestMetadata() on a secure connection returned authorization header %q, want %q", got, want)
102+
}
103+
104+
// The token must be withheld, without error, on an insecure connection.
105+
insecureCtx := credentials.NewContextWithRequestInfo(ctx, credentials.RequestInfo{
106+
AuthInfo: &testAuthInfo{secLevel: credentials.NoSecurity},
107+
})
108+
md, err = callCreds.GetRequestMetadata(insecureCtx)
109+
if err != nil {
110+
t.Fatalf("GetRequestMetadata() on an insecure connection failed: %v", err)
111+
}
112+
if len(md) != 0 {
113+
t.Fatalf("GetRequestMetadata() on an insecure connection returned metadata %v, want none", md)
114+
}
115+
}
116+
117+
// testAuthInfo implements credentials.AuthInfo for testing.
118+
type testAuthInfo struct {
119+
secLevel credentials.SecurityLevel
120+
}
121+
122+
func (t *testAuthInfo) AuthType() string {
123+
return "test"
124+
}
125+
126+
func (t *testAuthInfo) GetCommonAuthInfo() credentials.CommonAuthInfo {
127+
return credentials.CommonAuthInfo{SecurityLevel: t.secLevel}
128+
}

internal/xds/grpcservice/grpcservice.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,13 @@ func (g *GrpcService) Parse(gs *v3corepb.GrpcService) (Config, error) {
115115
// The target must be present in the allowed_grpc_services
116116
// allowlist, but the credentials themselves are resolved later,
117117
// at channel creation time; they are left empty in the parsed
118-
// config here.
119-
allowedSvc, ok := g.config.AllowedGRPCService(targetURI)
118+
// config here. A nil bootstrap config has no allowlist, so all
119+
// targets are rejected.
120+
var allowedSvc *bootstrap.AllowedGRPCService
121+
var ok bool
122+
if g.config != nil {
123+
allowedSvc, ok = g.config.AllowedGRPCService(targetURI)
124+
}
120125
if !ok {
121126
return Config{}, fmt.Errorf("grpcservice: target_uri %q is not present in allowed_grpc_services", targetURI)
122127
}

internal/xds/httpfilter/ext_authz/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ package extauthz
2020

2121
import (
2222
"google.golang.org/grpc/codes"
23+
"google.golang.org/grpc/internal/xds/grpcservice"
2324
"google.golang.org/grpc/internal/xds/httpfilter"
2425
"google.golang.org/grpc/internal/xds/matcher"
25-
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
2626
)
2727

2828
// config contains the configuration for the external authorization filter.
2929
type config struct {
3030
httpfilter.FilterConfig
3131
// grpcService is the configuration for the external authorization server.
32-
grpcService xdsresource.GRPCServiceConfig
32+
grpcService grpcservice.Config
3333
// filterEnabled specifies the percentage of requests to be authorized by
3434
// the external authorization server.
3535
filterEnabled fraction

internal/xds/httpfilter/ext_authz/ext_authz.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ import (
2626
"google.golang.org/grpc/codes"
2727
"google.golang.org/grpc/internal/envconfig"
2828
"google.golang.org/grpc/internal/transport"
29+
"google.golang.org/grpc/internal/xds/grpcservice"
2930
"google.golang.org/grpc/internal/xds/httpfilter"
3031
"google.golang.org/grpc/internal/xds/matcher"
31-
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
3232
"google.golang.org/protobuf/proto"
3333
"google.golang.org/protobuf/types/known/anypb"
3434

@@ -44,9 +44,10 @@ func init() {
4444
}
4545

4646
var (
47-
// TODO: Remove this once gRFC A102 is implemented.
48-
parseGRPCServiceConfig = func(*v3corepb.GrpcService) (xdsresource.GRPCServiceConfig, error) {
49-
return xdsresource.GRPCServiceConfig{}, fmt.Errorf("parseGRPCServiceConfig not implemented")
47+
// TODO: Parse via grpcservice.GrpcService with the filter parse context,
48+
// as ext_proc does, when ext_authz is wired up for gRFC A102.
49+
parseGRPCServiceConfig = func(*v3corepb.GrpcService) (grpcservice.Config, error) {
50+
return grpcservice.Config{}, fmt.Errorf("parseGRPCServiceConfig not implemented")
5051
}
5152
)
5253

internal/xds/httpfilter/ext_authz/ext_authz_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import (
2828
"google.golang.org/grpc/codes"
2929
"google.golang.org/grpc/internal/grpctest"
3030
"google.golang.org/grpc/internal/testutils"
31+
"google.golang.org/grpc/internal/xds/grpcservice"
3132
"google.golang.org/grpc/internal/xds/httpfilter"
3233
"google.golang.org/grpc/internal/xds/matcher"
33-
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
3434
"google.golang.org/protobuf/proto"
3535
"google.golang.org/protobuf/types/known/anypb"
3636
"google.golang.org/protobuf/types/known/wrapperspb"
@@ -53,18 +53,18 @@ func Test(t *testing.T) {
5353
// testParseGRPCServiceConfig is a helper function that parses a GrpcService
5454
// proto message into a GRPCServiceConfig. This is a temporary test
5555
// implementation that will be removed once gRFC A102 is implemented.
56-
func testParseGRPCServiceConfig(grpcService *corepb.GrpcService) (xdsresource.GRPCServiceConfig, error) {
56+
func testParseGRPCServiceConfig(grpcService *corepb.GrpcService) (grpcservice.Config, error) {
5757
if grpcService == nil {
58-
return xdsresource.GRPCServiceConfig{}, nil
58+
return grpcservice.Config{}, nil
5959
}
6060
if grpcService.GetGoogleGrpc() == nil {
61-
return xdsresource.GRPCServiceConfig{}, fmt.Errorf("only google_grpc grpc_service is supported")
61+
return grpcservice.Config{}, fmt.Errorf("only google_grpc grpc_service is supported")
6262
}
6363
if grpcService.GetGoogleGrpc().GetTargetUri() == "" {
64-
return xdsresource.GRPCServiceConfig{}, fmt.Errorf("targetURI must be a non-empty string")
64+
return grpcservice.Config{}, fmt.Errorf("targetURI must be a non-empty string")
6565
}
6666

67-
sc := xdsresource.GRPCServiceConfig{
67+
sc := grpcservice.Config{
6868
TargetURI: grpcService.GetGoogleGrpc().GetTargetUri(),
6969
}
7070
return sc, nil
@@ -73,7 +73,7 @@ func testParseGRPCServiceConfig(grpcService *corepb.GrpcService) (xdsresource.GR
7373
var cmpOpts = []cmp.Option{
7474
cmp.AllowUnexported(
7575
config{},
76-
xdsresource.GRPCServiceConfig{},
76+
grpcservice.Config{},
7777
fraction{},
7878
),
7979
cmp.Transformer("RegexpToString", func(r *regexp.Regexp) string {
@@ -115,7 +115,7 @@ func (s) TestParseFilterConfig_Success(t *testing.T) {
115115
},
116116
}),
117117
wantCfg: config{
118-
grpcService: xdsresource.GRPCServiceConfig{
118+
grpcService: grpcservice.Config{
119119
TargetURI: "localhost:1234",
120120
},
121121
filterEnabled: fraction{
@@ -171,7 +171,7 @@ func (s) TestParseFilterConfig_Success(t *testing.T) {
171171
IncludePeerCertificate: true,
172172
}),
173173
wantCfg: config{
174-
grpcService: xdsresource.GRPCServiceConfig{
174+
grpcService: grpcservice.Config{
175175
TargetURI: "localhost:5678",
176176
},
177177
filterEnabled: fraction{

internal/xds/httpfilter/extproc/config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ import (
2222
"time"
2323

2424
"google.golang.org/grpc/internal/optional"
25+
"google.golang.org/grpc/internal/xds/grpcservice"
2526
"google.golang.org/grpc/internal/xds/httpfilter"
2627
"google.golang.org/grpc/internal/xds/matcher"
27-
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
2828

2929
v3procfilterpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_proc/v3"
3030
)
@@ -38,7 +38,7 @@ type baseConfig struct {
3838
// config. If both are set, the override config will be used.
3939

4040
// server is the configuration for the external processing server.
41-
server xdsresource.GRPCServiceConfig
41+
server grpcservice.Config
4242
// processingModes specifies the processing mode for each dataplane event.
4343
processingModes processingModes
4444
// failureModeAllow specifies the behavior when the RPC to the external
@@ -90,7 +90,7 @@ type baseConfig struct {
9090
// base config.
9191
type overrideConfig struct {
9292
httpfilter.FilterConfig
93-
server optional.Optional[xdsresource.GRPCServiceConfig]
93+
server optional.Optional[grpcservice.Config]
9494
processingModes optional.Optional[processingModes]
9595
failureModeAllow optional.Optional[bool]
9696
requestAttributes []string

0 commit comments

Comments
 (0)