diff --git a/conf/defaults.ini b/conf/defaults.ini index be7e06d7223..65cec28cfbe 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -374,6 +374,9 @@ disable_brute_force_login_protection = false # max number of failed login attempts before user gets locked brute_force_login_protection_max_attempts = 5 +# disable protection against brute force login attempts by username +disable_username_login_protection = false + # disable protection against brute force login attempts by IP address disable_ip_address_login_protection = true diff --git a/conf/sample.ini b/conf/sample.ini index 6d3afd69292..fb6550c23f1 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -371,6 +371,9 @@ # max number of failed login attempts before user gets locked ;brute_force_login_protection_max_attempts = 5 +# disable protection against brute force login attempts by username +;disable_username_login_protection = false + # disable protection against brute force login attempts by IP address ; disable_ip_address_login_protection = true diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index a79ccc33e70..67cba858ef2 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -703,13 +703,17 @@ PostgreSQL, MySQL, and MSSQL data sources don't use the proxy and are not affect Set to `true` to disable [brute force login protection](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout). Default is `false`. -An existing user's account is unable to login for five minutes if all login attempts are spent within a 5 minute window. +Login is blocked for five minutes if all login attempts are spent within a 5 minute window. #### `brute_force_login_protection_max_attempts` -Configure how many login attempts a user can have within a five minute window before their account is locked. +Configure how many login attempts can be made within a five minute window before being blocked. Default is `5`. +#### `disable_username_login_protection` + +Set to `true` to disable [brute force login protection by username](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout). Default is `false`. User will be unable to login for 5 minutes if all login attempts are spent within a 5 minute window. + #### `disable_ip_address_login_protection` Set to `true` to disable [brute force login protection by IP address](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout). Default is `true`. Anyone from the IP address will be unable to login for 5 minutes if all login attempts are spent within a 5 minute window. diff --git a/pkg/services/loginattempt/loginattemptimpl/login_attempt.go b/pkg/services/loginattempt/loginattemptimpl/login_attempt.go index 3c901415333..224c8ae5d7f 100644 --- a/pkg/services/loginattempt/loginattemptimpl/login_attempt.go +++ b/pkg/services/loginattempt/loginattemptimpl/login_attempt.go @@ -47,7 +47,7 @@ func (s *Service) Run(ctx context.Context) error { } func (s *Service) Add(ctx context.Context, username, IPAddress string) error { - if s.cfg.DisableBruteForceLoginProtection { + if s.cfg.DisableBruteForceLoginProtection || (s.cfg.DisableUsernameLoginProtection && s.cfg.DisableIPAddressLoginProtection) { return nil } @@ -63,7 +63,7 @@ func (s *Service) Reset(ctx context.Context, username string) error { } func (s *Service) Validate(ctx context.Context, username string) (bool, error) { - if s.cfg.DisableBruteForceLoginProtection { + if s.cfg.DisableBruteForceLoginProtection || s.cfg.DisableUsernameLoginProtection { return true, nil } @@ -85,7 +85,7 @@ func (s *Service) Validate(ctx context.Context, username string) (bool, error) { } func (s *Service) ValidateIPAddress(ctx context.Context, IPAddress string) (bool, error) { - if s.cfg.DisableIPAddressLoginProtection { + if s.cfg.DisableBruteForceLoginProtection || s.cfg.DisableIPAddressLoginProtection { return true, nil } diff --git a/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go b/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go index 6afd594a9c9..a68024c1595 100644 --- a/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go +++ b/pkg/services/loginattempt/loginattemptimpl/login_attempt_test.go @@ -18,11 +18,12 @@ func TestService_Validate(t *testing.T) { const maxInvalidLoginAttempts = 5 testCases := []struct { - name string - loginAttempts int64 - disabled bool - expected bool - expectedErr error + name string + loginAttempts int64 + disableProtection bool + disableUsernameProtection bool + expected bool + expectedErr error }{ { name: "Should be valid when brute force protection enabled and user login attempt count is less than max", @@ -44,25 +45,70 @@ func TestService_Validate(t *testing.T) { }, { - name: "Should be valid when brute force protection disabled and user login attempt count is less than max", - loginAttempts: maxInvalidLoginAttempts - 1, - disabled: true, - expected: true, - expectedErr: nil, + name: "Should be valid when brute force protection disabled and user login attempt count is less than max", + loginAttempts: maxInvalidLoginAttempts - 1, + disableProtection: true, + expected: true, + expectedErr: nil, }, { - name: "Should be valid when brute force protection disabled and user login attempt count equals max", - loginAttempts: maxInvalidLoginAttempts, - disabled: true, - expected: true, - expectedErr: nil, + name: "Should be valid when brute force protection disabled and user login attempt count equals max", + loginAttempts: maxInvalidLoginAttempts, + disableProtection: true, + expected: true, + expectedErr: nil, }, { - name: "Should be valid when brute force protection disabled and user login attempt count is greater than max", - loginAttempts: maxInvalidLoginAttempts + 1, - disabled: true, - expected: true, - expectedErr: nil, + name: "Should be valid when brute force protection disabled and user login attempt count is greater than max", + loginAttempts: maxInvalidLoginAttempts + 1, + disableProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when brute force username protection disabled and user login attempt count is less than max", + loginAttempts: maxInvalidLoginAttempts - 1, + disableUsernameProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when brute force username protection disabled and user login attempt count equals max", + loginAttempts: maxInvalidLoginAttempts, + disableUsernameProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when brute force username protection disabled and user login attempt count is greater than max", + loginAttempts: maxInvalidLoginAttempts + 1, + disableUsernameProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when both brute force protections disabled and user login attempt count is less than max", + loginAttempts: maxInvalidLoginAttempts - 1, + disableProtection: true, + disableUsernameProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when both brute force protections disabled and user login attempt count equals max", + loginAttempts: maxInvalidLoginAttempts, + disableProtection: true, + disableUsernameProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when both brute force protections disabled and user login attempt count is greater than max", + loginAttempts: maxInvalidLoginAttempts + 1, + disableProtection: true, + disableUsernameProtection: true, + expected: true, + expectedErr: nil, }, } @@ -70,7 +116,8 @@ func TestService_Validate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { cfg := setting.NewCfg() cfg.BruteForceLoginProtectionMaxAttempts = maxInvalidLoginAttempts - cfg.DisableBruteForceLoginProtection = tt.disabled + cfg.DisableBruteForceLoginProtection = tt.disableProtection + cfg.DisableUsernameLoginProtection = tt.disableUsernameProtection service := &Service{ store: fakeStore{ ExpectedCount: tt.loginAttempts, @@ -93,6 +140,7 @@ func TestIntegrationUserLoginAttempts(t *testing.T) { ctx := context.Background() cfg := setting.NewCfg() cfg.DisableBruteForceLoginProtection = false + cfg.DisableUsernameLoginProtection = false cfg.BruteForceLoginProtectionMaxAttempts = 5 db := db.InitTestDB(t) service := ProvideService(db, cfg, nil) @@ -119,11 +167,12 @@ func TestService_ValidateIPAddress(t *testing.T) { const maxInvalidLoginAttempts = 5 testCases := []struct { - name string - loginAttempts int64 - disabled bool - expected bool - expectedErr error + name string + loginAttempts int64 + disableProtection bool + disableIPProtection bool + expected bool + expectedErr error }{ { name: "Should be valid when brute force protection enabled and IP address login attempt count is less than max", @@ -143,27 +192,71 @@ func TestService_ValidateIPAddress(t *testing.T) { expected: false, expectedErr: nil, }, - { - name: "Should be valid when brute force protection disabled and IP address login attempt count is less than max", - loginAttempts: maxInvalidLoginAttempts - 1, - disabled: true, - expected: true, - expectedErr: nil, + name: "Should be valid when brute force protection disabled and IP address login attempt count is less than max", + loginAttempts: maxInvalidLoginAttempts - 1, + disableProtection: true, + expected: true, + expectedErr: nil, }, { - name: "Should be valid when brute force protection disabled and IP address login attempt count equals max", - loginAttempts: maxInvalidLoginAttempts, - disabled: true, - expected: true, - expectedErr: nil, + name: "Should be valid when brute force protection disabled and IP address login attempt count equals max", + loginAttempts: maxInvalidLoginAttempts, + disableProtection: true, + expected: true, + expectedErr: nil, }, { - name: "Should be valid when brute force protection disabled and IP address login attempt count is greater than max", - loginAttempts: maxInvalidLoginAttempts + 1, - disabled: true, - expected: true, - expectedErr: nil, + name: "Should be valid when brute force protection disabled and IP address login attempt count is greater than max", + loginAttempts: maxInvalidLoginAttempts + 1, + disableProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when IP brute force protection disabled and IP address login attempt count is less than max", + loginAttempts: maxInvalidLoginAttempts - 1, + disableIPProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when IP brute force protection disabled and IP address login attempt count equals max", + loginAttempts: maxInvalidLoginAttempts, + disableIPProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when IP brute force protection disabled and IP address login attempt count is greater than max", + loginAttempts: maxInvalidLoginAttempts + 1, + disableIPProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when both brute force protections disabled and IP address login attempt count is less than max", + loginAttempts: maxInvalidLoginAttempts - 1, + disableProtection: true, + disableIPProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when both brute force protections disabled and IP address login attempt count equals max", + loginAttempts: maxInvalidLoginAttempts, + disableProtection: true, + disableIPProtection: true, + expected: true, + expectedErr: nil, + }, + { + name: "Should be valid when both brute force protections disabled and IP address login attempt count is greater than max", + loginAttempts: maxInvalidLoginAttempts + 1, + disableProtection: true, + disableIPProtection: true, + expected: true, + expectedErr: nil, }, } @@ -171,7 +264,8 @@ func TestService_ValidateIPAddress(t *testing.T) { t.Run(tt.name, func(t *testing.T) { cfg := setting.NewCfg() cfg.BruteForceLoginProtectionMaxAttempts = maxInvalidLoginAttempts - cfg.DisableIPAddressLoginProtection = tt.disabled + cfg.DisableBruteForceLoginProtection = tt.disableProtection + cfg.DisableIPAddressLoginProtection = tt.disableIPProtection service := &Service{ store: fakeStore{ ExpectedCount: tt.loginAttempts, @@ -193,6 +287,7 @@ func TestIntegrationIPLoginAttempts(t *testing.T) { } ctx := context.Background() cfg := setting.NewCfg() + cfg.DisableBruteForceLoginProtection = false cfg.DisableIPAddressLoginProtection = false cfg.BruteForceLoginProtectionMaxAttempts = 3 db := db.InitTestDB(t) @@ -224,6 +319,7 @@ func TestIntegrationIPv6AddressSupport(t *testing.T) { ctx := context.Background() cfg := setting.NewCfg() cfg.DisableBruteForceLoginProtection = false + cfg.DisableIPAddressLoginProtection = false cfg.BruteForceLoginProtectionMaxAttempts = 5 // Use controlled time like other tests to avoid timestamp conversion issues diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 32f7be6830b..5d347366a3d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -157,6 +157,7 @@ type Cfg struct { DisableInitAdminCreation bool DisableBruteForceLoginProtection bool BruteForceLoginProtectionMaxAttempts int64 + DisableUsernameLoginProtection bool DisableIPAddressLoginProtection bool CookieSecure bool CookieSameSiteDisabled bool @@ -1587,6 +1588,7 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error { cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false) cfg.BruteForceLoginProtectionMaxAttempts = security.Key("brute_force_login_protection_max_attempts").MustInt64(5) + cfg.DisableUsernameLoginProtection = security.Key("disable_username_login_protection").MustBool(false) cfg.DisableIPAddressLoginProtection = security.Key("disable_ip_address_login_protection").MustBool(true) // Ensure at least one login attempt can be performed.