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

Add auth token propagation for metrics reader #3341

Merged
merged 17 commits into from
Nov 2, 2021

Conversation

albertteoh
Copy link
Contributor

Signed-off-by: albertteoh see.kwang.teoh@gmail.com

Which problem is this PR solving?

Short description of the changes

  • Adds propagation of authorization token from the inbound request to the outbound request to a metrics server.
  • This is achieve by wrapping the http.Transport into a tokenAuthTransport struct that implements the http.RoundTripper's RoundTrip function, which injects the Authorization bearer token header into the outbound request.
  • Follows the existing pattern used by the ES reader: https://github.com/jaegertracing/jaeger/blob/master/pkg/es/config/config.go#L532

Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
@albertteoh albertteoh requested a review from a team as a code owner October 27, 2021 11:37
@codecov
Copy link

codecov bot commented Oct 27, 2021

Codecov Report

Merging #3341 (2742a11) into master (c4a3904) will increase coverage by 0.03%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3341      +/-   ##
==========================================
+ Coverage   96.46%   96.49%   +0.03%     
==========================================
  Files         259      260       +1     
  Lines       15178    15196      +18     
==========================================
+ Hits        14642    14664      +22     
+ Misses        452      449       -3     
+ Partials       84       83       -1     
Impacted Files Coverage Δ
cmd/query/app/server.go 95.58% <100.00%> (ø)
pkg/bearertoken/context.go 100.00% <100.00%> (ø)
pkg/bearertoken/http.go 100.00% <100.00%> (ø)
pkg/bearertoken/transport.go 100.00% <100.00%> (ø)
plugin/metrics/prometheus/metricsstore/reader.go 100.00% <100.00%> (ø)
plugin/storage/es/options.go 100.00% <100.00%> (ø)
plugin/storage/grpc/shared/grpc_client.go 83.43% <100.00%> (ø)
cmd/collector/app/server/zipkin.go 70.73% <0.00%> (+2.43%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c4a3904...2742a11. Read the comment docs.

th3M1ke
th3M1ke previously approved these changes Oct 27, 2021
Copy link
Contributor

@th3M1ke th3M1ke left a comment

Choose a reason for hiding this comment

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

🔥

pavolloffay
pavolloffay previously approved these changes Oct 27, 2021

// tokenAuthTransport wraps an instance of http.Transport for the purpose of
// propagating an Authorization token from inbound to outbound HTTP requests.
type tokenAuthTransport struct {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: could we take this opportunity and refactor/cleanup bearer token functionality into a standalone package? We have ./cmd/query/app/token_propagation_handler.go and ./storage/spanstore/token_propagation.go, which should really be in a single package like ./pkg/bearertoken.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in d1c96ad.

Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
@albertteoh albertteoh dismissed stale reviews from pavolloffay and th3M1ke via fa128b3 October 28, 2021 11:06
Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
// the Authorization token from the http.Request and inserts it into the http.Request
// context for easier access to the request token via GetBearerToken for bearer token
// propagation use cases.
func PropagationHandler(logger *zap.Logger, h http.Handler) http.Handler {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed bearerTokenPropagationHandler -> PropagationHandler to reduce stutter.

I considered renaming ContextWithBearerToken and GetBearerToken for similar reasons but on further thought, I felt it reduces readability. Let me know if you disagree.


// tokenAuthTransport wraps an instance of http.Transport for the purpose of
// propagating an Authorization token from inbound to outbound HTTP requests.
type tokenAuthTransport struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in d1c96ad.

func GetBearerToken(ctx context.Context) (string, bool) {
val, ok := ctx.Value(bearerToken).(string)
return val, ok
}
Copy link
Member

Choose a reason for hiding this comment

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

I would split this file:

  • up to this point it's context.go
  • below it's http.go
  • you may also want to add the http transport implementation instead of using private types in different storages

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in f0774b2.

)

func Test_bearTokenPropagationHandler(t *testing.T) {
func Test_GetBearerToken(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI"
Copy link
Member

Choose a reason for hiding this comment

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

Couldn't this string just be "blah"? Currently it looks like there's special meaning in the value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 8e5f1b9.

plugin/storage/es/options.go Show resolved Hide resolved
Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
plugin/storage/es/options.go Show resolved Hide resolved
@@ -331,12 +332,27 @@ func TestGetRoundTripper(t *testing.T) {
},
}, logger)
require.NoError(t, err)
assert.IsType(t, &http.Transport{}, rt)
if tc.tlsEnabled {
assert.NotNil(t, rt.(*http.Transport).TLSClientConfig)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moving the common transport into the bearertoken package meant that we can't inspect the wrapped http.Transport.
The original test didn't feel quite right in the first place when it had to make type assertions.

Suggestions also welcome.

// request's Authorization bearer token header.
// If the WithAllowOverrideFromCtx(true) option is provided, the request context's
// bearer token, will be used in preference to this token.
func WithToken(token string) Option {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I took the functional options approach because the defaultToken wasn't mandatory and didn't feel right to force callers to pass a sentinel token value, nor make callers guess what they should be setting it to if we exported this value in the struct, especially given it's in a separate package.

Let me know if you disagree with the approach.

Copy link
Member

Choose a reason for hiding this comment

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

We started to move away from the use of functional option pattern, it just adds unnecessary mental overhead and has poor discoverability in the docs. I feel like this type can simply be declared with public fields and instantiated directly, without a constructor function. The optionality of the fields is naturally available via struct zero values.

{
name: "No options provided and request context set should have empty Bearer token",
roundTripper: roundTripFunc(func(r *http.Request) (*http.Response, error) {
assert.Equal(t, "Bearer ", r.Header.Get("Authorization"))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't sure if this is an acceptable Auth header value or if an error should be returned. This was the original behaviour so I kept it that way.

// NewTransport returns a new bearer token transport that wraps the given
// http.RoundTripper, forwarding the authorization token from inbound to
// outbound HTTP requests.
func NewTransport(roundTripper http.RoundTripper, opts ...Option) http.RoundTripper {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't feel like it was necessary to expose the internals of the transport, hence why http.RoundTripper is returned, given that's the only use case right now.

Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
@albertteoh
Copy link
Contributor Author

It's not clear to me why the CIT Crossdock job is failing on:

✗ [endtoend] test_driver→ (services=go) ⇒ Fail: could not retrieve traces from query service

Comment on lines 21 to 23
// Key is the string literal used internally in the implementation of this context.
const Key = "bearer.token"
const bearerToken = contextKey(Key)
Copy link
Member

Choose a reason for hiding this comment

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

I don't see Key being used outside of this package

Suggested change
// Key is the string literal used internally in the implementation of this context.
const Key = "bearer.token"
const bearerToken = contextKey(Key)
type contextKeyType string
const contextKey = contextKeyType("bearer.token")

// request's Authorization bearer token header.
// If the WithAllowOverrideFromCtx(true) option is provided, the request context's
// bearer token, will be used in preference to this token.
func WithToken(token string) Option {
Copy link
Member

Choose a reason for hiding this comment

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

We started to move away from the use of functional option pattern, it just adds unnecessary mental overhead and has poor discoverability in the docs. I feel like this type can simply be declared with public fields and instantiated directly, without a constructor function. The optionality of the fields is naturally available via struct zero values.

token = headerToken
}
}
r.Header.Set("Authorization", "Bearer "+token)
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to check for token != "" before setting the header?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I considered it but was technically a breaking change; I don't know enough about Auth headers to determine if this is a bug or correct behaviour, though sounds like it's a bug given your suggestion.

pkg/bearertoken/transport.go Outdated Show resolved Hide resolved
plugin/storage/es/options.go Show resolved Hide resolved
Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
token = headerToken
}
}
r.Header.Set("Authorization", "Bearer "+token)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I considered it but was technically a breaking change; I don't know enough about Auth headers to determine if this is a bug or correct behaviour, though sounds like it's a bug given your suggestion.

const bearerToken = contextKey(BearerTokenKey)
const (
// Key is the key name for the bearer token context value.
Key = "bearer.token"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Key is used outside of this package here.

Copy link
Member

Choose a reason for hiding this comment

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

Strictly speaking, I think it would've been better if grpc plugin just defined its own public constant instead of relying on this one, since they are completely independent.

$ grx BearerTokenKey .
./plugin/storage/grpc/shared/grpc_client.go:76:		md.Set(spanstore.BearerTokenKey, bearerToken)
./plugin/storage/grpc/shared/grpc_client_test.go:115:	bearerTokenFromMetadata := md.Get(spanstore.BearerTokenKey)
./plugin/storage/grpc/README.md:191:		values := md.Get(spanstore.BearerTokenKey)

If we remove that external dependency, then this key can be made private, and doesn't need to be a string, just this:

type contextKeyType int
const contextKey = iota

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 696cb5f.

const bearerToken = contextKey(BearerTokenKey)
const (
// Key is the key name for the bearer token context value.
Key = "bearer.token"
Copy link
Member

Choose a reason for hiding this comment

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

Strictly speaking, I think it would've been better if grpc plugin just defined its own public constant instead of relying on this one, since they are completely independent.

$ grx BearerTokenKey .
./plugin/storage/grpc/shared/grpc_client.go:76:		md.Set(spanstore.BearerTokenKey, bearerToken)
./plugin/storage/grpc/shared/grpc_client_test.go:115:	bearerTokenFromMetadata := md.Get(spanstore.BearerTokenKey)
./plugin/storage/grpc/README.md:191:		values := md.Get(spanstore.BearerTokenKey)

If we remove that external dependency, then this key can be made private, and doesn't need to be a string, just this:

type contextKeyType int
const contextKey = iota

pkg/bearertoken/http.go Outdated Show resolved Hide resolved
Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
Signed-off-by: albertteoh <see.kwang.teoh@gmail.com>
@yurishkuro yurishkuro merged commit 4aa436b into jaegertracing:master Nov 2, 2021
@albertteoh albertteoh deleted the 3340-token-prop branch November 2, 2021 20:57
@albertteoh
Copy link
Contributor Author

Thanks for the review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Authorization header does not propagate to /api/metrics/* endpoints
4 participants