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

Passthrough OAuth bearer token supplied to Query service through to ES storage #1599

Merged
merged 1 commit into from
Jun 27, 2019

Conversation

rubenvp8510
Copy link
Contributor

Which problem is this PR solving?

  • To provide fine grained visibility of tracing information stored in Elasticsearch, protected using tools like SearchGuard, it would be useful if the OAuth bearer token for the authenticated user could be passed through the Query service to the ES storage plugin and then propagated to the ES cluster.
    To enable users to disable the propagation, this PR includes the possibility of enable/disable this behavior using a flag.

Short description of the changes

  • I injected the token in the context, and used it on ES plugin, also included a flag, but not sure if the flag should be at the query level, I put it the query level because this behavior only makes sense there. (IMHO). Also I did some changes to disable health check on plugin because the token is provided on each request, so I cannot check health of ES cluster when query service starts.

@@ -38,21 +38,18 @@ const (

// ServiceOperationStorage stores service to operation pairs.
type ServiceOperationStorage struct {
ctx context.Context
Copy link
Member

Choose a reason for hiding this comment

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

+1

func BearTokenPropagationHandler(logger *zap.Logger, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
authHeaderValue := r.Header.Get("Authorization")
Copy link
Member

Choose a reason for hiding this comment

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

What is the format of Authorization header?

Copy link
Contributor Author

@rubenvp8510 rubenvp8510 Jun 12, 2019

Choose a reason for hiding this comment

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

According to https://tools.ietf.org/html/rfc6750#section-2.1
It should be in the form : Authorization: "Bearer <token>"

if authHeaderValue != "" {
bearerToken := strings.Split(authHeaderValue, " ")
if len(bearerToken) == 2 {
ctx = context.WithValue(ctx, "Storage-Token", bearerToken)
Copy link
Member

Choose a reason for hiding this comment

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

The recommended way of storing values in Context is by using private keys:

type storageContextKeyType string
var storageContextKey = storageContextKeyType("Storage-Token")

Copy link
Contributor Author

@rubenvp8510 rubenvp8510 Jun 12, 2019

Choose a reason for hiding this comment

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

Thanks for the recomendation, I'll change it

if queryOpts.BearerTokenPropagation {
compressHandler = handlers.CompressHandler(BearTokenPropagationHandler(logger, r))
} else {
compressHandler = handlers.CompressHandler(r)
Copy link
Member

Choose a reason for hiding this comment

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

avoid duplicating wrappers. There's no business meaning of calling CompressHandler twice in different context.

I suggest var h http.Hander = r and then keep overwriting h with every new decorator (including in L90)

return &http.Server{
Handler: recoveryHandler(compressHandler),
}
}

func BearTokenPropagationHandler(logger *zap.Logger, h http.Handler) http.Handler {
Copy link
Member

Choose a reason for hiding this comment

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

doesn't need to be public

}
} else {
httpClient.Transport = httpTransport
options = append(options, elastic.SetBasicAuth(c.Username, c.Password))
}

if c.TokenFromContext {
httpClient.Transport = &tokenAuthTransport{
Copy link
Member

Choose a reason for hiding this comment

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

this will overwrite L281

token := r.Context().Value("Storage-Token")
if token != nil && token != "" {
r.Header.Set("Authorization", "Bearer "+token.(string))
return tr.wrapped.RoundTrip(r)
Copy link
Member

Choose a reason for hiding this comment

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

repeats L356-357, please DRY

@@ -231,7 +231,7 @@ func (s *SpanReader) GetServices(ctx context.Context) ([]string, error) {
defer span.Finish()
currentTime := time.Now()
jaegerIndices := s.timeRangeIndices(s.serviceIndexPrefix, currentTime.Add(-s.maxSpanAge), currentTime)
return s.serviceOperationStorage.getServices(jaegerIndices)
return s.serviceOperationStorage.getServices(jaegerIndices, ctx)
Copy link
Member

Choose a reason for hiding this comment

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

ctx should be in the first position

@rubenvp8510 rubenvp8510 force-pushed the es-bearer-token branch 9 times, most recently from e48b633 to 94aa253 Compare June 19, 2019 17:04
@codecov
Copy link

codecov bot commented Jun 19, 2019

Codecov Report

Merging #1599 into master will increase coverage by <.01%.
The diff coverage is 95%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1599      +/-   ##
==========================================
+ Coverage   98.72%   98.72%   +<.01%     
==========================================
  Files         191      193       +2     
  Lines        9182     9211      +29     
==========================================
+ Hits         9065     9094      +29     
+ Misses         91       90       -1     
- Partials       26       27       +1
Impacted Files Coverage Δ
cmd/query/app/flags.go 100% <100%> (ø) ⬆️
cmd/query/app/token_propagation_handler.go 100% <100%> (ø)
cmd/query/app/server.go 90.76% <100%> (+0.44%) ⬆️
plugin/storage/es/options.go 100% <100%> (ø) ⬆️
plugin/storage/es/spanstore/reader.go 100% <100%> (ø) ⬆️
plugin/storage/es/spanstore/writer.go 97.59% <100%> (ø) ⬆️
plugin/storage/es/spanstore/service_operation.go 100% <100%> (ø) ⬆️
storage/spanstore/token_propagation.go 71.42% <71.42%> (ø)
... and 1 more

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 5b52726...53aafca. Read the comment docs.

@rubenvp8510 rubenvp8510 force-pushed the es-bearer-token branch 4 times, most recently from 22e1a92 to e04a824 Compare June 19, 2019 23:54
@rubenvp8510 rubenvp8510 changed the title WIP: Passthrough OAuth bearer token supplied to Query service through to ES storage Passthrough OAuth bearer token supplied to Query service through to ES storage Jun 20, 2019
Copy link
Contributor

@jpkrohling jpkrohling left a comment

Choose a reason for hiding this comment

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

Looks very good! I have only a couple of comments, especially about sanitizing the bearer value and about making sure that only the appropriate authorization types are propagated down.

cmd/query/app/token_propagation_handler.go Outdated Show resolved Hide resolved
headerValue := strings.Split(authHeaderValue, " ")
token := ""
if len(headerValue) == 2 {
token = headerValue[1]
Copy link
Contributor

Choose a reason for hiding this comment

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

The way it is, you are probably propagating non-bearer authorization as well, such as Basic. If this is what you want, you should rename the functions from bearerTokenPropagation to authorizationHeaderPropagation.

If you didn't intend to propagate the basic auth as well, check explicitly for the authorization type ("Bearer" == headerValue[0]).

pkg/es/config/config.go Show resolved Hide resolved
pkg/es/config/config.go Show resolved Hide resolved
pkg/es/config/config.go Show resolved Hide resolved
func bearTokenPropagationHandler(logger *zap.Logger, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
authHeaderValue := r.Header.Get("Authorization")
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we also need to be checking for X-Forwarded-Access-Token - as (for example) if using the OpenShift OAuthProxy - it will receive the bearer token from the UI, and if configured to do so, will pass it in the upstream request in the X-Forwarded-Access-Token header instead of Authorization.

@jpkrohling does this sound reasonable to you?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it does. That's a common header set by reverse proxies.

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 put support for X-Forwarded-Access-Token, I tried to get token from there if Authorization header is not defined.

@rubenvp8510 rubenvp8510 force-pushed the es-bearer-token branch 3 times, most recently from fbc712d to ecf5ed6 Compare June 26, 2019 12:06

// ContextWithBearerToken set bearer token in context
func ContextWithBearerToken(ctx context.Context, token string) context.Context {
return context.WithValue(ctx, bearerToken, token)
Copy link
Contributor

@objectiser objectiser Jun 26, 2019

Choose a reason for hiding this comment

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

Should this check whether the token has a value? Currently, if Authorization is not Bearer the supplied string will be empty? If empty token, then could just return the ctx.

r.Header.Set("Authorization", "Bearer "+tr.token)
token := tr.token
if tr.allowOverrideFromCtx {
token, _ = spanstore.GetBearerToken(r.Context())
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be better to return the token into a local var, check a value has been found, before overwriting the token. That way - if a request is sent in without a bearer token, it will still revert to the supplied tr.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.

well, I wasn't sure if we wanted to do that, but ok I'll do the change

@objectiser
Copy link
Contributor

@rubenvp8510 Once the changes have been applied, could you do a quick test using the operator with Oauth proxy cli parameter --pass-user-bearer-token and check that a token is passed to ES?

@rubenvp8510
Copy link
Contributor Author

@objectiser sure!

…S storage

Signed-off-by: Ruben Vargas <ruben.vp8510@gmail.com>
)


func Test_bearTokenPropagationHandler(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.

Look, I found another bear!

@pavolloffay pavolloffay merged commit cf03be7 into jaegertracing:master Jun 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants