-
Notifications
You must be signed in to change notification settings - Fork 61
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
fix: remove call credentials from call options if DirectPath #3670
base: main
Are you sure you want to change the base?
Conversation
@lqiu96 I've verified this would fix the DirectPath case. No idea about the obscure failure in java-firestore yet. Will add tests next week. I think this approach is promising. |
I think this does work for DirectPath. However, doesn't this issue also exist for S2A where both tokens are being passed? Also, this would override any user set CallOption value. |
I was wondering if we could introduce a similar API as
This is by far the simplest approach and doesn't even need to touch how channels are constructed in any other non-DirectPath case (S2A with bound token will also need to be handled later). And somewhat biased by this, I'm hoping we could be a bit opinionated in the DirectPath case to disregard users' call credentials in the call options, given we clearly document this behavior. Note in my other PR, we are overriding the call option value as well. |
I would like this. I'm not able to easily map a way to determine this since I think it would require actually trying to create the CallCredentials and checking if they could be created. I guess that is a possibility we can also try. I was exploring dynamically setting the call options creds in #3671
I would like to be opinionated on this too. However, can you remind me the behavior if something like SACreds were used instead of UserCreds, would DP would work? Would it be possible to override the calloptions creds using DP?
I'm not against this, though I would like to try and see if we can figure a generic solution to tackle sending creds twice, rather than adding one-off fixes for specific flows. |
S2A don't generally take the CallCredentials except for the bound token case introduced in #3591, where
Any creds (SA, Users), as long as allowed right now by DirectPath, should work without any problem with this PR. It's just those creds need to be passed into the TransportChannelProvider to be used in the DirectPath channel creation. We don't enable bound tokens if the given creds are not ComputeEngine ones, but we still pass them as is into So basically, we document that DirectPath does not take in any CallCredentials from the ApiCallContext and we should be good. Is this what you were asking about?
In that case, I wonder if you are open to adding a more generic method to |
I see, so in the case of Mtls (non S2A) and just normal TLS, then the expectation is that CallCredentials should be attached via CallOptions? In that case, I think my assumption that always using ChannelCredentials instead of CallOptions is wrong.
Ok I see, I think I'm on the same page now. In this case only DP and MTLS_S2A would be the cases as you've mentioned above.
Yes I think if we take the opinionated route this what what we should do. The example that I was thinking above was that a user established a DP connection with some SA cred and overrides with a different SA cred via CallOptions (something like what Spanner has: https://github.com/googleapis/java-spanner/blob/7a8a29be40258294cafd13b1df7df5ea349a675d/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java#L2037). Is this a behavior that works or would or would it fail/ never worked?
Yeah, this is what the PR above was trying to explore. Adding a new method that determined if the CallCredentials was attached to the channel. When a GAPIC client is initialized, it will try to determine if this was a user set value or set by the client and it will ignore the client set value (but I think I got the logic wrong). |
cb3dc16
to
39489bc
Compare
if (!isDirectPath) return callOptions; | ||
// Remove the CallCredentials attached to the callOptions if it's DirectPath. | ||
return callOptions.withCallCredentials(null); |
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.
Can this logic reside inside the constructor? I think it would be better for the getter to just return value.
nit: Can the comment be updated to reflect the why
. Perhaps something like (probably needs better wording): CallCredentials is stripped from CallOptions because CallCredentials are attached to ChannelCredentials in DirectPath flows. Adding it again would duplicate the headers.
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.
Done. I also modify the merge logic to respect the isDirectPath of the coming context. Also several cases I take care of here:
- If
withChannel
is subsequently called, we attach a CallCreds from the creds back to calloptions and revertisDirectPath
to false. - If
withCallOptions
is subsequently called, we strip the CallCreds from it if DirectPath. - If
withCredentials
is subsequently called, we attach its corresponding CallCreds to the calloptions only if non-DirectPath
Please help check if I missed any corner case.
gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallContext.java
Show resolved
Hide resolved
gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallContext.java
Outdated
Show resolved
Hide resolved
I think the last thing is to update the GrpcCallContext's Thoughts on a small blurb like (re-phrase/ update): For certain flows like DirectPath, the channel is already created with a CallCredentials. Passing in an additional CallCredentials here will not override the ChannelCredential's CallCredential and may cause issues as the CallCredentials is duplicated. These flows will attempt to automatically strip the CallOption's CallCreds. |
(Not specific to this PR, but related to fixing this problem for MTLS_S2A)
I think it may be more than this. In order to set
This is due to the fact that for DirectPath: if you can use DirectPath then you will use DirectPath. However, this is not true for S2A, if you can use S2A, you may end up using S2A (you could end up using DirectPath since DP supersedes S2A or you could end up falling back to TLS if you fail to create S2A channel creds). I'll try to put together a PR taking this into account. Edit: Actually I see that #3671 did take this into account. Although I am unclear on how we can make sure createSingleChannel gets called before setting IsCallCredentialAttachedToChannel |
For posterity, we confirmed that [:authority: spanner.googleapis.com, :path: /google.spanner.v1.Spanner/BatchCreateSessions, :method: POST, :scheme: https, content-type: application/grpc, te: trailers, user-agent: spanner-java/6.86.0 grpc-java-netty/1.69.0, ..., grpc-accept-encoding: gzip, authorization: Bearer ya29.****, ..., authorization: Bearer 1234, grpc-timeout: 56962080u] The garbage token in the second authorization header didn't cause the call to fail the authn/z. In general, we shouldn't rely on this gRPC implementation detail since it could in theory change the appending order and break everything. This PR will fix the duplication header issue so we are good. |
return new GrpcCallContext( | ||
transportChannel.getChannel(), | ||
credentials, | ||
transportChannel.isDirectPath() ? callOptions.withCallCredentials(null) : callOptions, |
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.
Sorry, I meant can we do this logic/ check inside the private constructor? This should only pass the calloptions.
Constructor contains the:
this.callOptions = isDirectPath() ? calloptions.withCallCredentials(null) : callOptions
logic
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.
Ah this makes so much sense! Sorry I don't know what I was thinking. Changed.
/gcbrun |
/gcbrun |
@@ -96,6 +96,7 @@ public final class GrpcCallContext implements ApiCallContext { | |||
private final ImmutableMap<String, List<String>> extraHeaders; | |||
private final ApiCallContextOptions options; | |||
private final EndpointContext endpointContext; | |||
private final boolean isDirectPath; |
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.
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 the info. I thought that referred to canUseDirectPath()
which could be invoked multiple times before and after the credentials was set so might be no accurate, but by the time the ClientContext got this boolean from the channel, it should already be final.
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.
Talked with @surbhigarg92 regarding this to get some more information about the issue. This issue from Spanner's POV is not so much that the canUseDirectPath
value that is client is initialized with and used by the TransportChannel is incorrect, it's that Spanner doesn't have a way to get the canUseDirectPath
value when initializing the client for Otel.
Spanner was using GrpcSpannerStub.create(StubSettings)
and didn't have an easy way to access to TransportChannel's fields (StubSettings only exposes getTransportChannelProvider()
. Spanner could use GrpcSpannerStub.create(ClientContext)
, but that creates the Stub with a new StubSettings and not the one they manually configured.
For them, the DirectPath transportchannel was always created correctly and the value the client uses is correct. They used this workaround to be able to access the field for their use case.
I think their issue is a valid concern, but is different from this.
gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallContext.java
Outdated
Show resolved
Hide resolved
gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallContext.java
Outdated
Show resolved
Hide resolved
@@ -228,7 +250,8 @@ public GrpcCallContext withEndpointContext(EndpointContext endpointContext) { | |||
options, | |||
retrySettings, | |||
retryableCodes, | |||
endpointContext); | |||
endpointContext, |
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.
Its unfortunate that we have to change all the places that use the constructor. Ideally, if we had a builder for GrpcCallContext
, the code here would be simplified to this.toBuilder().setEndpointContext(endpointContext).build()
, and we don't have to change the code here at all.
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.
Agreed. But I'd prefer to leave the refactoring with a builder out of this PR since it would look cleaner. I can open an issue for it. Let me know what you think.
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.
SG. Yes please create a separate issue and we can put it in our backlog.
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. Created #3681
…into grpc-creds-alt
…va into grpc-creds-alt
/gcbrun |
Thanks for all the reviews. No rush but just in case, please help merge it when you see fit since I don't have the permission. |
This PR eliminates the issue where call credentials get attached twice to a RPC in DirectPath cases. Particularly, when user credentials get used, the problem causes the RPC to fail due to the duplication of the quota project ID (internal-only context: b/364288002).
The approach is to strip the credentials in the callOptions returned by the
GrpcCallContext
if theTransportChannel
is DirectPath. The side effect is that users won't be able to configure call credentials via theApiCallContext
if DirectPath is used.We think this is acceptable because:
TransportChannelProvider.withCredentials()
. At a higher level, this is done by configuring theCredentialsProvider
in theStubSettings
or theServiceOptions
.Tested DirectPath using Spanner
Headers sent
Bearer token is sent twice (first
ya29.***
value is valid and second1234
is invalid). The second one was attached by customizing the ApiCallContext to send an invalid CallCredentials as part of the CallOptions. The call still succeeded as the first Bearer token in the Metadata is used.