From d31ea4b23c8786250cc27fbd103709974e9f509a Mon Sep 17 00:00:00 2001 From: Hector <hector@hjs.dev> Date: Tue, 20 Jun 2023 21:41:15 +0100 Subject: [PATCH 1/6] move middleware to new package --- exporter.go | 17 +++++++++-------- {auth => server}/middleware.go | 2 +- {auth => server}/middleware_test.go | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) rename {auth => server}/middleware.go (98%) rename {auth => server}/middleware_test.go (99%) diff --git a/exporter.go b/exporter.go index 449f5ac..65857ba 100644 --- a/exporter.go +++ b/exporter.go @@ -2,17 +2,18 @@ package main import ( "fmt" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/auth" - "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/cfg" - "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/collector/f2b" - "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/collector/textfile" "log" "net/http" "os" "os/signal" "syscall" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/cfg" + "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/collector/f2b" + "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/collector/textfile" + "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/server" ) const ( @@ -66,8 +67,8 @@ func main() { textFileCollector := textfile.NewCollector(appSettings) prometheus.MustRegister(textFileCollector) - http.HandleFunc("/", auth.BasicAuthMiddleware(rootHtmlHandler, appSettings.BasicAuthProvider)) - http.HandleFunc(metricsPath, auth.BasicAuthMiddleware( + http.HandleFunc("/", server.BasicAuthMiddleware(rootHtmlHandler, appSettings.BasicAuthProvider)) + http.HandleFunc(metricsPath, server.BasicAuthMiddleware( func(w http.ResponseWriter, r *http.Request) { metricHandler(w, r, textFileCollector) }, diff --git a/auth/middleware.go b/server/middleware.go similarity index 98% rename from auth/middleware.go rename to server/middleware.go index dd16fe3..92dec38 100644 --- a/auth/middleware.go +++ b/server/middleware.go @@ -1,4 +1,4 @@ -package auth +package server import ( "net/http" diff --git a/auth/middleware_test.go b/server/middleware_test.go similarity index 99% rename from auth/middleware_test.go rename to server/middleware_test.go index 27c8ab6..7c7e428 100644 --- a/auth/middleware_test.go +++ b/server/middleware_test.go @@ -1,4 +1,4 @@ -package auth +package server import ( "net/http" From 17fb995928a61ab6e2d44b917d9af73905decfe2 Mon Sep 17 00:00:00 2001 From: Hector <hector@hjs.dev> Date: Tue, 20 Jun 2023 22:15:12 +0100 Subject: [PATCH 2/6] rewrite auth provider logic --- auth/basic.go | 29 +++++++++++++++++++++++++++++ auth/hash.go | 4 ++-- auth/provider.go | 34 ++++++++++++++++++++++++++++++++++ cfg/cfg.go | 15 ++++++++++++++- cfg/settings.go | 4 +++- exporter.go | 7 ++----- server/middleware.go | 30 ++++++++---------------------- server/middleware_test.go | 28 ++++++++-------------------- 8 files changed, 100 insertions(+), 51 deletions(-) create mode 100644 auth/basic.go create mode 100644 auth/provider.go diff --git a/auth/basic.go b/auth/basic.go new file mode 100644 index 0000000..243b2a5 --- /dev/null +++ b/auth/basic.go @@ -0,0 +1,29 @@ +package auth + +import ( + "fmt" + "net/http" +) + +func NewBasicAuthProvider(username, password string) AuthProvider { + return &basicAuthProvider{ + hashedAuth: encodeBasicAuth(username, password), + } +} + +type basicAuthProvider struct { + hashedAuth string +} + +func (p *basicAuthProvider) IsAllowed(request *http.Request) bool { + username, password, ok := request.BasicAuth() + if !ok { + return false + } + requestAuth := encodeBasicAuth(username, password) + return p.hashedAuth == requestAuth +} + +func encodeBasicAuth(username, password string) string { + return HashString(fmt.Sprintf("%s:%s", username, password)) +} diff --git a/auth/hash.go b/auth/hash.go index e1b4b3b..cc41e36 100644 --- a/auth/hash.go +++ b/auth/hash.go @@ -5,7 +5,7 @@ import ( "encoding/hex" ) -func Hash(data []byte) []byte { +func hash(data []byte) []byte { if len(data) == 0 { return []byte{} } @@ -14,5 +14,5 @@ func Hash(data []byte) []byte { } func HashString(data string) string { - return hex.EncodeToString(Hash([]byte(data))) + return hex.EncodeToString(hash([]byte(data))) } diff --git a/auth/provider.go b/auth/provider.go new file mode 100644 index 0000000..4d8167e --- /dev/null +++ b/auth/provider.go @@ -0,0 +1,34 @@ +package auth + +import ( + "net/http" +) + +type AuthProvider interface { + IsAllowed(*http.Request) bool +} + +func NewEmptyAuthProvider() AuthProvider { + return &emptyAuthProvider{} +} + +type emptyAuthProvider struct { +} + +func (p *emptyAuthProvider) IsAllowed(request *http.Request) bool { + return true +} + +type compositeAuthProvider struct { + providers []AuthProvider +} + +func (p *compositeAuthProvider) IsAllowed(request *http.Request) bool { + for i := 0; i < len(p.providers); i++ { + provider := p.providers[i] + if provider.IsAllowed(request) { + return true + } + } + return false +} diff --git a/cfg/cfg.go b/cfg/cfg.go index cee729e..9628632 100644 --- a/cfg/cfg.go +++ b/cfg/cfg.go @@ -2,9 +2,11 @@ package cfg import ( "fmt" + "log" "os" "github.com/alecthomas/kong" + "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/auth" ) var cliStruct struct { @@ -36,11 +38,22 @@ func Parse() *AppSettings { Fail2BanSocketPath: cliStruct.F2bSocketPath, FileCollectorPath: cliStruct.TextFileExporterPath, ExitOnSocketConnError: cliStruct.ExitOnSocketError, - BasicAuthProvider: newHashedBasicAuth(cliStruct.BasicAuthUser, cliStruct.BasicAuthPass), + AuthProvider: createAuthProvider(), } return settings } +func createAuthProvider() auth.AuthProvider { + username := cliStruct.BasicAuthUser + password := cliStruct.BasicAuthPass + + if len(username) == 0 && len(password) == 0 { + return auth.NewEmptyAuthProvider() + } + log.Print("basic auth enabled") + return auth.NewBasicAuthProvider(username, password) +} + func validateFlags(cliCtx *kong.Context) { var flagsValid = true var messages = []string{} diff --git a/cfg/settings.go b/cfg/settings.go index 160234e..519f8e0 100644 --- a/cfg/settings.go +++ b/cfg/settings.go @@ -1,10 +1,12 @@ package cfg +import "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/auth" + type AppSettings struct { VersionMode bool MetricsAddress string Fail2BanSocketPath string FileCollectorPath string - BasicAuthProvider *hashedBasicAuth + AuthProvider auth.AuthProvider ExitOnSocketConnError bool } diff --git a/exporter.go b/exporter.go index 65857ba..6bc0126 100644 --- a/exporter.go +++ b/exporter.go @@ -67,17 +67,14 @@ func main() { textFileCollector := textfile.NewCollector(appSettings) prometheus.MustRegister(textFileCollector) - http.HandleFunc("/", server.BasicAuthMiddleware(rootHtmlHandler, appSettings.BasicAuthProvider)) + http.HandleFunc("/", server.BasicAuthMiddleware(rootHtmlHandler, appSettings.AuthProvider)) http.HandleFunc(metricsPath, server.BasicAuthMiddleware( func(w http.ResponseWriter, r *http.Request) { metricHandler(w, r, textFileCollector) }, - appSettings.BasicAuthProvider, + appSettings.AuthProvider, )) log.Printf("metrics available at '%s'", metricsPath) - if appSettings.BasicAuthProvider.Enabled() { - log.Printf("basic auth enabled") - } svrErr := make(chan error) go func() { diff --git a/server/middleware.go b/server/middleware.go index 92dec38..7593bbc 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -2,30 +2,16 @@ package server import ( "net/http" + + "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/auth" ) -type BasicAuthProvider interface { - Enabled() bool - DoesBasicAuthMatch(username, password string) bool -} - -func BasicAuthMiddleware(handlerFunc http.HandlerFunc, basicAuthProvider BasicAuthProvider) http.HandlerFunc { - if basicAuthProvider.Enabled() { - return func(w http.ResponseWriter, r *http.Request) { - if doesBasicAuthMatch(r, basicAuthProvider) { - handlerFunc.ServeHTTP(w, r) - } else { - w.WriteHeader(http.StatusUnauthorized) - } +func BasicAuthMiddleware(handlerFunc http.HandlerFunc, authProvider auth.AuthProvider) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if authProvider.IsAllowed(r) { + handlerFunc.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusUnauthorized) } } - return handlerFunc -} - -func doesBasicAuthMatch(r *http.Request, basicAuthProvider BasicAuthProvider) bool { - rawUsername, rawPassword, ok := r.BasicAuth() - if ok { - return basicAuthProvider.DoesBasicAuthMatch(rawUsername, rawPassword) - } - return false } diff --git a/server/middleware_test.go b/server/middleware_test.go index 7c7e428..3ac564b 100644 --- a/server/middleware_test.go +++ b/server/middleware_test.go @@ -7,15 +7,10 @@ import ( ) type testAuthProvider struct { - enabled bool - match bool + match bool } -func (p testAuthProvider) Enabled() bool { - return p.enabled -} - -func (p testAuthProvider) DoesBasicAuthMatch(username, password string) bool { +func (p testAuthProvider) IsAllowed(request *http.Request) bool { return p.match } @@ -23,18 +18,15 @@ func newTestRequest() *http.Request { return httptest.NewRequest(http.MethodGet, "http://example.com", nil) } -func executeBasicAuthMiddlewareTest(t *testing.T, authEnabled bool, authMatches bool, expectedCode int, expectedCallCount int) { +func executeBasicAuthMiddlewareTest(t *testing.T, authMatches bool, expectedCode int, expectedCallCount int) { callCount := 0 testHandler := func(w http.ResponseWriter, r *http.Request) { callCount++ } - handler := BasicAuthMiddleware(testHandler, testAuthProvider{enabled: authEnabled, match: authMatches}) + handler := BasicAuthMiddleware(testHandler, testAuthProvider{match: authMatches}) recorder := httptest.NewRecorder() request := newTestRequest() - if authEnabled { - request.SetBasicAuth("test", "test") - } handler.ServeHTTP(recorder, request) if recorder.Code != expectedCode { @@ -45,14 +37,10 @@ func executeBasicAuthMiddlewareTest(t *testing.T, authEnabled bool, authMatches } } -func Test_GIVEN_DisabledBasicAuth_WHEN_MethodCalled_THEN_RequestProcessed(t *testing.T) { - executeBasicAuthMiddlewareTest(t, false, false, http.StatusOK, 1) +func Test_GIVEN_MatchingBasicAuth_WHEN_MethodCalled_THEN_RequestProcessed(t *testing.T) { + executeBasicAuthMiddlewareTest(t, true, http.StatusOK, 1) } -func Test_GIVEN_EnabledBasicAuth_WHEN_MethodCalledWithCorrectCredentials_THEN_RequestProcessed(t *testing.T) { - executeBasicAuthMiddlewareTest(t, true, true, http.StatusOK, 1) -} - -func Test_GIVEN_EnabledBasicAuth_WHEN_MethodCalledWithIncorrectCredentials_THEN_RequestRejected(t *testing.T) { - executeBasicAuthMiddlewareTest(t, true, false, http.StatusUnauthorized, 0) +func Test_GIVEN_NonMatchingBasicAuth_WHEN_MethodCalled_THEN_RequestRejected(t *testing.T) { + executeBasicAuthMiddlewareTest(t, false, http.StatusUnauthorized, 0) } From ebae219a473becdae77e6f1311699d56ea4560e7 Mon Sep 17 00:00:00 2001 From: Hector <hector@hjs.dev> Date: Tue, 20 Jun 2023 22:27:56 +0100 Subject: [PATCH 3/6] add test for basic auth --- auth/basic_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 auth/basic_test.go diff --git a/auth/basic_test.go b/auth/basic_test.go new file mode 100644 index 0000000..ba23f5f --- /dev/null +++ b/auth/basic_test.go @@ -0,0 +1,53 @@ +package auth + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithCorrectCreds_THEN_TrueReturned(t *testing.T) { + // assemble + username := "u1" + password := "p1" + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + request.SetBasicAuth(username, password) + provider := NewBasicAuthProvider(username, password) + + // act + result := provider.IsAllowed(request) + + // assert + if !result { + t.Errorf("expected request to be allowed, but failed") + } +} + +func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithoutCreds_THEN_FalseReturned(t *testing.T) { + // assemble + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + provider := NewBasicAuthProvider("u1", "p1") + + // act + result := provider.IsAllowed(request) + + // assert + if result { + t.Errorf("expected request to be denied, but was allowed") + } +} + +func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithWrongCreds_THEN_FalseReturned(t *testing.T) { + // assemble + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + request.SetBasicAuth("wrong", "pw") + provider := NewBasicAuthProvider("u1", "p1") + + // act + result := provider.IsAllowed(request) + + // assert + if result { + t.Errorf("expected request to be denied, but was allowed") + } +} From a654eb942aa9b65000f566878d370751f30585a0 Mon Sep 17 00:00:00 2001 From: Hector <hector@hjs.dev> Date: Tue, 20 Jun 2023 22:31:16 +0100 Subject: [PATCH 4/6] add tests for empty auth --- auth/empty.go | 14 ++++++++++++++ auth/empty_test.go | 36 ++++++++++++++++++++++++++++++++++++ auth/provider.go | 25 ------------------------- 3 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 auth/empty.go create mode 100644 auth/empty_test.go diff --git a/auth/empty.go b/auth/empty.go new file mode 100644 index 0000000..01e6775 --- /dev/null +++ b/auth/empty.go @@ -0,0 +1,14 @@ +package auth + +import "net/http" + +func NewEmptyAuthProvider() AuthProvider { + return &emptyAuthProvider{} +} + +type emptyAuthProvider struct { +} + +func (p *emptyAuthProvider) IsAllowed(request *http.Request) bool { + return true +} diff --git a/auth/empty_test.go b/auth/empty_test.go new file mode 100644 index 0000000..048de9d --- /dev/null +++ b/auth/empty_test.go @@ -0,0 +1,36 @@ +package auth + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func Test_GIVEN_EmptyAuth_WHEN_CallingIsAllowedWithoutAuth_THEN_TrueReturned(t *testing.T) { + // assemble + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + provider := NewEmptyAuthProvider() + + // act + response := provider.IsAllowed(request) + + // assert + if !response { + t.Errorf("expected request to be allowed, but failed") + } +} + +func Test_GIVEN_EmptyAuth_WHEN_CallingIsAllowedWithAuth_THEN_TrueReturned(t *testing.T) { + // assemble + request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + request.SetBasicAuth("user", "pass") + provider := NewEmptyAuthProvider() + + // act + response := provider.IsAllowed(request) + + // assert + if !response { + t.Errorf("expected request to be allowed, but failed") + } +} diff --git a/auth/provider.go b/auth/provider.go index 4d8167e..a259ea0 100644 --- a/auth/provider.go +++ b/auth/provider.go @@ -7,28 +7,3 @@ import ( type AuthProvider interface { IsAllowed(*http.Request) bool } - -func NewEmptyAuthProvider() AuthProvider { - return &emptyAuthProvider{} -} - -type emptyAuthProvider struct { -} - -func (p *emptyAuthProvider) IsAllowed(request *http.Request) bool { - return true -} - -type compositeAuthProvider struct { - providers []AuthProvider -} - -func (p *compositeAuthProvider) IsAllowed(request *http.Request) bool { - for i := 0; i < len(p.providers); i++ { - provider := p.providers[i] - if provider.IsAllowed(request) { - return true - } - } - return false -} From 82e67d82546631e6c51a088484fa92cc59f9fcce Mon Sep 17 00:00:00 2001 From: Hector <hector@hjs.dev> Date: Tue, 20 Jun 2023 22:32:33 +0100 Subject: [PATCH 5/6] remove old auth code --- cfg/basicAuth.go | 25 ------------------ cfg/basicAuth_test.go | 60 ------------------------------------------- 2 files changed, 85 deletions(-) delete mode 100644 cfg/basicAuth.go delete mode 100644 cfg/basicAuth_test.go diff --git a/cfg/basicAuth.go b/cfg/basicAuth.go deleted file mode 100644 index 991861e..0000000 --- a/cfg/basicAuth.go +++ /dev/null @@ -1,25 +0,0 @@ -package cfg - -import "gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/auth" - -type hashedBasicAuth struct { - username string - password string -} - -func newHashedBasicAuth(rawUsername, rawPassword string) *hashedBasicAuth { - return &hashedBasicAuth{ - username: auth.HashString(rawUsername), - password: auth.HashString(rawPassword), - } -} - -func (p *hashedBasicAuth) Enabled() bool { - return len(p.username) > 0 && len(p.password) > 0 -} - -func (p *hashedBasicAuth) DoesBasicAuthMatch(rawUsername, rawPassword string) bool { - username := auth.HashString(rawUsername) - password := auth.HashString(rawPassword) - return username == p.username && password == p.password -} diff --git a/cfg/basicAuth_test.go b/cfg/basicAuth_test.go deleted file mode 100644 index 85bd8b3..0000000 --- a/cfg/basicAuth_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package cfg - -import "testing" - -func Test_hashedBasicAuth_DoesBasicAuthMatch(t *testing.T) { - type args struct { - username string - password string - } - type fields struct { - username string - password string - } - tests := []struct { - name string - fields fields - args args - want bool - }{ - {"Happy test #1", fields{username: "1234", password: "1234"}, args{username: "1234", password: "1234"}, true}, - {"Happy test #2", fields{username: "test", password: "1234"}, args{username: "test", password: "1234"}, true}, - {"Happy test #3", fields{username: "TEST", password: "1234"}, args{username: "TEST", password: "1234"}, true}, - {"Non match #1", fields{username: "test", password: "1234"}, args{username: "1234", password: "1234"}, false}, - {"Non match #2", fields{username: "1234", password: "test"}, args{username: "1234", password: "1234"}, false}, - {"Non match #3", fields{username: "1234", password: "test"}, args{username: "1234", password: "TEST"}, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - basicAuth := newHashedBasicAuth(tt.fields.username, tt.fields.password) - if got := basicAuth.DoesBasicAuthMatch(tt.args.username, tt.args.password); got != tt.want { - t.Errorf("DoesBasicAuthMatch() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_hashedBasicAuth_Enabled(t *testing.T) { - type fields struct { - username string - password string - } - tests := []struct { - name string - fields fields - want bool - }{ - {"Both blank", fields{username: "", password: ""}, false}, - {"Single blank #1", fields{username: "test", password: ""}, false}, - {"Single blank #1", fields{username: "", password: "test"}, false}, - {"Both populated", fields{username: "test", password: "test"}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - basicAuth := newHashedBasicAuth(tt.fields.username, tt.fields.password) - if got := basicAuth.Enabled(); got != tt.want { - t.Errorf("Enabled() = %v, want %v", got, tt.want) - } - }) - } -} From 605cafa635431fcb17a25f6293ecb1c3c99ebce5 Mon Sep 17 00:00:00 2001 From: Hector <hector@hjs.dev> Date: Wed, 21 Jun 2023 11:27:01 +0100 Subject: [PATCH 6/6] hash password in test --- auth/basic_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/basic_test.go b/auth/basic_test.go index ba23f5f..e4ca1fd 100644 --- a/auth/basic_test.go +++ b/auth/basic_test.go @@ -9,7 +9,7 @@ import ( func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithCorrectCreds_THEN_TrueReturned(t *testing.T) { // assemble username := "u1" - password := "p1" + password := HashString("abc") request := httptest.NewRequest(http.MethodGet, "http://example.com", nil) request.SetBasicAuth(username, password) provider := NewBasicAuthProvider(username, password)