diff --git a/config/config.go b/config/config.go index 09e0bfcb..6d0a0540 100644 --- a/config/config.go +++ b/config/config.go @@ -5,6 +5,7 @@ import ( "github.com/TwinProduction/gatus/alerting" "github.com/TwinProduction/gatus/alerting/provider" "github.com/TwinProduction/gatus/core" + "github.com/TwinProduction/gatus/security" "gopkg.in/yaml.v2" "io/ioutil" "log" @@ -18,16 +19,18 @@ const ( ) var ( - ErrNoServiceInConfig = errors.New("configuration file should contain at least 1 service") - ErrConfigFileNotFound = errors.New("configuration file not found") - ErrConfigNotLoaded = errors.New("configuration is nil") - config *Config + ErrNoServiceInConfig = errors.New("configuration file should contain at least 1 service") + ErrConfigFileNotFound = errors.New("configuration file not found") + ErrConfigNotLoaded = errors.New("configuration is nil") + ErrInvalidSecurityConfig = errors.New("invalid security configuration") + config *Config ) // Config is the main configuration structure type Config struct { Metrics bool `yaml:"metrics"` Debug bool `yaml:"debug"` + Security *security.Config `yaml:"security"` Alerting *alerting.Config `yaml:"alerting"` Services []*core.Service `yaml:"services"` } @@ -83,6 +86,7 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) { err = ErrNoServiceInConfig } else { validateAlertingConfig(config) + validateSecurityConfig(config) validateServicesConfig(config) } return @@ -98,6 +102,20 @@ func validateServicesConfig(config *Config) { log.Printf("[config][validateServicesConfig] Validated %d services", len(config.Services)) } +func validateSecurityConfig(config *Config) { + if config.Security != nil { + if config.Security.IsValid() { + if config.Debug { + log.Printf("[config][validateSecurityConfig] Basic security configuration has been validated") + } + } else { + // If there was an attempt to configure security, then it must mean that some confidential or private + // data are exposed. As a result, we'll force a panic because it's better to be safe than sorry. + panic(ErrInvalidSecurityConfig) + } + } +} + func validateAlertingConfig(config *Config) { if config.Alerting == nil { log.Printf("[config][validateAlertingConfig] Alerting is not configured") diff --git a/config/config_test.go b/config/config_test.go index 20f9e95c..d4d76982 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "github.com/TwinProduction/gatus/core" "testing" "time" @@ -217,3 +218,56 @@ services: t.Fatal("PagerDuty alerting config should've been invalid") } } + +func TestParseAndValidateConfigBytesWithInvalidSecurityConfig(t *testing.T) { + defer func() { recover() }() + _, _ = parseAndValidateConfigBytes([]byte(` +security: + basic: + username: "admin" + password-sha512: "invalid-sha512-hash" +services: + - name: twinnation + url: https://twinnation.org/actuator/health + conditions: + - "[STATUS] == 200" +`)) + t.Error("Function should've panicked") +} + +func TestParseAndValidateConfigBytesWithValidSecurityConfig(t *testing.T) { + const expectedUsername = "admin" + const expectedPasswordHash = "6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22" + config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(` +security: + basic: + username: "%s" + password-sha512: "%s" +services: + - name: twinnation + url: https://twinnation.org/actuator/health + conditions: + - "[STATUS] == 200" +`, expectedUsername, expectedPasswordHash))) + if err != nil { + t.Error("No error should've been returned") + } + if config == nil { + t.Fatal("Config shouldn't have been nil") + } + if config.Security == nil { + t.Fatal("config.Security shouldn't have been nil") + } + if !config.Security.IsValid() { + t.Error("Security config should've been valid") + } + if config.Security.Basic == nil { + t.Fatal("config.Security.Basic shouldn't have been nil") + } + if config.Security.Basic.Username != expectedUsername { + t.Errorf("config.Security.Basic.Username should've been %s, but was %s", expectedUsername, config.Security.Basic.Username) + } + if config.Security.Basic.PasswordSha512Hash != expectedPasswordHash { + t.Errorf("config.Security.Basic.PasswordSha512Hash should've been %s, but was %s", expectedPasswordHash, config.Security.Basic.PasswordSha512Hash) + } +} diff --git a/security/security.go b/security/security.go new file mode 100644 index 00000000..29b5340c --- /dev/null +++ b/security/security.go @@ -0,0 +1,18 @@ +package security + +type Config struct { + Basic *BasicConfig `yaml:"basic"` +} + +func (c *Config) IsValid() bool { + return c.Basic != nil && c.Basic.IsValid() +} + +type BasicConfig struct { + Username string `yaml:"username"` + PasswordSha512Hash string `yaml:"password-sha512"` +} + +func (c *BasicConfig) IsValid() bool { + return len(c.Username) > 0 && len(c.PasswordSha512Hash) == 128 +} diff --git a/security/security_test.go b/security/security_test.go new file mode 100644 index 00000000..67d4e796 --- /dev/null +++ b/security/security_test.go @@ -0,0 +1,23 @@ +package security + +import "testing" + +func TestBasicConfig_IsValid(t *testing.T) { + basicConfig := &BasicConfig{ + Username: "admin", + PasswordSha512Hash: Sha512("test"), + } + if !basicConfig.IsValid() { + t.Error("basicConfig should've been valid") + } +} + +func TestBasicConfig_IsValidWhenPasswordIsInvalid(t *testing.T) { + basicConfig := &BasicConfig{ + Username: "admin", + PasswordSha512Hash: "", + } + if basicConfig.IsValid() { + t.Error("basicConfig shouldn't have been valid") + } +}