-
Notifications
You must be signed in to change notification settings - Fork 4.6k
credentials: implement file-based JWT Call Credentials (part 1 for A97) #8431
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
base: master
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #8431 +/- ##
==========================================
- Coverage 82.27% 82.13% -0.14%
==========================================
Files 414 415 +1
Lines 40424 40648 +224
==========================================
+ Hits 33259 33387 +128
- Misses 5795 5883 +88
- Partials 1370 1378 +8
🚀 New features to boost your workflow:
|
@dfawley hey 👋 Given you approved A97, would you mind having a cursory look at the PR to confirm if at least at a high level the approach looks good? |
I will take a look at this , I need to go through the gRFC first. |
Sorry for the delay here. @easwars would you be able to review this change? I think you have more background into some of the things than I do, like the bootstrap integration. Thank you! |
Thank you for your contribution @dimpavloff. Yes, it would be nice if you can split this into smaller PRs. I will continue to use this PR to review the JWT call credentials implementation. If you can move the xDS implementation out to one or more PRs, I would greatly appreciate that and would be happy to review them as well. |
|
||
// Verify cached expiration is 30 seconds before actual token expiration | ||
impl := creds.(*jwtTokenFileCallCreds) | ||
impl.mu.RLock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please only test the API surface. Relying on implementation internals in tests makes them brittle and would result in test changes when any changes to implementation is made.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume you are referring to using a private field rather than obtaining mu specifically.
In general I agree -- white box tests may get fragile and break during a refactor. However, this test and the next couple of ones are about the caching behaviour -- it is meant to be transparent to the external API. If I don't make assertions about the private fields, the tests may pass trivially and become more flaky (e.g. when testing the backoff in the next test).
One alternative could be factoring out these behaviours out into a separate private struct with "public" functions which expose the same information. Given that it would require shifting the majority of the implementation into that struct, I'm not sure it is an improvement from the current approach.
Please do let me know your thoughts and if you have other suggestions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks quite good. Most of the comments are nits.
I still have to review some of the tests in credentials/jwt/jwt_token_file_call_creds_test.go though (starting with TestTokenFileCallCreds_CacheExpirationIsBeforeTokenExpiration
).
if err == nil { | ||
t.Fatalf("GetRequestMetadata() expected error, got nil") | ||
} | ||
if !strings.Contains(err.Error(), tt.wantErrContains) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here as well, we should be checking for the status code instead of the actual contents of the error string.
Also, using status.Code(err)
would work with nil
error and return status.OK
which would make the test logic much simpler as it would mostly the same for success and failure cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice point, updated.
Btw, this has helped uncover that credentials.CheckSecurityLevel
and its call in GetRequestMetadata
doesn't return a grpc code. This is also true in credentials/oauth/oauth.go
and credentials/sts/sts.go
. Therefore, this will map to codes.Unknown
is this appropriate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will check with folks on the team and will get back to you soon on this. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chatted with @dfawley and we definitely don't want to use strings to determine what error is being returned. We have two options:
- Define a new error type that is returned by the
jWTFileReader
that allows callers to inspect it and determine how to proceed, or, - Return a grpc status error
Given that the jWTFileReader
is not a generic JWT file reader that can be used outside of this package, we feel that returning a grpc status error is totally fine (and thereby moving the logic of deciding what status code to return based on the type of error encountered into the file reader). But we are fine with either of the above two approaches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops .. the above commented was intended for the other thread about returning error values from the reader.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, this has helped uncover that credentials.CheckSecurityLevel and its call in GetRequestMetadata doesn't return a grpc code. This is also true in credentials/oauth/oauth.go and credentials/sts/sts.go. Therefore, this will map to codes.Unknown is this appropriate?
Where does this map to codes.Unknown
?
Returning non grpc status errors from GetRequestMetadata
is fine. The transport will convert them into Unauthenticated
. See here: https://github.com/grpc/grpc-go/blob/master/internal/transport/http2_client.go#L691
So, it should be fine for this call creds implementation to return a non grpc status error when the transport creds does not support credentials.PrivacyAndIntegrity
.
Hope that helps. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re: Define a new error type that is returned by the jWTFileReader that allows callers to inspect it and determine how to proceed, or,
I would prefer something more like
var someErrorKindA = errors.New("...")
var someErrorKindB = errors.New("...")
return someErrorKindA; // or fmt.Errorf("%w: some explanatory text", someErrorKindA)
if err == someErrorKindA { ... } // or errors.Is(someErrorKindA)
instead of a new type, i.e.
type MyError struct {
....
}
func (MyError) Error() string {...}
return MyError{...}
if err.(*MyError) // or errors.As(...) if wrapping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does this map to codes.Unknown?
Returning non grpc status errors from GetRequestMetadata is fine. The transport will convert them into Unauthenticated. See here: https://github.com/grpc/grpc-go/blob/master/internal/transport/http2_client.go#L691
Ah, I wasn't aware of this. Then it should be fine.
I'll look into the errors, thanks for your inputs!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking care of all of my comments. I think I'm mostly happy with where we are at now. I'll follow up on the two open issues and get back to you soon.
} | ||
} | ||
|
||
// createTestJWT creates a test JWT token with the specified audience and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: remove mention of audience from the comment.
credentials/jwt/jwt_file_reader.go
Outdated
} | ||
} | ||
|
||
// ReadToken reads and parses a JWT token from the configured file. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will check with folks on the team and will get back to you soon on this. Thanks.
if err == nil { | ||
t.Fatalf("GetRequestMetadata() expected error, got nil") | ||
} | ||
if !strings.Contains(err.Error(), tt.wantErrContains) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will check with folks on the team and will get back to you soon on this. Thanks.
Part one for grpc/proposal#492 (A97).
This is done in a new
credentials/jwt
package to provide file-based PerRPCCallCredentials. It can be used beyond XDS. The package handles token reloading, caching, and validation as per A97 .There will be a separate PR which uses it in
xds/bootstrap
.Whilst implementing the above, I considered
credentials/oauth
andcredentials/xds
packages instead of creating a new one. The former package hasNewJWTAccessFromKey
andjwtAccess
which seem very relevant at first. However, I think thejwtAccess
behaviour seems more tailored towards Google services. Also, the refresh, caching, and error behaviour for A97 is quite different than what's already there and therefore a separate implementation would have still made sense.WRT
credentials/xds
, it could have been extended to both handle transport and call credentials. However, this is a bit at odds with A97 which says that the implementation should be non-XDS specific and, from reading between the lines, usable beyond XDS.I think the current approach makes review easier but because of the similarities with the other two packages, it is a bit confusing to navigate. Please let me know whether the structure should change.
Relates to istio/istio#53532
RELEASE NOTES:
credentials/jwt
package providing file-based JWT PerRPCCredentials (A97)