diff --git a/.chloggen/jpkroehling-add-query-params-to-authcontext-api.yaml b/.chloggen/jpkroehling-add-query-params-to-authcontext-api.yaml new file mode 100644 index 00000000000..94bf713c523 --- /dev/null +++ b/.chloggen/jpkroehling-add-query-params-to-authcontext-api.yaml @@ -0,0 +1,4 @@ +change_type: 'enhancement' +component: confighttp +note: Add option to include query params in auth context +issues: [4806] diff --git a/.chloggen/jpkroehling-add-query-params-to-authcontext.yaml b/.chloggen/jpkroehling-add-query-params-to-authcontext.yaml new file mode 100644 index 00000000000..f59c916f88f --- /dev/null +++ b/.chloggen/jpkroehling-add-query-params-to-authcontext.yaml @@ -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] diff --git a/config/confighttp/README.md b/config/confighttp/README.md index 2a6b0e7fd64..d72a3805953 100644 --- a/config/confighttp/README.md +++ b/config/confighttp/README.md @@ -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" @@ -94,6 +95,8 @@ receivers: http: include_metadata: true auth: + request_params: + - token authenticator: some-authenticator-extension cors: allowed_origins: diff --git a/config/confighttp/confighttp.go b/config/confighttp/confighttp.go index d787ca9c270..f53bedbb008 100644 --- a/config/confighttp/confighttp.go +++ b/config/confighttp/confighttp.go @@ -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"` @@ -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) @@ -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 { @@ -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 diff --git a/config/confighttp/confighttp_test.go b/config/confighttp/confighttp_test.go index 05261b23d3a..04bafc5234a 100644 --- a/config/confighttp/confighttp_test.go +++ b/config/confighttp/confighttp_test.go @@ -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, + }, }, } @@ -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, + }, }, } @@ -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, + }, }, } @@ -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{ @@ -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 diff --git a/extension/auth/server.go b/extension/auth/server.go index 6e552e4285d..4fd3a550c01 100644 --- a/extension/auth/server.go +++ b/extension/auth/server.go @@ -18,7 +18,7 @@ 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. @@ -26,7 +26,7 @@ type Server interface { // 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 { @@ -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. diff --git a/extension/zpagesextension/zpagesextension_test.go b/extension/zpagesextension/zpagesextension_test.go index 60bbc1b61db..7b188b2d769 100644 --- a/extension/zpagesextension/zpagesextension_test.go +++ b/extension/zpagesextension/zpagesextension_test.go @@ -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"), + }, }, }, }