From 8efb8fa9a83e58944293f38036ef40669fd26307 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 01:00:32 -0400 Subject: [PATCH 1/4] Bump github.com/prometheus/client_golang from 1.20.4 to 1.20.5 (#960) Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.4 to 1.20.5. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.20.4...v1.20.5) --- updated-dependencies: - dependency-name: github.com/prometheus/client_golang dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e764e160..40938377 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( github.com/gomodule/redigo v1.9.2 github.com/mna/redisc v1.4.0 - github.com/prometheus/client_golang v1.20.4 + github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 github.com/sirupsen/logrus v1.9.3 ) diff --git a/go.sum b/go.sum index 7976fa07..f157d33d 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= From d9ccc72f2061857f41585768d2318ae5b765cfdf Mon Sep 17 00:00:00 2001 From: Nicolai Antiferov <75987872+nantiferov@users.noreply.github.com> Date: Thu, 31 Oct 2024 06:04:21 +0200 Subject: [PATCH 2/4] Hotfix: Change type of search_total_cycles/search_total_ms_run metrics (#962) Initially added in https://github.com/oliver006/redis_exporter/pull/953 as gauges, but usage shows that they're counters. Related: #942 --- exporter/exporter.go | 4 ++-- exporter/modules_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exporter/exporter.go b/exporter/exporter.go index f0252d4c..7473c8f6 100644 --- a/exporter/exporter.go +++ b/exporter/exporter.go @@ -299,8 +299,6 @@ func NewRedisExporter(uri string, opts Options) (*Exporter, error) { "search_global_idle": "search_global_idle", "search_global_total": "search_global_total", "search_bytes_collected": "search_collected_bytes", - "search_total_cycles": "search_total_cycles", - "search_total_ms_run": "search_total_run_ms", "search_dialect_1": "search_dialect_1", "search_dialect_2": "search_dialect_2", "search_dialect_3": "search_dialect_3", @@ -359,6 +357,8 @@ func NewRedisExporter(uri string, opts Options) (*Exporter, error) { // Redis Modules metrics, RediSearch module "search_total_indexing_time": "search_indexing_time_ms_total", + "search_total_cycles": "search_cycles_total", + "search_total_ms_run": "search_run_ms_total", }, } diff --git a/exporter/modules_test.go b/exporter/modules_test.go index af3659a4..89900379 100644 --- a/exporter/modules_test.go +++ b/exporter/modules_test.go @@ -41,8 +41,8 @@ func TestModules(t *testing.T) { "search_global_idle": false, "search_global_total": false, "search_collected_bytes": false, - "search_total_cycles": false, - "search_total_run_ms": false, + "search_cycles_total": false, + "search_run_ms_total": false, "search_dialect_1": false, "search_dialect_2": false, "search_dialect_3": false, From 51783a590f77ee1fbd27d39cba18304dbed60207 Mon Sep 17 00:00:00 2001 From: joernchen Date: Sun, 15 Dec 2024 21:25:25 +0100 Subject: [PATCH 3/4] Add basic authentication (#967) * Add basic authentication capabilities --- README.md | 2 + exporter/exporter.go | 2 + exporter/http.go | 32 ++++++ exporter/http_test.go | 233 ++++++++++++++++++++++++++++++++++++++++++ main.go | 4 + 5 files changed, 273 insertions(+) diff --git a/README.md b/README.md index 2cc1cdd2..0201dda0 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,8 @@ Prometheus uses file watches and all changes to the json file are applied immedi | check-key-groups | REDIS_EXPORTER_CHECK_KEY_GROUPS | Comma separated list of [LUA regexes](https://www.lua.org/pil/20.1.html) for classifying keys into groups. The regexes are applied in specified order to individual keys, and the group name is generated by concatenating all capture groups of the first regex that matches a key. A key will be tracked under the `unclassified` group if none of the specified regexes matches it. | | max-distinct-key-groups | REDIS_EXPORTER_MAX_DISTINCT_KEY_GROUPS | Maximum number of distinct key groups that can be tracked independently *per Redis database*. If exceeded, only key groups with the highest memory consumption within the limit will be tracked separately, all remaining key groups will be tracked under a single `overflow` key group. | | config-command | REDIS_EXPORTER_CONFIG_COMMAND | What to use for the CONFIG command, defaults to `CONFIG`, , set to "-" to skip config metrics extraction. | +| basic-auth-username | REDIS_EXPORTER_BASIC_AUTH_USERNAME | Username for Basic Authentication with the redis exporter needs to be set together with basic-auth-password to be effective +| basic-auth-password | REDIS_EXPORTER_BASIC_AUTH_PASSWORD | Password for Basic Authentication with the redis exporter needs to be set together with basic-auth-username to be effective Redis instance addresses can be tcp addresses: `redis://localhost:6379`, `redis.example.com:6379` or e.g. unix sockets: `unix:///tmp/redis.sock`.\ SSL is supported by using the `rediss://` schema, for example: `rediss://azure-ssl-enabled-host.redis.cache.windows.net:6380` (note that the port is required when connecting to a non-standard 6379 port, e.g. with Azure Redis instances).\ diff --git a/exporter/exporter.go b/exporter/exporter.go index 7473c8f6..07cc8055 100644 --- a/exporter/exporter.go +++ b/exporter/exporter.go @@ -83,6 +83,8 @@ type Options struct { RedisPwdFile string Registry *prometheus.Registry BuildInfo BuildInfo + BasicAuthUsername string + BasicAuthPassword string } // NewRedisExporter returns a new exporter of Redis metrics. diff --git a/exporter/http.go b/exporter/http.go index 266c8ff5..1bfe5934 100644 --- a/exporter/http.go +++ b/exporter/http.go @@ -1,6 +1,8 @@ package exporter import ( + "crypto/subtle" + "errors" "fmt" "net/http" "net/url" @@ -12,6 +14,12 @@ import ( ) func (e *Exporter) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if err := e.verifyBasicAuth(r.BasicAuth()); err != nil { + w.Header().Set("WWW-Authenticate", `Basic realm="redis-exporter, charset=UTF-8"`) + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + e.mux.ServeHTTP(w, r) } @@ -107,3 +115,27 @@ func (e *Exporter) reloadPwdFile(w http.ResponseWriter, r *http.Request) { e.Unlock() _, _ = w.Write([]byte(`ok`)) } + +func (e *Exporter) isBasicAuthConfigured() bool { + return e.options.BasicAuthUsername != "" && e.options.BasicAuthPassword != "" +} + +func (e *Exporter) verifyBasicAuth(user, password string, authHeaderSet bool) error { + + if !e.isBasicAuthConfigured() { + return nil + } + + if !authHeaderSet { + return errors.New("Unauthorized") + } + + userCorrect := subtle.ConstantTimeCompare([]byte(user), []byte(e.options.BasicAuthUsername)) + passCorrect := subtle.ConstantTimeCompare([]byte(password), []byte(e.options.BasicAuthPassword)) + + if userCorrect == 0 || passCorrect == 0 { + return errors.New("Unauthorized") + } + + return nil +} diff --git a/exporter/http_test.go b/exporter/http_test.go index 236b3f95..70c709bd 100644 --- a/exporter/http_test.go +++ b/exporter/http_test.go @@ -393,6 +393,239 @@ func TestReloadHandlers(t *testing.T) { } } +func TestIsBasicAuthConfigured(t *testing.T) { + tests := []struct { + name string + username string + password string + want bool + }{ + { + name: "no credentials configured", + username: "", + password: "", + want: false, + }, + { + name: "only username configured", + username: "user", + password: "", + want: false, + }, + { + name: "only password configured", + username: "", + password: "pass", + want: false, + }, + { + name: "both credentials configured", + username: "user", + password: "pass", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e, _ := NewRedisExporter("", Options{ + BasicAuthUsername: tt.username, + BasicAuthPassword: tt.password, + }) + + if got := e.isBasicAuthConfigured(); got != tt.want { + t.Errorf("isBasicAuthConfigured() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVerifyBasicAuth(t *testing.T) { + tests := []struct { + name string + configUser string + configPass string + providedUser string + providedPass string + authHeaderSet bool + wantErr bool + wantErrString string + }{ + { + name: "no auth configured - no credentials provided", + configUser: "", + configPass: "", + providedUser: "", + providedPass: "", + authHeaderSet: false, + wantErr: false, + }, + { + name: "auth configured - no auth header", + configUser: "user", + configPass: "pass", + providedUser: "", + providedPass: "", + authHeaderSet: false, + wantErr: true, + wantErrString: "Unauthorized", + }, + { + name: "auth configured - correct credentials", + configUser: "user", + configPass: "pass", + providedUser: "user", + providedPass: "pass", + authHeaderSet: true, + wantErr: false, + }, + { + name: "auth configured - wrong username", + configUser: "user", + configPass: "pass", + providedUser: "wronguser", + providedPass: "pass", + authHeaderSet: true, + wantErr: true, + wantErrString: "Unauthorized", + }, + { + name: "auth configured - wrong password", + configUser: "user", + configPass: "pass", + providedUser: "user", + providedPass: "wrongpass", + authHeaderSet: true, + wantErr: true, + wantErrString: "Unauthorized", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e, _ := NewRedisExporter("", Options{ + BasicAuthUsername: tt.configUser, + BasicAuthPassword: tt.configPass, + }) + + err := e.verifyBasicAuth(tt.providedUser, tt.providedPass, tt.authHeaderSet) + + if (err != nil) != tt.wantErr { + t.Errorf("verifyBasicAuth() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if err != nil && err.Error() != tt.wantErrString { + t.Errorf("verifyBasicAuth() error = %v, wantErrString %v", err, tt.wantErrString) + } + }) + } +} + +func TestBasicAuth(t *testing.T) { + if os.Getenv("TEST_REDIS_URI") == "" { + t.Skipf("TEST_REDIS_URI not set - skipping") + } + + tests := []struct { + name string + username string + password string + configUsername string + configPassword string + wantStatusCode int + }{ + { + name: "No auth configured - no credentials provided", + username: "", + password: "", + configUsername: "", + configPassword: "", + wantStatusCode: http.StatusOK, + }, + { + name: "Auth configured - correct credentials", + username: "testuser", + password: "testpass", + configUsername: "testuser", + configPassword: "testpass", + wantStatusCode: http.StatusOK, + }, + { + name: "Auth configured - wrong username", + username: "wronguser", + password: "testpass", + configUsername: "testuser", + configPassword: "testpass", + wantStatusCode: http.StatusUnauthorized, + }, + { + name: "Auth configured - wrong password", + username: "testuser", + password: "wrongpass", + configUsername: "testuser", + configPassword: "testpass", + wantStatusCode: http.StatusUnauthorized, + }, + { + name: "Auth configured - no credentials provided", + username: "", + password: "", + configUsername: "testuser", + configPassword: "testpass", + wantStatusCode: http.StatusUnauthorized, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e, _ := NewRedisExporter(os.Getenv("TEST_REDIS_URI"), Options{ + Namespace: "test", + Registry: prometheus.NewRegistry(), + BasicAuthUsername: tt.configUsername, + BasicAuthPassword: tt.configPassword, + }) + ts := httptest.NewServer(e) + defer ts.Close() + + client := &http.Client{} + req, err := http.NewRequest("GET", ts.URL+"/metrics", nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + + if tt.username != "" || tt.password != "" { + req.SetBasicAuth(tt.username, tt.password) + } + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("Failed to send request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != tt.wantStatusCode { + t.Errorf("Expected status code %d, got %d", tt.wantStatusCode, resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + + if tt.wantStatusCode == http.StatusOK { + if !strings.Contains(string(body), "test_up") { + t.Errorf("Expected body to contain 'test_up', got: %s", string(body)) + } + } else { + if !strings.Contains(resp.Header.Get("WWW-Authenticate"), "Basic realm=\"redis-exporter") { + t.Errorf("Expected WWW-Authenticate header, got: %s", resp.Header.Get("WWW-Authenticate")) + } + } + }) + } +} + func downloadURL(t *testing.T, u string) string { _, res := downloadURLWithStatusCode(t, u) return res diff --git a/main.go b/main.go index fdb1bc07..1eac5f46 100644 --- a/main.go +++ b/main.go @@ -100,6 +100,8 @@ func main() { redactConfigMetrics = flag.Bool("redact-config-metrics", getEnvBool("REDIS_EXPORTER_REDACT_CONFIG_METRICS", true), "Whether to redact config settings that include potentially sensitive information like passwords") inclSystemMetrics = flag.Bool("include-system-metrics", getEnvBool("REDIS_EXPORTER_INCL_SYSTEM_METRICS", false), "Whether to include system metrics like e.g. redis_total_system_memory_bytes") skipTLSVerification = flag.Bool("skip-tls-verification", getEnvBool("REDIS_EXPORTER_SKIP_TLS_VERIFICATION", false), "Whether to to skip TLS verification") + basicAuthUsername = flag.String("basic-auth-username", getEnv("REDIS_EXPORTER_BASIC_AUTH_USERNAME", ""), "Username for basic authentication") + basicAuthPassword = flag.String("basic-auth-password", getEnv("REDIS_EXPORTER_BASIC_AUTH_PASSWORD", ""), "Password for basic authentication") ) flag.Parse() @@ -201,6 +203,8 @@ func main() { CommitSha: BuildCommitSha, Date: BuildDate, }, + BasicAuthUsername: *basicAuthUsername, + BasicAuthPassword: *basicAuthPassword, }, ) if err != nil { From e2bb7fd6af3b950efa3267e4b932531098dc06b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:27:47 -0500 Subject: [PATCH 4/4] Bump codecov/codecov-action from 4 to 5 (#965) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 50ecb447..92f05c5a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -64,7 +64,7 @@ jobs: - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: fail_ci_if_error: true files: ./coverage.txt