diff --git a/config/sample_webconfig.conf b/config/sample_webconfig.conf index c336ac4..04eebcb 100644 --- a/config/sample_webconfig.conf +++ b/config/sample_webconfig.conf @@ -155,6 +155,13 @@ webconfig { server_api_token_auth { enabled = true } + + // Protect /config with API token authentication. + // Defaults to false to preserve existing endpoint access. + config_api_token_auth { + enabled = false + } + device_api_token_auth { enabled = true } diff --git a/http/router.go b/http/router.go index 103c77a..d3e621c 100644 --- a/http/router.go +++ b/http/router.go @@ -32,6 +32,9 @@ func (s *WebconfigServer) AddBaseRoutes(testOnly bool, router *mux.Router) { r2.HandleFunc("", s.VersionHandler).Methods("GET") r3 := router.Path("/config").Subrouter() + if s.ConfigApiTokenAuthEnabled() { + r3.Use(s.ApiMiddleware) + } r3.HandleFunc("", s.ServerConfigHandler).Methods("GET") if s.TokenApiEnabled() { diff --git a/http/webconfig_server.go b/http/webconfig_server.go index dcdaf30..0da72bf 100644 --- a/http/webconfig_server.go +++ b/http/webconfig_server.go @@ -64,6 +64,7 @@ const ( MetricsEnabledDefault = true FactoryResetEnabledDefault = false serverApiTokenAuthEnabledDefault = true + configApiTokenAuthEnabledDefault = false deviceApiTokenAuthEnabledDefault = true tokenApiEnabledDefault = false activeDriverDefault = "cassandra" @@ -109,6 +110,7 @@ type WebconfigServer struct { metricsEnabled bool factoryResetEnabled bool serverApiTokenAuthEnabled bool + configApiTokenAuthEnabled bool deviceApiTokenAuthEnabled bool tokenApiEnabled bool kafkaEnabled bool @@ -270,6 +272,7 @@ func NewWebconfigServer(sc *common.ServerConfig, testOnly bool) *WebconfigServer tlsConfig, _ := NewTlsConfig(conf) serverApiTokenAuthEnabled := conf.GetBoolean("webconfig.jwt.server_api_token_auth.enabled", serverApiTokenAuthEnabledDefault) + configApiTokenAuthEnabled := conf.GetBoolean("webconfig.jwt.config_api_token_auth.enabled", configApiTokenAuthEnabledDefault) if conf.GetNode("webconfig.jwt.server_api_token_auth.enabled") == nil { log.Warn("webconfig.jwt.server_api_token_auth.enabled is not set in config; defaulting to true (server API token auth enforced). See MIGRATION.md.") } @@ -399,6 +402,7 @@ func NewWebconfigServer(sc *common.ServerConfig, testOnly bool) *WebconfigServer metricsEnabled: metricsEnabled, factoryResetEnabled: factoryResetEnabled, serverApiTokenAuthEnabled: serverApiTokenAuthEnabled, + configApiTokenAuthEnabled: configApiTokenAuthEnabled, deviceApiTokenAuthEnabled: deviceApiTokenAuthEnabled, tokenApiEnabled: tokenApiEnabled, kafkaEnabled: kafkaEnabled, @@ -609,6 +613,10 @@ func (s *WebconfigServer) TestingCpeMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(fn) } +func isConfigEndpoint(r *http.Request) bool { + return r.URL.Path == "/config" +} + func (s *WebconfigServer) VerifyApiToken(tokenStr string) (bool, error) { if s.JwksEnabled() { if _, err := s.JwksManager.VerifyApiToken(tokenStr); err != nil { @@ -642,6 +650,14 @@ func (s *WebconfigServer) SetServerApiTokenAuthEnabled(enabled bool) { s.serverApiTokenAuthEnabled = enabled } +func (s *WebconfigServer) ConfigApiTokenAuthEnabled() bool { + return s.configApiTokenAuthEnabled +} + +func (s *WebconfigServer) SetConfigApiTokenAuthEnabled(enabled bool) { + s.configApiTokenAuthEnabled = enabled +} + func (s *WebconfigServer) DeviceApiTokenAuthEnabled() bool { return s.deviceApiTokenAuthEnabled } @@ -958,7 +974,9 @@ func (s *WebconfigServer) logRequestStarts(w http.ResponseWriter, r *http.Reques } tfields := common.FilterLogFields(fields) - log.WithFields(tfields).Info("Request started") + if !isConfigEndpoint(r) { + log.WithFields(tfields).Info("Request started") + } xwriter.LogDebug(r, "tracing", fmt.Sprintf("Trace final out_traceparent %s out_traceState %s", xpcTrace.OutTraceparent, xpcTrace.OutTracestate)) return xwriter @@ -1046,7 +1064,9 @@ func (s *WebconfigServer) logRequestEnds(xw *XResponseWriter, r *http.Request) { s.XpcTracer.SetSpan(fields, s.XpcTracer.MoracideTagPrefix()) tfields := common.FilterLogFields(fields) - log.WithFields(tfields).Info("Request finished") + if !isConfigEndpoint(r) { + log.WithFields(tfields).Info("Request finished") + } } func LogError(w http.ResponseWriter, err error) { diff --git a/http/webconfig_server_test.go b/http/webconfig_server_test.go index bc57955..92d3e1a 100644 --- a/http/webconfig_server_test.go +++ b/http/webconfig_server_test.go @@ -18,8 +18,11 @@ package http import ( + "net/http" + "strings" "testing" + log "github.com/sirupsen/logrus" "gotest.tools/assert" ) @@ -27,9 +30,66 @@ func TestApiTokenAuthSecureDefaults(t *testing.T) { // Admin write endpoints (document, rootdocument, poke, reference) MUST // be guarded by ApiMiddleware out of the box. Regression guard for f003. assert.Assert(t, serverApiTokenAuthEnabledDefault, "server API token auth must default to enabled") + assert.Assert(t, !configApiTokenAuthEnabledDefault, "config API token auth must default to disabled") assert.Assert(t, deviceApiTokenAuthEnabledDefault, "device API token auth must default to enabled") } +func TestConfigEndpointRemainsUnauthenticatedByDefault(t *testing.T) { + server := NewWebconfigServer(sc, true) + assert.Assert(t, !server.ConfigApiTokenAuthEnabled()) + router := server.GetRouter(false) + + req, err := http.NewRequest("GET", "/config", nil) + assert.NilError(t, err) + res := ExecuteRequest(req, router).Result() + assert.Equal(t, res.StatusCode, http.StatusOK) +} + +func TestConfigEndpointRequiresApiTokenWhenEnabled(t *testing.T) { + server := NewWebconfigServer(sc, true) + server.SetConfigApiTokenAuthEnabled(true) + assert.Assert(t, server.ConfigApiTokenAuthEnabled()) + router := server.GetRouter(false) + + req, err := http.NewRequest("GET", "/config", nil) + assert.NilError(t, err) + res := ExecuteRequest(req, router).Result() + assert.Equal(t, res.StatusCode, http.StatusForbidden) +} + +func TestApiMiddlewareSuppressesConfigRequestLogs(t *testing.T) { + server := NewWebconfigServer(sc, true) + server.SetConfigApiTokenAuthEnabled(true) + router := server.GetRouter(false) + + var logs strings.Builder + previousOutput := log.StandardLogger().Out + previousLevel := log.StandardLogger().Level + log.SetOutput(&logs) + log.SetLevel(log.InfoLevel) + defer func() { + log.SetOutput(previousOutput) + log.SetLevel(previousLevel) + }() + + req, err := http.NewRequest("GET", "/config", nil) + assert.NilError(t, err) + res := ExecuteRequest(req, router).Result() + assert.Equal(t, res.StatusCode, http.StatusForbidden) + assert.Assert(t, !strings.Contains(logs.String(), "Request started")) + assert.Assert(t, !strings.Contains(logs.String(), "Request finished")) + + logs.Reset() + req, err = http.NewRequest("GET", "/other", nil) + assert.NilError(t, err) + res = ExecuteRequest(req, server.ApiMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }))).Result() + assert.Equal(t, res.StatusCode, http.StatusForbidden) + assert.Assert(t, strings.Contains(logs.String(), "Request started")) + assert.Assert(t, strings.Contains(logs.String(), "Request finished")) +} + func TestWebconfigServerSetterGetter(t *testing.T) { server := NewWebconfigServer(sc, true) @@ -49,6 +109,14 @@ func TestWebconfigServerSetterGetter(t *testing.T) { server.SetServerApiTokenAuthEnabled(enabled) assert.Equal(t, server.ServerApiTokenAuthEnabled(), enabled) + // config api token auth + enabled = true + server.SetConfigApiTokenAuthEnabled(enabled) + assert.Equal(t, server.ConfigApiTokenAuthEnabled(), enabled) + enabled = false + server.SetConfigApiTokenAuthEnabled(enabled) + assert.Equal(t, server.ConfigApiTokenAuthEnabled(), enabled) + // device api token auth enabled = true server.SetDeviceApiTokenAuthEnabled(enabled)