You've already forked prometheus-fail2ban-exporter
feat: add support for basic auth (#16)
Add new CLI parameters to enable protecting the API endpoints with basic auth authentication. Wrap the server endpoints in a new auth middleware that protects it using the provided basic auth credentials (if set). Store the provided basic auth credentials as hashed values to prevent them from being accidentally leaked. Add unit tests to ensure the new functionality works as expected.
This commit is contained in:
25
src/cfg/basicAuth.go
Normal file
25
src/cfg/basicAuth.go
Normal file
@ -0,0 +1,25 @@
|
||||
package cfg
|
||||
|
||||
import "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
|
||||
}
|
60
src/cfg/basicAuth_test.go
Normal file
60
src/cfg/basicAuth_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -18,9 +18,13 @@ type AppSettings struct {
|
||||
Fail2BanSocketPath string
|
||||
FileCollectorPath string
|
||||
FileCollectorEnabled bool
|
||||
BasicAuthProvider *hashedBasicAuth
|
||||
}
|
||||
|
||||
func Parse() *AppSettings {
|
||||
var rawBasicAuthUsername string
|
||||
var rawBasicAuthPassword string
|
||||
|
||||
appSettings := &AppSettings{}
|
||||
flag.BoolVar(&appSettings.VersionMode, "version", false, "show version info and exit")
|
||||
flag.StringVar(&appSettings.MetricsAddress, "web.listen-address", "0.0.0.0", "address to use for the metrics server")
|
||||
@ -28,12 +32,19 @@ func Parse() *AppSettings {
|
||||
flag.StringVar(&appSettings.Fail2BanSocketPath, "socket", "", "path to the fail2ban server socket")
|
||||
flag.BoolVar(&appSettings.FileCollectorEnabled, "collector.textfile", false, "enable the textfile collector")
|
||||
flag.StringVar(&appSettings.FileCollectorPath, "collector.textfile.directory", "", "directory to read text files with metrics from")
|
||||
flag.StringVar(&rawBasicAuthUsername, "web.basic-auth.username", "", "username to use to protect endpoints with basic auth")
|
||||
flag.StringVar(&rawBasicAuthPassword, "web.basic-auth.password", "", "password to use to protect endpoints with basic auth")
|
||||
|
||||
flag.Parse()
|
||||
appSettings.setBasicAuthValues(rawBasicAuthUsername, rawBasicAuthPassword)
|
||||
appSettings.validateFlags()
|
||||
return appSettings
|
||||
}
|
||||
|
||||
func (settings *AppSettings) setBasicAuthValues(rawUsername, rawPassword string) {
|
||||
settings.BasicAuthProvider = newHashedBasicAuth(rawUsername, rawPassword)
|
||||
}
|
||||
|
||||
func (settings *AppSettings) validateFlags() {
|
||||
var flagsValid = true
|
||||
if !settings.VersionMode {
|
||||
@ -50,6 +61,10 @@ func (settings *AppSettings) validateFlags() {
|
||||
fmt.Printf("file collector directory path must not be empty if collector enabled\n")
|
||||
flagsValid = false
|
||||
}
|
||||
if (len(settings.BasicAuthProvider.username) > 0) != (len(settings.BasicAuthProvider.password) > 0) {
|
||||
fmt.Printf("to enable basic auth both the username and the password must be provided")
|
||||
flagsValid = false
|
||||
}
|
||||
}
|
||||
if !flagsValid {
|
||||
flag.Usage()
|
||||
|
Reference in New Issue
Block a user