diff --git a/CHANGELOG.md b/CHANGELOG.md index 112e3ac7ed2..674477b8a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## 💡 Enhancements 💡 - Allow more zap logger configs: `disable_caller`, `disable_stacktrace`, `output_paths`, `error_output_paths`, `initial_fields` (#1048) +- `configauth`: add ServerAuthenticator interfaces for HTTP receivers. (#4506) ## v0.41.0 Beta diff --git a/config/configauth/mock_serverauth.go b/config/configauth/mock_serverauth.go index 3fdd735fe1e..09eab9a744f 100644 --- a/config/configauth/mock_serverauth.go +++ b/config/configauth/mock_serverauth.go @@ -16,6 +16,7 @@ package configauth // import "go.opentelemetry.io/collector/config/configauth" import ( "context" + "net/http" "google.golang.org/grpc" @@ -31,6 +32,9 @@ var ( type MockServerAuthenticator struct { // AuthenticateFunc to use during the authentication phase of this mock. Optional. AuthenticateFunc AuthenticateFunc + + // HTTPInterceptor to use in the test + HTTPInterceptorFunc HTTPInterceptorFunc // TODO: implement the other funcs } @@ -52,6 +56,14 @@ func (m *MockServerAuthenticator) GRPCStreamServerInterceptor(interface{}, grpc. return nil } +// HTTPInterceptor isn't currently implemented and always returns nil. +func (m *MockServerAuthenticator) HTTPInterceptor(next http.Handler) http.Handler { + if m.HTTPInterceptorFunc == nil { + return next + } + return m.HTTPInterceptorFunc(next, m.AuthenticateFunc) +} + // Start isn't currently implemented and always returns nil. func (m *MockServerAuthenticator) Start(context.Context, component.Host) error { return nil diff --git a/config/configauth/serverauth.go b/config/configauth/serverauth.go index 55aab963841..4befd7038e8 100644 --- a/config/configauth/serverauth.go +++ b/config/configauth/serverauth.go @@ -17,6 +17,7 @@ package configauth // import "go.opentelemetry.io/collector/config/configauth" import ( "context" "errors" + "net/http" "google.golang.org/grpc" "google.golang.org/grpc/metadata" @@ -60,6 +61,11 @@ type ServerAuthenticator interface { // Once the authentication succeeds, the interceptor is expected to call the handler. // See https://pkg.go.dev/google.golang.org/grpc#StreamServerInterceptor. GRPCStreamServerInterceptor(srv interface{}, stream grpc.ServerStream, srvInfo *grpc.StreamServerInfo, handler grpc.StreamHandler) error + + // HTTPInterceptor is a helper method to provide an HTTP handler responsible for intercepting the incoming HTTP requests, using the + // request's meta data as source of data for the authentication. Once the authentication succeeds, the interceptor is expected to call + // the next handler. + HTTPInterceptor(next http.Handler) http.Handler } // AuthenticateFunc defines the signature for the function responsible for performing the authentication based on the given headers map. @@ -76,6 +82,10 @@ type GRPCUnaryInterceptorFunc func(ctx context.Context, req interface{}, info *g // See ServerAuthenticator.GRPCStreamServerInterceptor. type GRPCStreamInterceptorFunc func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler, authenticate AuthenticateFunc) error +// HTTPInterceptorFunc defines the signature for the function intercepting HTTP calls, useful for authenticators to use as +// types for internal structs, making it easier to mock them in tests. +type HTTPInterceptorFunc func(handler http.Handler, authenticate AuthenticateFunc) http.Handler + // DefaultGRPCUnaryServerInterceptor provides a default implementation of GRPCUnaryInterceptorFunc, useful for most authenticators. // It extracts the headers from the incoming request, under the assumption that the credentials will be part of the resulting map. func DefaultGRPCUnaryServerInterceptor(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler, authenticate AuthenticateFunc) (interface{}, error) { @@ -110,3 +120,17 @@ func DefaultGRPCStreamServerInterceptor(srv interface{}, stream grpc.ServerStrea wrapped.WrappedContext = ctx return handler(srv, wrapped) } + +// DefaultHTTPInterceptor provides a default implementation of HTTPInterceptorFunc, useful for most authenticators. +// It passes the headers from the incoming request as it is, under the assumption that the credentials are part of it. +func DefaultHTTPInterceptor(next http.Handler, authenticate AuthenticateFunc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, err := authenticate(r.Context(), r.Header) + if err != nil { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/config/confighttp/confighttp.go b/config/confighttp/confighttp.go index ffcb24685f6..57ca7f98cb0 100644 --- a/config/confighttp/confighttp.go +++ b/config/confighttp/confighttp.go @@ -187,6 +187,9 @@ type HTTPServerSettings struct { // CORS configures the server for HTTP cross-origin resource sharing (CORS). CORS *CORSSettings `mapstructure:"cors,omitempty"` + + // Auth for this receiver + Auth *configauth.Authentication `mapstructure:"auth,omitempty"` } // ToListener creates a net.Listener. @@ -226,7 +229,7 @@ func WithErrorHandler(e middleware.ErrorHandler) ToServerOption { } // ToServer creates an http.Server from settings object. -func (hss *HTTPServerSettings) ToServer(_ component.Host, settings component.TelemetrySettings, handler http.Handler, opts ...ToServerOption) (*http.Server, error) { +func (hss *HTTPServerSettings) ToServer(host component.Host, settings component.TelemetrySettings, handler http.Handler, opts ...ToServerOption) (*http.Server, error) { serverOpts := &toServerOptions{} for _, o := range opts { o(serverOpts) @@ -248,6 +251,15 @@ func (hss *HTTPServerSettings) ToServer(_ component.Host, settings component.Tel } // TODO: emit a warning when non-empty CorsHeaders and empty CorsOrigins. + if hss.Auth != nil { + authenticator, err := hss.Auth.GetServerAuthenticator(host.GetExtensions()) + if err != nil { + return nil, err + } + + handler = authenticator.HTTPInterceptor(handler) + } + // Enable OpenTelemetry observability plugin. // TODO: Consider to use component ID string as prefix for all the operations. handler = otelhttp.NewHandler( diff --git a/config/confighttp/confighttp_test.go b/config/confighttp/confighttp_test.go index e39d8bf287a..af5efa1a35d 100644 --- a/config/confighttp/confighttp_test.go +++ b/config/confighttp/confighttp_test.go @@ -15,6 +15,7 @@ package confighttp import ( + "context" "errors" "fmt" "io/ioutil" @@ -724,3 +725,60 @@ func TestContextWithClient(t *testing.T) { }) } } + +func TestServerAuth(t *testing.T) { + // prepare + authCalled := false + hss := HTTPServerSettings{ + Auth: &configauth.Authentication{ + AuthenticatorID: config.NewComponentID("mock"), + }, + } + host := &mockHost{ + ext: map[config.ComponentID]component.Extension{ + config.NewComponentID("mock"): &configauth.MockServerAuthenticator{ + AuthenticateFunc: func(ctx context.Context, headers map[string][]string) (context.Context, error) { + authCalled = true + return ctx, nil + }, + HTTPInterceptorFunc: configauth.DefaultHTTPInterceptor, + }, + }, + } + + handlerCalled := false + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + }) + + srv, err := hss.ToServer(host, componenttest.NewNopTelemetrySettings(), handler) + require.NoError(t, err) + + // test + srv.Handler.ServeHTTP(&httptest.ResponseRecorder{}, httptest.NewRequest("GET", "/", nil)) + + // verify + assert.True(t, handlerCalled) + assert.True(t, authCalled) +} + +func TestInvalidServerAuth(t *testing.T) { + hss := HTTPServerSettings{ + Auth: &configauth.Authentication{ + AuthenticatorID: config.NewComponentID("non-existing"), + }, + } + + srv, err := hss.ToServer(componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings(), http.NewServeMux()) + require.Error(t, err) + require.Nil(t, srv) +} + +type mockHost struct { + component.Host + ext map[config.ComponentID]component.Extension +} + +func (nh *mockHost) GetExtensions() map[config.ComponentID]component.Extension { + return nh.ext +}