Skip to content
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

credentials, transport, grpc : add a call option to override the :authority header on a per-RPC basis #8068

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

eshitachandwani
Copy link
Member

Fixes: #5361

RELEASE NOTES:

  • Added a CallAuthority callOption that can be used to overwrite the http :authority header on per-RPC basis.
  • Introduces an option AuthorityValidator interface which needs to be implemented by credentials that want to allow authority overwrite.
  • Adds implementations of AuthorityValidator interface for Insecure and TLS credentials.

@eshitachandwani eshitachandwani added Type: Feature New features or improvements in behavior Area: Auth Includes regular credentials API and implementation. Also includes advancedtls, authz, rbac etc. labels Feb 6, 2025
@eshitachandwani eshitachandwani added this to the 1.71 Release milestone Feb 6, 2025
Copy link

codecov bot commented Feb 6, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 82.36%. Comparing base (0003b4f) to head (b1583a6).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8068      +/-   ##
==========================================
+ Coverage   82.32%   82.36%   +0.04%     
==========================================
  Files         387      387              
  Lines       39064    39091      +27     
==========================================
+ Hits        32159    32197      +38     
+ Misses       5593     5584       -9     
+ Partials     1312     1310       -2     
Files with missing lines Coverage Δ
credentials/credentials.go 87.87% <ø> (ø)
credentials/insecure/insecure.go 83.33% <100.00%> (+1.51%) ⬆️
credentials/tls.go 88.46% <100.00%> (+1.88%) ⬆️
internal/transport/http2_client.go 91.77% <100.00%> (+0.06%) ⬆️
internal/transport/transport.go 91.56% <ø> (ø)
rpc_util.go 80.97% <100.00%> (+0.29%) ⬆️
stream.go 81.54% <100.00%> (-0.24%) ⬇️

... and 19 files with indirect coverage changes

Copy link
Contributor

@purnesh42H purnesh42H left a comment

Choose a reason for hiding this comment

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

Overall lgtm. Just few minor comments. I think we can combine some individual into t-tests.

@@ -120,6 +120,14 @@ type AuthInfo interface {
AuthType() string
}

// AuthorityValidator defines an interface for validating the authority used to
Copy link
Contributor

Choose a reason for hiding this comment

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

no need to mention "defines an interface". Can just say "AuthorityValidator validates the authority....."

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

// precedence to determine the :authority header. Any value in Host field of
// CallHdr is overwritten. But before overriding, we validate the authority
// string against the peer certificates and fail the RPC with `UNAVAILABLE`
// status code if eirther of the condition fails.
Copy link
Contributor

Choose a reason for hiding this comment

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

typo: either

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

if !ok {
return nil, &NewStreamError{Err: status.Error(codes.Unavailable, fmt.Sprintf("credentials type %s does not implement the AuthorityValidator interface", t.authInfo.AuthType())), AllowTransparentRetry: false}
}
err := auth.ValidateAuthority(callHdr.Authority)
Copy link
Contributor

Choose a reason for hiding this comment

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

can combine this with if

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func CallAuthority(auth string) CallOption {
Copy link
Contributor

Choose a reason for hiding this comment

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

should we call it PerRPCAuthority? Similar to PerRPCCredentials?

Copy link
Contributor

Choose a reason for hiding this comment

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

We went through this in the design doc. We had consensus on CallAuthority.

//
// Notice: This type is EXPERIMENTAL and may be changed or removed in a
// later release.
type AuthorityOverrideCallOption struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we name above PerRPCAuthority, AuthorityOverrideCallOption can change to PerRPCAuthorityCallOption

defer cancel()

_, err = testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}, grpc.CallAuthority(tt.expectedAuth))
if tt.expectRPCError {
Copy link
Contributor

Choose a reason for hiding this comment

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

s/expect/want

Copy link
Member Author

Choose a reason for hiding this comment

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

done.

}
}

func (s) TestTLSCredsWithNoAuthorityOverride(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

the tests tls_ext_test.go should already be testing this right? Does this need to be here? or it should just be another test case in the above TestAuthorityCallOptionsWithTLSCreds

// Perform a test RPC with a specified call authority.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()

Copy link
Contributor

Choose a reason for hiding this comment

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

nix new line

}

// FakeCredsNoAuthValidator is a test credential that does not implement AuthorityValidator.
type FakeCredsNoAuthValidator struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

the FakeCreds can be at the top

Copy link
Contributor

Choose a reason for hiding this comment

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

Please rename this testCreds. We use the term fake when we use a test double that actually provides a fake implementation of something that the code under test depends on.

Also, the AuthInfo types need to be named such that it is clear from their names that they either implement the AuthorityValidator interface or not.

// TestCallOptionWithNoAuthorityValidator tests the CallAuthority call option
// with custom credentials that do not implement AuthorityValidator and verifies
// that it fails with `UNAVAILABLE` status code.
func (s) TestCallOptionWithNoAuthorityValidator(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same. This can be another test case in test with CustomCreds

Copy link
Member Author

Choose a reason for hiding this comment

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

done.

@purnesh42H
Copy link
Contributor

Also, don't feel strongly but may be we can utilize the setup in tls_ext_test or move the tls tests to tls_ext_test.go

@@ -71,6 +71,10 @@ func (info) AuthType() string {
return "insecure"
}

func (info) ValidateAuthority(_ string) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Can get rid of the _ variable name as this method contains only one parameter.

return nil
}
}
return fmt.Errorf("credentials: failed to verify authority %s", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

The err variable will only contain the last error here. Should we instead include all errors?

Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Also, the error message could be something like
fmt.Errorf(credentials: invalid authority %q: %s", authority, err)

@@ -120,6 +120,13 @@ type AuthInfo interface {
AuthType() string
}

// AuthorityValidator validates the authority used to override the `:authority`
// header. A struct implementing AuthInfo should also implement
Copy link
Contributor

Choose a reason for hiding this comment

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

s/A struct implementing AuthInfo should/Implementations of AuthInfo should/

// header. A struct implementing AuthInfo should also implement
// AuthorityValidator if the credentials need to support per-RPC authority overrides.
type AuthorityValidator interface {
ValidateAuthority(authority string) error
Copy link
Contributor

Choose a reason for hiding this comment

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

Please document what this authority means and what the return value indicates.

// AuthorityValidator validates the authority used to override the `:authority`
// header. A struct implementing AuthInfo should also implement
// AuthorityValidator if the credentials need to support per-RPC authority overrides.
type AuthorityValidator interface {
Copy link
Contributor

Choose a reason for hiding this comment

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

Also, I think we should focus on documenting a couple of things here:

  • that this is an option interface
  • will be invoked only when the appropriate call option is specified by the application

Something to this effect could work (might need some cleanup)

// AuthorityValidator validates  `:authority` header overrides. This is an
// optional interface for transport credentials to implement.
type AuthorityValidator interface {
	// ValidateAuthority validates the authority used to override the
	// `:authority` header. If implemented by the AuthInfo type returned by the
	// transport credentials, this method will be invoked if the client
	// application attempts to override the `:authority` header by using the
	// CallAuthority call option.
	ValidateAuthority(authority string) error
}

return nil
}
}
return fmt.Errorf("credentials: failed to verify authority %s", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Also, the error message could be something like
fmt.Errorf(credentials: invalid authority %q: %s", authority, err)

//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func CallAuthority(auth string) CallOption {
Copy link
Contributor

Choose a reason for hiding this comment

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

We went through this in the design doc. We had consensus on CallAuthority.

//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func CallAuthority(auth string) CallOption {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: s/auth/authority/

Please note that this is part of our public API. Parameter names also serve as documentation. So using good parameter names is critical.

@@ -365,6 +366,36 @@ func (o MaxRecvMsgSizeCallOption) before(c *callInfo) error {
}
func (o MaxRecvMsgSizeCallOption) after(*callInfo, *csAttempt) {}

// CallAuthority returns a CallOption which sets the authority to override the
// `:authority` pseudoheader in a RPC. If this is set, the RPC will use only
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the only place where we use the term pseudoheader. Either use it everywhere (like in the dosctrings of the AuthorityValidator interface), or don't use it here. Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

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

How about something more simpler and direct

"CallAuthority creates a CallOption that sets the HTTP/2 :authority header of an RPC to the specified value."

}

// FakeCredsNoAuthValidator is a test credential that does not implement AuthorityValidator.
type FakeCredsNoAuthValidator struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please rename this testCreds. We use the term fake when we use a test double that actually provides a fake implementation of something that the code under test depends on.

Also, the AuthInfo types need to be named such that it is clear from their names that they either implement the AuthorityValidator interface or not.

Comment on lines +205 to +209
// TestAuthInfo implements the AuthInfo interface.
type TestAuthInfo struct{}

// AuthType returns the authentication type.
func (TestAuthInfo) AuthType() string { return "test" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: The definition of this type is sandwiched between the implementation of the test creds type. Please move it to be separate.

}

// TestAuthInfo implements the AuthInfo interface.
type TestAuthInfo struct{}
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this have to be exported?


// ServerHandshake performs the server-side handshake.
// Returns a test AuthInfo object to satisfy the interface requirements.
func (c *FakeCredsWithAuthValidator) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Basically, we can have a single testCreds type that contains a boolean field that indicates whether or not it should return an AuthInfo that implements AuthorityValidator or not. We don't need two test credentials types here.

name string
creds credentials.TransportCredentials
expectedAuth string
wantRPCError bool
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here. You can directly use the status that you want the RPC to return.

@easwars easwars removed their assignment Feb 14, 2025
@arjan-bal arjan-bal modified the milestones: 1.71 Release, 1.72 Release Feb 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: Auth Includes regular credentials API and implementation. Also includes advancedtls, authz, rbac etc. Type: Feature New features or improvements in behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

grpc: add a call option to override the :authority header on a per-RPC basis
4 participants