Skip to content

Commit

Permalink
[confighttp] Add option to include query params in auth context (#10445)
Browse files Browse the repository at this point in the history
This PR adds a new option to the ServerConfig's Auth option, allowing
users to specify a list of query parameters to add to the sources of
auth data, in addition to HTTP headers. Instead of simply adding all
parameters, which might be numeruous, we require users to specify which
ones to include.

Auth extensions don't need to be changed, but should document which
attributes they expect to find in the context and how to configure
confighttp to accomplish that.

Fixes #4806

Signed-off-by: Juraci Paixão Kröhling <juraci@kroehling.de>

---------

Signed-off-by: Juraci Paixão Kröhling <juraci@kroehling.de>
  • Loading branch information
jpkrohling authored Jul 19, 2024
1 parent e590ed1 commit 7d5b1ba
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
change_type: 'enhancement'
component: confighttp
note: Add option to include query params in auth context
issues: [4806]
10 changes: 10 additions & 0 deletions .chloggen/jpkroehling-add-query-params-to-authcontext.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
change_type: 'breaking'
component: confighttp
note: Auth data type signature has changed
subtext: |
As part of the linked PR, the `auth` attribute was moved from `configauth.Authentication`
to a new `AuthConfig`, which contains a `configauth.Authentication`. For end-users, this
is a non-breaking change. For users of the API, create a new AuthConfig using the
`configauth.Authentication` instance that was being used before.
issues: [4806]
change_logs: [api]
3 changes: 3 additions & 0 deletions config/confighttp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ will not be enabled.
- `compression_algorithms`: configures the list of compression algorithms the server can accept. Default: ["", "gzip", "zstd", "zlib", "snappy", "deflate"]
- [`tls`](../configtls/README.md)
- [`auth`](../configauth/README.md)
- `request_params`: a list of query parameter names to add to the auth context, along with the HTTP headers

You can enable [`attribute processor`][attribute-processor] to append any http header to span's attribute using custom key. You also need to enable the "include_metadata"

Expand All @@ -94,6 +95,8 @@ receivers:
http:
include_metadata: true
auth:
request_params:
- token
authenticator: some-authenticator-extension
cors:
allowed_origins:
Expand Down
24 changes: 20 additions & 4 deletions config/confighttp/confighttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ type ServerConfig struct {
CORS *CORSConfig `mapstructure:"cors"`

// Auth for this receiver
Auth *configauth.Authentication `mapstructure:"auth"`
Auth *AuthConfig `mapstructure:"auth"`

// MaxRequestBodySize sets the maximum request body size in bytes. Default: 20MiB.
MaxRequestBodySize int64 `mapstructure:"max_request_body_size"`
Expand All @@ -308,6 +308,15 @@ type ServerConfig struct {
CompressionAlgorithms []string `mapstructure:"compression_algorithms"`
}

type AuthConfig struct {
// Auth for this receiver.
*configauth.Authentication `mapstructure:"-"`

// RequestParameters is a list of parameters that should be extracted from the request and added to the context.
// When a parameter is found in both the query string and the header, the value from the query string will be used.
RequestParameters []string `mapstructure:"request_params"`
}

// ToListener creates a net.Listener.
func (hss *ServerConfig) ToListener(ctx context.Context) (net.Listener, error) {
listener, err := net.Listen("tcp", hss.Endpoint)
Expand Down Expand Up @@ -387,7 +396,7 @@ func (hss *ServerConfig) ToServer(_ context.Context, host component.Host, settin
return nil, err
}

handler = authInterceptor(handler, server)
handler = authInterceptor(handler, server, hss.Auth.RequestParameters)
}

if hss.CORS != nil && len(hss.CORS.AllowedOrigins) > 0 {
Expand Down Expand Up @@ -467,9 +476,16 @@ type CORSConfig struct {
MaxAge int `mapstructure:"max_age"`
}

func authInterceptor(next http.Handler, server auth.Server) http.Handler {
func authInterceptor(next http.Handler, server auth.Server, requestParams []string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, err := server.Authenticate(r.Context(), r.Header)
sources := r.Header
query := r.URL.Query()
for _, param := range requestParams {
if val, ok := query[param]; ok {
sources[param] = val
}
}
ctx, err := server.Authenticate(r.Context(), sources)
if err != nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
Expand Down
66 changes: 58 additions & 8 deletions config/confighttp/confighttp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -865,8 +865,10 @@ func TestHttpCorsWithSettings(t *testing.T) {
CORS: &CORSConfig{
AllowedOrigins: []string{"*"},
},
Auth: &configauth.Authentication{
AuthenticatorID: mockID,
Auth: &AuthConfig{
Authentication: &configauth.Authentication{
AuthenticatorID: mockID,
},
},
}

Expand Down Expand Up @@ -1168,8 +1170,10 @@ func TestServerAuth(t *testing.T) {
authCalled := false
hss := ServerConfig{
Endpoint: "localhost:0",
Auth: &configauth.Authentication{
AuthenticatorID: mockID,
Auth: &AuthConfig{
Authentication: &configauth.Authentication{
AuthenticatorID: mockID,
},
},
}

Expand Down Expand Up @@ -1202,8 +1206,10 @@ func TestServerAuth(t *testing.T) {

func TestInvalidServerAuth(t *testing.T) {
hss := ServerConfig{
Auth: &configauth.Authentication{
AuthenticatorID: nonExistingID,
Auth: &AuthConfig{
Authentication: &configauth.Authentication{
AuthenticatorID: nonExistingID,
},
},
}

Expand All @@ -1216,8 +1222,10 @@ func TestFailedServerAuth(t *testing.T) {
// prepare
hss := ServerConfig{
Endpoint: "localhost:0",
Auth: &configauth.Authentication{
AuthenticatorID: mockID,
Auth: &AuthConfig{
Authentication: &configauth.Authentication{
AuthenticatorID: mockID,
},
},
}
host := &mockHost{
Expand Down Expand Up @@ -1390,6 +1398,48 @@ func TestDefaultMaxRequestBodySize(t *testing.T) {
}
}

func TestAuthWithQueryParams(t *testing.T) {
// prepare
authCalled := false
hss := ServerConfig{
Endpoint: "localhost:0",
Auth: &AuthConfig{
RequestParameters: []string{"auth"},
Authentication: &configauth.Authentication{
AuthenticatorID: mockID,
},
},
}

host := &mockHost{
ext: map[component.ID]component.Component{
mockID: auth.NewServer(
auth.WithServerAuthenticate(func(ctx context.Context, sources map[string][]string) (context.Context, error) {
require.Len(t, sources, 1)
assert.Equal(t, "1", sources["auth"][0])
authCalled = true
return ctx, nil
}),
),
},
}

handlerCalled := false
handler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
handlerCalled = true
})

srv, err := hss.ToServer(context.Background(), host, componenttest.NewNopTelemetrySettings(), handler)
require.NoError(t, err)

// test
srv.Handler.ServeHTTP(&httptest.ResponseRecorder{}, httptest.NewRequest("GET", "/?auth=1", nil))

// verify
assert.True(t, handlerCalled)
assert.True(t, authCalled)
}

type mockHost struct {
component.Host
ext map[component.ID]component.Component
Expand Down
12 changes: 6 additions & 6 deletions extension/auth/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ import (
type Server interface {
extension.Extension

// Authenticate checks whether the given headers map contains valid auth data. Successfully authenticated calls will always return a nil error.
// Authenticate checks whether the given map contains valid auth data. Successfully authenticated calls will always return a nil error.
// When the authentication fails, an error must be returned and the caller must not retry. This function is typically called from interceptors,
// on behalf of receivers, but receivers can still call this directly if the usage of interceptors isn't suitable.
// The deadline and cancellation given to this function must be respected, but note that authentication data has to be part of the map, not context.
// The resulting context should contain the authentication data, such as the principal/username, group membership (if available), and the raw
// authentication data (if possible). This will allow other components in the pipeline to make decisions based on that data, such as routing based
// on tenancy as determined by the group membership, or passing through the authentication data to the next collector/backend.
// The context keys to be used are not defined yet.
Authenticate(ctx context.Context, headers map[string][]string) (context.Context, error)
Authenticate(ctx context.Context, sources map[string][]string) (context.Context, error)
}

type defaultServer struct {
Expand All @@ -39,14 +39,14 @@ type defaultServer struct {
type ServerOption func(*defaultServer)

// ServerAuthenticateFunc defines the signature for the function responsible for performing the authentication based
// on the given headers map. See Server.Authenticate.
type ServerAuthenticateFunc func(ctx context.Context, headers map[string][]string) (context.Context, error)
// on the given sources map. See Server.Authenticate.
type ServerAuthenticateFunc func(ctx context.Context, sources map[string][]string) (context.Context, error)

func (f ServerAuthenticateFunc) Authenticate(ctx context.Context, headers map[string][]string) (context.Context, error) {
func (f ServerAuthenticateFunc) Authenticate(ctx context.Context, sources map[string][]string) (context.Context, error) {
if f == nil {
return ctx, nil
}
return f(ctx, headers)
return f(ctx, sources)
}

// WithServerAuthenticate specifies which function to use to perform the authentication.
Expand Down
6 changes: 4 additions & 2 deletions extension/zpagesextension/zpagesextension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ func TestZPagesExtensionBadAuthExtension(t *testing.T) {
cfg := &Config{
confighttp.ServerConfig{
Endpoint: "localhost:0",
Auth: &configauth.Authentication{
AuthenticatorID: component.MustNewIDWithName("foo", "bar"),
Auth: &confighttp.AuthConfig{
Authentication: &configauth.Authentication{
AuthenticatorID: component.MustNewIDWithName("foo", "bar"),
},
},
},
}
Expand Down

0 comments on commit 7d5b1ba

Please sign in to comment.