From 2cbc1b0917112fe2e796fd432abe31fe708550cd Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 22 Apr 2026 23:16:20 -0400 Subject: [PATCH] feat(config): add HTTP basic auth for metrics Signed-off-by: Xe Iaso --- lib/config/metrics.go | 65 ++++++++++++++++++++++++--------- lib/config/metrics_test.go | 73 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 16 deletions(-) diff --git a/lib/config/metrics.go b/lib/config/metrics.go index a4fcc7fb..27316808 100644 --- a/lib/config/metrics.go +++ b/lib/config/metrics.go @@ -10,25 +10,29 @@ import ( ) var ( - ErrInvalidMetricsConfig = errors.New("config: invalid metrics configuration") - ErrInvalidMetricsTLSConfig = errors.New("config: invalid metrics TLS configuration") - ErrNoMetricsBind = errors.New("config.Metrics: must define bind") - ErrNoMetricsNetwork = errors.New("config.Metrics: must define network") - ErrNoMetricsSocketMode = errors.New("config.Metrics: must define socket mode when using unix sockets") - ErrInvalidMetricsSocketMode = errors.New("config.Metrics: invalid unix socket mode") - ErrInvalidMetricsNetwork = errors.New("config.Metrics: invalid metrics network") - ErrNoMetricsTLSCertificate = errors.New("config.Metrics.TLS: must define certificate file") - ErrNoMetricsTLSKey = errors.New("config.Metrics.TLS: must define key file") - ErrInvalidMetricsTLSKeypair = errors.New("config.Metrics.TLS: keypair is invalid") - ErrInvalidMetricsCACertificate = errors.New("config.Metrics.TLS: invalid CA certificate") - ErrCantReadFile = errors.New("config: can't read required file") + ErrInvalidMetricsConfig = errors.New("config: invalid metrics configuration") + ErrInvalidMetricsTLSConfig = errors.New("config: invalid metrics TLS configuration") + ErrInvalidMetricsBasicAuthConfig = errors.New("config: invalid metrics basic auth configuration") + ErrNoMetricsBind = errors.New("config.Metrics: must define bind") + ErrNoMetricsNetwork = errors.New("config.Metrics: must define network") + ErrNoMetricsSocketMode = errors.New("config.Metrics: must define socket mode when using unix sockets") + ErrInvalidMetricsSocketMode = errors.New("config.Metrics: invalid unix socket mode") + ErrInvalidMetricsNetwork = errors.New("config.Metrics: invalid metrics network") + ErrNoMetricsTLSCertificate = errors.New("config.Metrics.TLS: must define certificate file") + ErrNoMetricsTLSKey = errors.New("config.Metrics.TLS: must define key file") + ErrInvalidMetricsTLSKeypair = errors.New("config.Metrics.TLS: keypair is invalid") + ErrInvalidMetricsCACertificate = errors.New("config.Metrics.TLS: invalid CA certificate") + ErrCantReadFile = errors.New("config: can't read required file") + ErrNoMetricsBasicAuthUsername = errors.New("config.Metrics.BasicAuth: must define username") + ErrNoMetricsBasicAuthPassword = errors.New("config.Metrics.BasicAuth: must define password") ) type Metrics struct { - Bind string `json:"bind" yaml:"bind"` - Network string `json:"network" yaml:"network"` - SocketMode string `json:"socketMode" yaml:"socketMode"` - TLS *MetricsTLS `json:"tls" yaml:"tls"` + Bind string `json:"bind" yaml:"bind"` + Network string `json:"network" yaml:"network"` + SocketMode string `json:"socketMode" yaml:"socketMode"` + TLS *MetricsTLS `json:"tls" yaml:"tls"` + BasicAuth *MetricsBasicAuth `json:"basicAuth" yaml:"basicAuth"` } func (m *Metrics) Valid() error { @@ -62,6 +66,12 @@ func (m *Metrics) Valid() error { } } + if m.BasicAuth != nil { + if err := m.BasicAuth.Valid(); err != nil { + errs = append(errs, err) + } + } + if len(errs) != 0 { return errors.Join(ErrInvalidMetricsConfig, errors.Join(errs...)) } @@ -131,3 +141,26 @@ func canReadFile(fname string) error { return nil } + +type MetricsBasicAuth struct { + Username string `json:"username" yaml:"username"` + Password string `json:"password" yaml:"password"` +} + +func (mba *MetricsBasicAuth) Valid() error { + var errs []error + + if mba.Username == "" { + errs = append(errs, ErrNoMetricsBasicAuthUsername) + } + + if mba.Password == "" { + errs = append(errs, ErrNoMetricsBasicAuthPassword) + } + + if len(errs) != 0 { + return errors.Join(ErrInvalidMetricsBasicAuthConfig, errors.Join(errs...)) + } + + return nil +} diff --git a/lib/config/metrics_test.go b/lib/config/metrics_test.go index 2e3335f5..c4876caa 100644 --- a/lib/config/metrics_test.go +++ b/lib/config/metrics_test.go @@ -157,6 +157,79 @@ func TestMetricsValid(t *testing.T) { }, err: ErrInvalidMetricsCACertificate, }, + { + name: "basic auth credentials set", + input: &Metrics{ + Bind: ":9090", + Network: "tcp", + BasicAuth: &MetricsBasicAuth{ + Username: "admin", + Password: "hunter2", + }, + }, + }, + { + name: "invalid basic auth config", + input: &Metrics{ + Bind: ":9090", + Network: "tcp", + BasicAuth: &MetricsBasicAuth{}, + }, + err: ErrInvalidMetricsBasicAuthConfig, + }, + } { + t.Run(tt.name, func(t *testing.T) { + if err := tt.input.Valid(); !errors.Is(err, tt.err) { + t.Logf("wanted error: %v", tt.err) + t.Logf("got error: %v", err) + t.Error("validation failed") + } + }) + } +} + +func TestMetricsBasicAuthValid(t *testing.T) { + for _, tt := range []struct { + name string + input *MetricsBasicAuth + err error + }{ + { + name: "both set", + input: &MetricsBasicAuth{ + Username: "admin", + Password: "hunter2", + }, + }, + { + name: "empty username and password", + input: &MetricsBasicAuth{}, + err: ErrInvalidMetricsBasicAuthConfig, + }, + { + name: "missing username", + input: &MetricsBasicAuth{ + Password: "hunter2", + }, + err: ErrNoMetricsBasicAuthUsername, + }, + { + name: "missing password", + input: &MetricsBasicAuth{ + Username: "admin", + }, + err: ErrNoMetricsBasicAuthPassword, + }, + { + name: "missing both surfaces wrapper error", + input: &MetricsBasicAuth{}, + err: ErrNoMetricsBasicAuthUsername, + }, + { + name: "missing both surfaces password error", + input: &MetricsBasicAuth{}, + err: ErrNoMetricsBasicAuthPassword, + }, } { t.Run(tt.name, func(t *testing.T) { if err := tt.input.Valid(); !errors.Is(err, tt.err) {