You've already forked prometheus-fail2ban-exporter
refactor: rewrite auth handler code (!89)
* Rewrite the code handling basic auth to make it easier to extend for other types of auth. * The behaviour of the existing code is maintained. * No changes to how basic auth is configured from a user's perspective. https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/merge_requests/89
This commit is contained in:
29
auth/basic.go
Normal file
29
auth/basic.go
Normal file
@ -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))
|
||||
}
|
53
auth/basic_test.go
Normal file
53
auth/basic_test.go
Normal file
@ -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 := HashString("abc")
|
||||
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")
|
||||
}
|
||||
}
|
14
auth/empty.go
Normal file
14
auth/empty.go
Normal file
@ -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
|
||||
}
|
36
auth/empty_test.go
Normal file
36
auth/empty_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return handlerFunc
|
||||
}
|
||||
|
||||
func doesBasicAuthMatch(r *http.Request, basicAuthProvider BasicAuthProvider) bool {
|
||||
rawUsername, rawPassword, ok := r.BasicAuth()
|
||||
if ok {
|
||||
return basicAuthProvider.DoesBasicAuthMatch(rawUsername, rawPassword)
|
||||
}
|
||||
return false
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testAuthProvider struct {
|
||||
enabled bool
|
||||
match bool
|
||||
}
|
||||
|
||||
func (p testAuthProvider) Enabled() bool {
|
||||
return p.enabled
|
||||
}
|
||||
|
||||
func (p testAuthProvider) DoesBasicAuthMatch(username, password string) bool {
|
||||
return p.match
|
||||
}
|
||||
|
||||
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) {
|
||||
callCount := 0
|
||||
testHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
handler := BasicAuthMiddleware(testHandler, testAuthProvider{enabled: authEnabled, match: authMatches})
|
||||
recorder := httptest.NewRecorder()
|
||||
request := newTestRequest()
|
||||
if authEnabled {
|
||||
request.SetBasicAuth("test", "test")
|
||||
}
|
||||
handler.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != expectedCode {
|
||||
t.Errorf("statusCode = %v, want %v", recorder.Code, expectedCode)
|
||||
}
|
||||
if callCount != expectedCallCount {
|
||||
t.Errorf("callCount = %v, want %v", callCount, expectedCallCount)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GIVEN_DisabledBasicAuth_WHEN_MethodCalled_THEN_RequestProcessed(t *testing.T) {
|
||||
executeBasicAuthMiddlewareTest(t, false, false, 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)
|
||||
}
|
9
auth/provider.go
Normal file
9
auth/provider.go
Normal file
@ -0,0 +1,9 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AuthProvider interface {
|
||||
IsAllowed(*http.Request) bool
|
||||
}
|
Reference in New Issue
Block a user