-
Notifications
You must be signed in to change notification settings - Fork 4.6k
credentials, transport, grpc : add a call option to override the :authority header on a per-RPC basis #8068
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 14 commits
6840baf
11d96c1
d5b5af1
34175aa
4791ac7
ac318de
d1ea438
72d132a
b1583a6
74e7a8a
9b81586
515fad1
9e48a2a
c5aac00
e3e6aab
24b408e
9777548
b9e93d0
4efe178
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 |
|---|---|---|
| @@ -0,0 +1,275 @@ | ||
| /* | ||
| * | ||
| * Copyright 2025 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_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/tls" | ||
| "fmt" | ||
| "log" | ||
| "net" | ||
| "testing" | ||
|
|
||
| "google.golang.org/grpc" | ||
| "google.golang.org/grpc/codes" | ||
| "google.golang.org/grpc/credentials" | ||
| "google.golang.org/grpc/credentials/insecure" | ||
| "google.golang.org/grpc/internal/stubserver" | ||
| "google.golang.org/grpc/metadata" | ||
| "google.golang.org/grpc/status" | ||
| "google.golang.org/grpc/testdata" | ||
|
|
||
| testgrpc "google.golang.org/grpc/interop/grpc_testing" | ||
| testpb "google.golang.org/grpc/interop/grpc_testing" | ||
| ) | ||
|
|
||
| var cert tls.Certificate | ||
| var creds credentials.TransportCredentials | ||
easwars marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| func init() { | ||
easwars marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| var err error | ||
| cert, err = tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem")) | ||
| if err != nil { | ||
| log.Fatalf("failed to load key pair: %s", err) | ||
eshitachandwani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| creds, err = credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "x.test.example.com") | ||
| if err != nil { | ||
| log.Fatalf("Failed to create credentials %v", err) | ||
| } | ||
| } | ||
|
|
||
| func authorityChecker(ctx context.Context, wantAuthority string) (*testpb.Empty, error) { | ||
easwars marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| md, ok := metadata.FromIncomingContext(ctx) | ||
| if !ok { | ||
| return nil, status.Error(codes.InvalidArgument, "failed to parse metadata") | ||
| } | ||
| auths, ok := md[":authority"] | ||
| if !ok { | ||
| return nil, status.Error(codes.InvalidArgument, "no authority header") | ||
| } | ||
| if len(auths) != 1 { | ||
| return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("no authority header, auths = %v", auths)) | ||
easwars marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| if auths[0] != wantAuthority { | ||
| return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid authority header %v, want %v", auths[0], wantAuthority)) | ||
easwars marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| return &testpb.Empty{}, nil | ||
| } | ||
|
|
||
| // Tests the grpc.CallAuthority option with TLS credentials. This test verifies | ||
| // that the provided authority is correctly propagated to the server when using | ||
| // TLS. It covers both positive and negative cases: correct authority and | ||
eshitachandwani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // incorrect authority, expecting the RPC to fail with `UNAVAILABLE` status code | ||
| // error in the later case. | ||
| func TestAuthorityCallOptionsWithTLSCreds(t *testing.T) { | ||
easwars marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| tests := []struct { | ||
| name string | ||
| wantAuth string | ||
easwars marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| wantStatus codes.Code | ||
| }{ | ||
| { | ||
| name: "CorrectAuthority", | ||
| wantAuth: "auth.test.example.com", | ||
| wantStatus: codes.OK, | ||
| }, | ||
| { | ||
| name: "IncorrectAuthority", | ||
| wantAuth: "auth.example.com", | ||
| wantStatus: codes.Unavailable, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| ss := &stubserver.StubServer{ | ||
| EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) { | ||
| return authorityChecker(ctx, tt.wantAuth) | ||
| }, | ||
| } | ||
| if err := ss.StartServer(grpc.Creds(credentials.NewServerTLSFromCert(&cert))); err != nil { | ||
| t.Fatalf("Error starting endpoint server: %v", err) | ||
| } | ||
| defer ss.Stop() | ||
|
|
||
| cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(creds)) | ||
| if err != nil { | ||
| t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err) | ||
| } | ||
| defer cc.Close() | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) | ||
| defer cancel() | ||
|
|
||
| if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(tt.wantAuth)); status.Code(err) != tt.wantStatus { | ||
| t.Fatalf("EmptyCall() returned status %v, want %v", status.Code(err), tt.wantStatus) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // Tests the scenario where the `grpc.CallAuthority` call option is used with | ||
| // insecure transport credentials. The test verifies that the specified | ||
| // authority is correctly propagated to the server. | ||
| func (s) TestAuthorityCallOptionWithInsecureCreds(t *testing.T) { | ||
| const wantAuthority = "test.server.name" | ||
|
|
||
| ss := &stubserver.StubServer{ | ||
| EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) { | ||
| return authorityChecker(ctx, wantAuthority) | ||
| }, | ||
| } | ||
| if err := ss.Start(nil); err != nil { | ||
| t.Fatalf("Error starting endpoint server: %v", err) | ||
| } | ||
| defer ss.Stop() | ||
|
|
||
| cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(insecure.NewCredentials())) | ||
| if err != nil { | ||
| t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err) | ||
| } | ||
| defer cc.Close() | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) | ||
| defer cancel() | ||
| if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(wantAuthority)); err != nil { | ||
| t.Fatalf("EmptyCall() rpc failed: %v", err) | ||
| } | ||
| } | ||
|
|
||
| // testAuthInfoNoValidator implements only credentials.AuthInfo and not | ||
| // credentials.AuthorityValidator. | ||
| type testAuthInfoNoValidator struct{} | ||
|
|
||
| // AuthType returns the authentication type. | ||
| func (testAuthInfoNoValidator) AuthType() string { | ||
| return "test" | ||
| } | ||
|
|
||
| // testAuthInfoWithValidator implements both credentials.AuthInfo and | ||
| // credentials.AuthorityValidator. | ||
| type testAuthInfoWithValidator struct{} | ||
|
|
||
| // AuthType returns the authentication type. | ||
| func (testAuthInfoWithValidator) AuthType() string { | ||
| return "test" | ||
| } | ||
|
|
||
| // ValidateAuthority implements credentials.AuthorityValidator. | ||
| func (testAuthInfoWithValidator) ValidateAuthority(authority string) error { | ||
| if authority == "auth.test.example.com" { | ||
easwars marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return nil | ||
| } | ||
| return fmt.Errorf("invalid authority") | ||
|
||
| } | ||
|
|
||
| // testCreds is a test TransportCredentials that can optionally support | ||
| // authority validation. | ||
| type testCreds struct { | ||
| WithValidator bool | ||
easwars marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
eshitachandwani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // ClientHandshake performs the client-side handshake. | ||
| func (c *testCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { | ||
| if c.WithValidator { | ||
| return rawConn, testAuthInfoWithValidator{}, nil | ||
| } | ||
| return rawConn, testAuthInfoNoValidator{}, nil | ||
| } | ||
|
|
||
| // ServerHandshake performs the server-side handshake. | ||
| func (c *testCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { | ||
| if c.WithValidator { | ||
| return rawConn, testAuthInfoWithValidator{}, nil | ||
| } | ||
| return rawConn, testAuthInfoNoValidator{}, nil | ||
| } | ||
|
|
||
| // Clone creates a copy of testCreds. | ||
| func (c *testCreds) Clone() credentials.TransportCredentials { | ||
| return &testCreds{WithValidator: c.WithValidator} | ||
| } | ||
|
|
||
| // Info provides protocol information. | ||
| func (c *testCreds) Info() credentials.ProtocolInfo { | ||
| return credentials.ProtocolInfo{} | ||
| } | ||
|
|
||
| // OverrideServerName overrides the server name used for verification. | ||
| func (c *testCreds) OverrideServerName(serverName string) error { | ||
| return nil | ||
| } | ||
|
|
||
| // TestCorrectAuthorityWithCustomCreds tests the `grpc.CallAuthority` call | ||
| // option using custom credentials. It verifies behavior both, when the | ||
eshitachandwani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // credentials implement AuthorityValidator with both correct and incorrect | ||
| // authority overrides, as well as when the credentials do not implement | ||
| // AuthorityValidator. The later two cases, i.e when the credentials do not | ||
| // implement AuthorityValidator, and the authority used to override is invalid, | ||
| // are expected to fail with `UNAVAILABLE` status code. | ||
| func (s) TestCorrectAuthorityWithCustomCreds(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| creds credentials.TransportCredentials | ||
| wantAuth string | ||
| wantStatus codes.Code | ||
eshitachandwani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }{ | ||
| { | ||
| name: "CorrectAuthorityWithFakeCreds", | ||
easwars marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| wantAuth: "auth.test.example.com", | ||
| creds: &testCreds{WithValidator: true}, | ||
| wantStatus: codes.OK, | ||
| }, | ||
| { | ||
| name: "IncorrectAuthorityWithFakeCreds", | ||
| wantAuth: "auth.example.com", | ||
| creds: &testCreds{WithValidator: true}, | ||
| wantStatus: codes.Unavailable, | ||
| }, | ||
| { | ||
| name: "FakeCredsWithNoAuthValidator", | ||
| creds: &testCreds{WithValidator: false}, | ||
| wantAuth: "auth.test.example.com", | ||
| wantStatus: codes.Unavailable, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| ss := stubserver.StubServer{ | ||
| EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) { | ||
| return authorityChecker(ctx, tt.wantAuth) | ||
| }, | ||
| } | ||
| if err := ss.StartServer(); err != nil { | ||
| t.Fatalf("Failed to start stub server: %v", err) | ||
| } | ||
| defer ss.Stop() | ||
|
|
||
| cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(tt.creds)) | ||
| if err != nil { | ||
| t.Fatalf("grpc.NewClient(%q) = %v", ss.Address, err) | ||
| } | ||
| defer cc.Close() | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) | ||
| defer cancel() | ||
| if _, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(tt.wantAuth)); status.Code(err) != tt.wantStatus { | ||
| t.Fatalf("EmptyCall() returned status %v, want %v", status.Code(err), tt.wantStatus) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -749,6 +749,25 @@ func (t *http2Client) NewStream(ctx context.Context, callHdr *CallHdr) (*ClientS | |
| callHdr = &newCallHdr | ||
| } | ||
|
|
||
| // The authority specified via the `CallAuthority` CallOption takes the | ||
| // highest precedence when determining the `:authority` header. It overrides | ||
| // any value present in the Host field of CallHdr. Before applying this | ||
| // override, the authority string is validated. If the credentials do not | ||
| // implement the AuthorityValidator interface, or if validation fails, the | ||
| // RPC is failed with a status code of `UNAVAILABLE`. | ||
| if callHdr.Authority != "" { | ||
| auth, ok := t.authInfo.(credentials.AuthorityValidator) | ||
| if !ok { | ||
| return nil, &NewStreamError{Err: status.Error(codes.Unavailable, fmt.Sprintf("credentials type %s does not implement the AuthorityValidator interface, but authority override specified with CallAuthority call option", t.authInfo.AuthType()))} | ||
|
||
| } | ||
| if err := auth.ValidateAuthority(callHdr.Authority); err != nil { | ||
| return nil, &NewStreamError{Err: status.Error(codes.Unavailable, fmt.Sprintf("failed to validate authority %s : %s", callHdr.Authority, err))} | ||
|
||
| } | ||
| newCallHdr := *callHdr | ||
| newCallHdr.Host = callHdr.Authority | ||
| callHdr = &newCallHdr | ||
| } | ||
|
|
||
| headerFields, err := t.createHeaderFields(ctx, callHdr) | ||
| if err != nil { | ||
| return nil, &NewStreamError{Err: err, AllowTransparentRetry: false} | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.