-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Changes from 7 commits
a5133cc
fa128b3
d1c96ad
f0774b2
8e5f1b9
73db361
787b07b
9af8681
bac98cc
56ac31b
f5e3801
65208c2
696eb45
4d7e1c5
696cb5f
bf6a818
2742a11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package bearertoken | ||
|
||
import "context" | ||
|
||
type contextKey string | ||
|
||
// Key is the string literal used internally in the implementation of this context. | ||
const Key = "bearer.token" | ||
const bearerToken = contextKey(Key) | ||
|
||
// StoragePropagationKey is a key for viper configuration to pass this option to storage plugins. | ||
const StoragePropagationKey = "storage.propagate.token" | ||
|
||
// ContextWithBearerToken set bearer token in context. | ||
func ContextWithBearerToken(ctx context.Context, token string) context.Context { | ||
if token == "" { | ||
return ctx | ||
} | ||
return context.WithValue(ctx, bearerToken, token) | ||
} | ||
|
||
// GetBearerToken from context, or empty string if there is no token. | ||
func GetBearerToken(ctx context.Context) (string, bool) { | ||
val, ok := ctx.Value(bearerToken).(string) | ||
return val, ok | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package bearertoken | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func Test_GetBearerToken(t *testing.T) { | ||
const token = "blah" | ||
ctx := context.Background() | ||
ctx = ContextWithBearerToken(ctx, token) | ||
contextToken, ok := GetBearerToken(ctx) | ||
assert.True(t, ok) | ||
assert.Equal(t, contextToken, token) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package bearertoken | ||
|
||
import ( | ||
"errors" | ||
"net/http" | ||
) | ||
|
||
// transport implements the http.RoundTripper interface, | ||
// itself wrapping an instance of http.RoundTripper. | ||
type transport struct { | ||
defaultToken string | ||
allowOverrideFromCtx bool | ||
wrapped http.RoundTripper | ||
} | ||
albertteoh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Option sets attributes of this transport. | ||
type Option func(*transport) | ||
|
||
// WithAllowOverrideFromCtx sets whether the defaultToken can be overridden | ||
// with the token within the request context. | ||
func WithAllowOverrideFromCtx(allow bool) Option { | ||
return func(t *transport) { | ||
t.allowOverrideFromCtx = allow | ||
} | ||
} | ||
|
||
// WithToken sets the defaultToken that will be injected into the outbound HTTP | ||
// 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I took the functional options approach because the Let me know if you disagree with the approach. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
return func(t *transport) { | ||
t.defaultToken = token | ||
} | ||
} | ||
|
||
// 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
t := &transport{wrapped: roundTripper} | ||
for _, opt := range opts { | ||
opt(t) | ||
} | ||
return t | ||
} | ||
|
||
// RoundTrip injects the outbound Authorization header with the | ||
// token provided in the inbound request. | ||
func (tr *transport) RoundTrip(r *http.Request) (*http.Response, error) { | ||
if tr.wrapped == nil { | ||
return nil, errors.New("no http.RoundTripper provided") | ||
} | ||
token := tr.defaultToken | ||
if tr.allowOverrideFromCtx { | ||
headerToken, _ := GetBearerToken(r.Context()) | ||
if headerToken != "" { | ||
token = headerToken | ||
} | ||
} | ||
r.Header.Set("Authorization", "Bearer "+token) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to check for There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
return tr.wrapped.RoundTrip(r) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package bearertoken | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
type roundTripFunc func(r *http.Request) (*http.Response, error) | ||
|
||
func (s roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { | ||
return s(r) | ||
} | ||
|
||
func TestNewTransport(t *testing.T) { | ||
for _, tc := range []struct { | ||
name string | ||
roundTripper http.RoundTripper | ||
requestContext context.Context | ||
options []Option | ||
wantError bool | ||
}{ | ||
{ | ||
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")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
return &http.Response{ | ||
StatusCode: http.StatusOK, | ||
}, nil | ||
}), | ||
requestContext: ContextWithBearerToken(context.Background(), "tokenFromContext"), | ||
}, | ||
{ | ||
name: "Allow override from context provided, and request context set should use request context token", | ||
roundTripper: roundTripFunc(func(r *http.Request) (*http.Response, error) { | ||
assert.Equal(t, "Bearer tokenFromContext", r.Header.Get("Authorization")) | ||
return &http.Response{ | ||
StatusCode: http.StatusOK, | ||
}, nil | ||
}), | ||
requestContext: ContextWithBearerToken(context.Background(), "tokenFromContext"), | ||
options: []Option{ | ||
WithAllowOverrideFromCtx(true), | ||
}, | ||
}, | ||
{ | ||
name: "Allow override from context and token provided, and request context unset should use defaultToken", | ||
roundTripper: roundTripFunc(func(r *http.Request) (*http.Response, error) { | ||
assert.Equal(t, "Bearer initToken", r.Header.Get("Authorization")) | ||
return &http.Response{}, nil | ||
}), | ||
requestContext: context.Background(), | ||
options: []Option{ | ||
WithAllowOverrideFromCtx(true), | ||
WithToken("initToken"), | ||
}, | ||
}, | ||
{ | ||
name: "Allow override from context and token provided, and request context set should use context token", | ||
roundTripper: roundTripFunc(func(r *http.Request) (*http.Response, error) { | ||
assert.Equal(t, "Bearer tokenFromContext", r.Header.Get("Authorization")) | ||
return &http.Response{}, nil | ||
}), | ||
requestContext: ContextWithBearerToken(context.Background(), "tokenFromContext"), | ||
options: []Option{ | ||
WithAllowOverrideFromCtx(true), | ||
WithToken("initToken"), | ||
}, | ||
}, | ||
{ | ||
name: "Nil roundTripper provided should return an error", | ||
requestContext: context.Background(), | ||
wantError: true, | ||
}, | ||
} { | ||
t.Run(tc.name, func(t *testing.T) { | ||
server := httptest.NewServer(nil) | ||
defer server.Close() | ||
req, err := http.NewRequestWithContext(tc.requestContext, "GET", server.URL, nil) | ||
require.NoError(t, err) | ||
|
||
tr := NewTransport(tc.roundTripper, tc.options...) | ||
resp, err := tr.RoundTrip(req) | ||
|
||
if tc.wantError { | ||
assert.Nil(t, resp) | ||
assert.Error(t, err) | ||
} else { | ||
assert.NotNil(t, resp) | ||
assert.NoError(t, err) | ||
} | ||
}) | ||
} | ||
} |
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 don't see
Key
being used outside of this package