mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LoginAttempt: Move logic around login attempts into the service (#58962)
* LoginAttemps: Remove from sqlstore mock * LoginAttemps: Move from models package to service package * LoginAttemps: Implement functionallity from brute force login in service * LoginAttemps: Call service * LoginAttempts: Update name and remove internal functions * LoginAttempts: Add tests * LoginAttempt: Add service fake * LoginAttempt: Register service as a background_services and remove job from cleanup service * LoginAttemps: Remove result from command struct * LoginAttempt: No longer pass pointers
This commit is contained in:
parent
082c8ba7a0
commit
189bf102cf
@ -82,6 +82,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
|
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
|
||||||
authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
|
authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
|
||||||
"github.com/grafana/grafana/pkg/services/login/loginservice"
|
"github.com/grafana/grafana/pkg/services/login/loginservice"
|
||||||
|
"github.com/grafana/grafana/pkg/services/loginattempt"
|
||||||
"github.com/grafana/grafana/pkg/services/loginattempt/loginattemptimpl"
|
"github.com/grafana/grafana/pkg/services/loginattempt/loginattemptimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||||
ngmetrics "github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
ngmetrics "github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||||
@ -227,6 +228,7 @@ var wireSet = wire.NewSet(
|
|||||||
loginpkg.ProvideService,
|
loginpkg.ProvideService,
|
||||||
wire.Bind(new(loginpkg.Authenticator), new(*loginpkg.AuthenticatorService)),
|
wire.Bind(new(loginpkg.Authenticator), new(*loginpkg.AuthenticatorService)),
|
||||||
loginattemptimpl.ProvideService,
|
loginattemptimpl.ProvideService,
|
||||||
|
wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)),
|
||||||
datasourceproxy.ProvideService,
|
datasourceproxy.ProvideService,
|
||||||
search.ProvideService,
|
search.ProvideService,
|
||||||
searchV2.ProvideService,
|
searchV2.ProvideService,
|
||||||
|
@ -50,16 +50,19 @@ func ProvideService(store db.DB, loginService login.Service, loginAttemptService
|
|||||||
|
|
||||||
// AuthenticateUser authenticates the user via username & password
|
// AuthenticateUser authenticates the user via username & password
|
||||||
func (a *AuthenticatorService) AuthenticateUser(ctx context.Context, query *models.LoginUserQuery) error {
|
func (a *AuthenticatorService) AuthenticateUser(ctx context.Context, query *models.LoginUserQuery) error {
|
||||||
if err := validateLoginAttempts(ctx, query, a.loginAttemptService); err != nil {
|
ok, err := a.loginAttemptService.Validate(ctx, query.Username)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !ok {
|
||||||
|
return ErrTooManyLoginAttempts
|
||||||
|
}
|
||||||
|
|
||||||
if err := validatePasswordSet(query.Password); err != nil {
|
if err := validatePasswordSet(query.Password); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
isGrafanaLoginEnabled := !query.Cfg.DisableLogin
|
isGrafanaLoginEnabled := !query.Cfg.DisableLogin
|
||||||
var err error
|
|
||||||
|
|
||||||
if isGrafanaLoginEnabled {
|
if isGrafanaLoginEnabled {
|
||||||
err = loginUsingGrafanaDB(ctx, query, a.userService)
|
err = loginUsingGrafanaDB(ctx, query, a.userService)
|
||||||
@ -84,7 +87,7 @@ func (a *AuthenticatorService) AuthenticateUser(ctx context.Context, query *mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, ErrInvalidCredentials) || errors.Is(err, ldap.ErrInvalidCredentials) {
|
if errors.Is(err, ErrInvalidCredentials) || errors.Is(err, ldap.ErrInvalidCredentials) {
|
||||||
if err := saveInvalidLoginAttempt(ctx, query, a.loginAttemptService); err != nil {
|
if err := a.loginAttemptService.Add(ctx, query.Username, query.IpAddress); err != nil {
|
||||||
loginLogger.Error("Failed to save invalid login attempt", "err", err)
|
loginLogger.Error("Failed to save invalid login attempt", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ldap"
|
"github.com/grafana/grafana/pkg/services/ldap"
|
||||||
"github.com/grafana/grafana/pkg/services/login"
|
"github.com/grafana/grafana/pkg/services/login"
|
||||||
"github.com/grafana/grafana/pkg/services/login/logintest"
|
"github.com/grafana/grafana/pkg/services/login/logintest"
|
||||||
"github.com/grafana/grafana/pkg/services/loginattempt"
|
"github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -18,16 +18,15 @@ import (
|
|||||||
|
|
||||||
func TestAuthenticateUser(t *testing.T) {
|
func TestAuthenticateUser(t *testing.T) {
|
||||||
authScenario(t, "When a user authenticates without setting a password", func(sc *authScenarioContext) {
|
authScenario(t, "When a user authenticates without setting a password", func(sc *authScenarioContext) {
|
||||||
mockLoginAttemptValidation(nil, sc)
|
|
||||||
mockLoginUsingGrafanaDB(nil, sc)
|
mockLoginUsingGrafanaDB(nil, sc)
|
||||||
mockLoginUsingLDAP(false, nil, sc)
|
mockLoginUsingLDAP(false, nil, sc)
|
||||||
|
|
||||||
loginQuery := models.LoginUserQuery{
|
loginAttemptService := &loginattempttest.FakeLoginAttemptService{ExpectedValid: true}
|
||||||
|
a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}}
|
||||||
|
err := a.AuthenticateUser(context.Background(), &models.LoginUserQuery{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "",
|
Password: "",
|
||||||
}
|
})
|
||||||
a := AuthenticatorService{loginAttemptService: nil, loginService: &logintest.LoginServiceFake{}}
|
|
||||||
err := a.AuthenticateUser(context.Background(), &loginQuery)
|
|
||||||
|
|
||||||
require.EqualError(t, err, ErrPasswordEmpty.Error())
|
require.EqualError(t, err, ErrPasswordEmpty.Error())
|
||||||
assert.False(t, sc.grafanaLoginWasCalled)
|
assert.False(t, sc.grafanaLoginWasCalled)
|
||||||
@ -36,164 +35,154 @@ func TestAuthenticateUser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
authScenario(t, "When user authenticates with no auth provider enabled", func(sc *authScenarioContext) {
|
authScenario(t, "When user authenticates with no auth provider enabled", func(sc *authScenarioContext) {
|
||||||
mockLoginAttemptValidation(nil, sc)
|
|
||||||
sc.loginUserQuery.Cfg.DisableLogin = true
|
sc.loginUserQuery.Cfg.DisableLogin = true
|
||||||
|
|
||||||
a := AuthenticatorService{loginAttemptService: nil, loginService: &logintest.LoginServiceFake{}}
|
loginAttemptService := &loginattempttest.MockLoginAttemptService{ExpectedValid: true}
|
||||||
|
a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}}
|
||||||
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
||||||
|
|
||||||
require.EqualError(t, err, ErrNoAuthProvider.Error())
|
require.EqualError(t, err, ErrNoAuthProvider.Error())
|
||||||
assert.True(t, sc.loginAttemptValidationWasCalled)
|
|
||||||
assert.False(t, sc.grafanaLoginWasCalled)
|
assert.False(t, sc.grafanaLoginWasCalled)
|
||||||
assert.False(t, sc.ldapLoginWasCalled)
|
assert.False(t, sc.ldapLoginWasCalled)
|
||||||
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
|
|
||||||
assert.Equal(t, "", sc.loginUserQuery.AuthModule)
|
assert.Equal(t, "", sc.loginUserQuery.AuthModule)
|
||||||
|
assert.False(t, loginAttemptService.AddCalled)
|
||||||
|
assert.True(t, loginAttemptService.ValidateCalled)
|
||||||
})
|
})
|
||||||
|
|
||||||
authScenario(t, "When a user authenticates having too many login attempts", func(sc *authScenarioContext) {
|
authScenario(t, "When a user authenticates having too many login attempts", func(sc *authScenarioContext) {
|
||||||
mockLoginAttemptValidation(ErrTooManyLoginAttempts, sc)
|
|
||||||
mockLoginUsingGrafanaDB(nil, sc)
|
mockLoginUsingGrafanaDB(nil, sc)
|
||||||
mockLoginUsingLDAP(true, nil, sc)
|
mockLoginUsingLDAP(true, nil, sc)
|
||||||
mockSaveInvalidLoginAttempt(sc)
|
|
||||||
|
|
||||||
a := AuthenticatorService{loginService: &logintest.LoginServiceFake{}}
|
loginAttemptService := &loginattempttest.MockLoginAttemptService{ExpectedValid: false}
|
||||||
|
a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}}
|
||||||
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
||||||
|
|
||||||
require.EqualError(t, err, ErrTooManyLoginAttempts.Error())
|
require.EqualError(t, err, ErrTooManyLoginAttempts.Error())
|
||||||
assert.True(t, sc.loginAttemptValidationWasCalled)
|
|
||||||
assert.False(t, sc.grafanaLoginWasCalled)
|
assert.False(t, sc.grafanaLoginWasCalled)
|
||||||
assert.False(t, sc.ldapLoginWasCalled)
|
assert.False(t, sc.ldapLoginWasCalled)
|
||||||
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
|
|
||||||
assert.Empty(t, sc.loginUserQuery.AuthModule)
|
assert.Empty(t, sc.loginUserQuery.AuthModule)
|
||||||
|
assert.False(t, loginAttemptService.AddCalled)
|
||||||
|
assert.True(t, loginAttemptService.ValidateCalled)
|
||||||
})
|
})
|
||||||
|
|
||||||
authScenario(t, "When grafana user authenticate with valid credentials", func(sc *authScenarioContext) {
|
authScenario(t, "When grafana user authenticate with valid credentials", func(sc *authScenarioContext) {
|
||||||
mockLoginAttemptValidation(nil, sc)
|
|
||||||
mockLoginUsingGrafanaDB(nil, sc)
|
mockLoginUsingGrafanaDB(nil, sc)
|
||||||
mockLoginUsingLDAP(true, ErrInvalidCredentials, sc)
|
mockLoginUsingLDAP(true, ErrInvalidCredentials, sc)
|
||||||
mockSaveInvalidLoginAttempt(sc)
|
|
||||||
|
|
||||||
a := AuthenticatorService{loginService: &logintest.LoginServiceFake{}}
|
loginAttemptService := &loginattempttest.MockLoginAttemptService{ExpectedValid: true}
|
||||||
|
a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}}
|
||||||
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, sc.loginAttemptValidationWasCalled)
|
|
||||||
assert.True(t, sc.grafanaLoginWasCalled)
|
assert.True(t, sc.grafanaLoginWasCalled)
|
||||||
assert.False(t, sc.ldapLoginWasCalled)
|
assert.False(t, sc.ldapLoginWasCalled)
|
||||||
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
|
|
||||||
assert.Equal(t, "grafana", sc.loginUserQuery.AuthModule)
|
assert.Equal(t, "grafana", sc.loginUserQuery.AuthModule)
|
||||||
|
assert.False(t, loginAttemptService.AddCalled)
|
||||||
|
assert.True(t, loginAttemptService.ValidateCalled)
|
||||||
})
|
})
|
||||||
|
|
||||||
authScenario(t, "When grafana user authenticate and unexpected error occurs", func(sc *authScenarioContext) {
|
authScenario(t, "When grafana user authenticate and unexpected error occurs", func(sc *authScenarioContext) {
|
||||||
customErr := errors.New("custom")
|
customErr := errors.New("custom")
|
||||||
mockLoginAttemptValidation(nil, sc)
|
|
||||||
mockLoginUsingGrafanaDB(customErr, sc)
|
mockLoginUsingGrafanaDB(customErr, sc)
|
||||||
mockLoginUsingLDAP(true, ErrInvalidCredentials, sc)
|
mockLoginUsingLDAP(true, ErrInvalidCredentials, sc)
|
||||||
mockSaveInvalidLoginAttempt(sc)
|
|
||||||
|
|
||||||
a := AuthenticatorService{loginService: &logintest.LoginServiceFake{}}
|
loginAttemptService := &loginattempttest.MockLoginAttemptService{ExpectedValid: true}
|
||||||
|
a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}}
|
||||||
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
||||||
|
|
||||||
require.EqualError(t, err, customErr.Error())
|
require.EqualError(t, err, customErr.Error())
|
||||||
assert.True(t, sc.loginAttemptValidationWasCalled)
|
|
||||||
assert.True(t, sc.grafanaLoginWasCalled)
|
assert.True(t, sc.grafanaLoginWasCalled)
|
||||||
assert.False(t, sc.ldapLoginWasCalled)
|
assert.False(t, sc.ldapLoginWasCalled)
|
||||||
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
|
|
||||||
assert.Equal(t, "grafana", sc.loginUserQuery.AuthModule)
|
assert.Equal(t, "grafana", sc.loginUserQuery.AuthModule)
|
||||||
|
assert.False(t, loginAttemptService.AddCalled)
|
||||||
|
assert.True(t, loginAttemptService.ValidateCalled)
|
||||||
})
|
})
|
||||||
|
|
||||||
authScenario(t, "When a non-existing grafana user authenticate and ldap disabled", func(sc *authScenarioContext) {
|
authScenario(t, "When a non-existing grafana user authenticate and ldap disabled", func(sc *authScenarioContext) {
|
||||||
mockLoginAttemptValidation(nil, sc)
|
|
||||||
mockLoginUsingGrafanaDB(user.ErrUserNotFound, sc)
|
mockLoginUsingGrafanaDB(user.ErrUserNotFound, sc)
|
||||||
mockLoginUsingLDAP(false, nil, sc)
|
mockLoginUsingLDAP(false, nil, sc)
|
||||||
mockSaveInvalidLoginAttempt(sc)
|
|
||||||
|
|
||||||
a := AuthenticatorService{loginService: &logintest.LoginServiceFake{}}
|
loginAttemptService := &loginattempttest.MockLoginAttemptService{ExpectedValid: true}
|
||||||
|
a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}}
|
||||||
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
||||||
|
|
||||||
require.EqualError(t, err, user.ErrUserNotFound.Error())
|
require.EqualError(t, err, user.ErrUserNotFound.Error())
|
||||||
assert.True(t, sc.loginAttemptValidationWasCalled)
|
|
||||||
assert.True(t, sc.grafanaLoginWasCalled)
|
assert.True(t, sc.grafanaLoginWasCalled)
|
||||||
assert.True(t, sc.ldapLoginWasCalled)
|
assert.True(t, sc.ldapLoginWasCalled)
|
||||||
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
|
|
||||||
assert.Empty(t, sc.loginUserQuery.AuthModule)
|
assert.Empty(t, sc.loginUserQuery.AuthModule)
|
||||||
|
assert.False(t, loginAttemptService.AddCalled)
|
||||||
|
assert.True(t, loginAttemptService.ValidateCalled)
|
||||||
})
|
})
|
||||||
|
|
||||||
authScenario(t, "When a non-existing grafana user authenticate and invalid ldap credentials", func(sc *authScenarioContext) {
|
authScenario(t, "When a non-existing grafana user authenticate and invalid ldap credentials", func(sc *authScenarioContext) {
|
||||||
mockLoginAttemptValidation(nil, sc)
|
|
||||||
mockLoginUsingGrafanaDB(user.ErrUserNotFound, sc)
|
mockLoginUsingGrafanaDB(user.ErrUserNotFound, sc)
|
||||||
mockLoginUsingLDAP(true, ldap.ErrInvalidCredentials, sc)
|
mockLoginUsingLDAP(true, ldap.ErrInvalidCredentials, sc)
|
||||||
mockSaveInvalidLoginAttempt(sc)
|
|
||||||
|
|
||||||
a := AuthenticatorService{loginService: &logintest.LoginServiceFake{}}
|
loginAttemptService := &loginattempttest.MockLoginAttemptService{ExpectedValid: true}
|
||||||
|
a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}}
|
||||||
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
||||||
|
|
||||||
require.EqualError(t, err, ErrInvalidCredentials.Error())
|
require.EqualError(t, err, ErrInvalidCredentials.Error())
|
||||||
assert.True(t, sc.loginAttemptValidationWasCalled)
|
|
||||||
assert.True(t, sc.grafanaLoginWasCalled)
|
assert.True(t, sc.grafanaLoginWasCalled)
|
||||||
assert.True(t, sc.ldapLoginWasCalled)
|
assert.True(t, sc.ldapLoginWasCalled)
|
||||||
assert.True(t, sc.saveInvalidLoginAttemptWasCalled)
|
|
||||||
assert.Equal(t, login.LDAPAuthModule, sc.loginUserQuery.AuthModule)
|
assert.Equal(t, login.LDAPAuthModule, sc.loginUserQuery.AuthModule)
|
||||||
|
assert.True(t, loginAttemptService.AddCalled)
|
||||||
|
assert.True(t, loginAttemptService.ValidateCalled)
|
||||||
})
|
})
|
||||||
|
|
||||||
authScenario(t, "When a non-existing grafana user authenticate and valid ldap credentials", func(sc *authScenarioContext) {
|
authScenario(t, "When a non-existing grafana user authenticate and valid ldap credentials", func(sc *authScenarioContext) {
|
||||||
mockLoginAttemptValidation(nil, sc)
|
|
||||||
mockLoginUsingGrafanaDB(user.ErrUserNotFound, sc)
|
mockLoginUsingGrafanaDB(user.ErrUserNotFound, sc)
|
||||||
mockLoginUsingLDAP(true, nil, sc)
|
mockLoginUsingLDAP(true, nil, sc)
|
||||||
mockSaveInvalidLoginAttempt(sc)
|
|
||||||
|
|
||||||
a := AuthenticatorService{loginService: &logintest.LoginServiceFake{}}
|
loginAttemptService := &loginattempttest.MockLoginAttemptService{ExpectedValid: true}
|
||||||
|
a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}}
|
||||||
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, sc.loginAttemptValidationWasCalled)
|
|
||||||
assert.True(t, sc.grafanaLoginWasCalled)
|
assert.True(t, sc.grafanaLoginWasCalled)
|
||||||
assert.True(t, sc.ldapLoginWasCalled)
|
assert.True(t, sc.ldapLoginWasCalled)
|
||||||
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
|
|
||||||
assert.Equal(t, login.LDAPAuthModule, sc.loginUserQuery.AuthModule)
|
assert.Equal(t, login.LDAPAuthModule, sc.loginUserQuery.AuthModule)
|
||||||
|
assert.False(t, loginAttemptService.AddCalled)
|
||||||
|
assert.True(t, loginAttemptService.ValidateCalled)
|
||||||
})
|
})
|
||||||
|
|
||||||
authScenario(t, "When a non-existing grafana user authenticate and ldap returns unexpected error", func(sc *authScenarioContext) {
|
authScenario(t, "When a non-existing grafana user authenticate and ldap returns unexpected error", func(sc *authScenarioContext) {
|
||||||
customErr := errors.New("custom")
|
customErr := errors.New("custom")
|
||||||
mockLoginAttemptValidation(nil, sc)
|
|
||||||
mockLoginUsingGrafanaDB(user.ErrUserNotFound, sc)
|
mockLoginUsingGrafanaDB(user.ErrUserNotFound, sc)
|
||||||
mockLoginUsingLDAP(true, customErr, sc)
|
mockLoginUsingLDAP(true, customErr, sc)
|
||||||
mockSaveInvalidLoginAttempt(sc)
|
|
||||||
|
|
||||||
a := AuthenticatorService{loginService: &logintest.LoginServiceFake{}}
|
loginAttemptService := &loginattempttest.MockLoginAttemptService{ExpectedValid: true}
|
||||||
|
a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}}
|
||||||
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
||||||
|
|
||||||
require.EqualError(t, err, customErr.Error())
|
require.EqualError(t, err, customErr.Error())
|
||||||
assert.True(t, sc.loginAttemptValidationWasCalled)
|
|
||||||
assert.True(t, sc.grafanaLoginWasCalled)
|
assert.True(t, sc.grafanaLoginWasCalled)
|
||||||
assert.True(t, sc.ldapLoginWasCalled)
|
assert.True(t, sc.ldapLoginWasCalled)
|
||||||
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
|
|
||||||
assert.Equal(t, login.LDAPAuthModule, sc.loginUserQuery.AuthModule)
|
assert.Equal(t, login.LDAPAuthModule, sc.loginUserQuery.AuthModule)
|
||||||
|
assert.False(t, loginAttemptService.AddCalled)
|
||||||
|
assert.True(t, loginAttemptService.ValidateCalled)
|
||||||
})
|
})
|
||||||
|
|
||||||
authScenario(t, "When grafana user authenticate with invalid credentials and invalid ldap credentials", func(sc *authScenarioContext) {
|
authScenario(t, "When grafana user authenticate with invalid credentials and invalid ldap credentials", func(sc *authScenarioContext) {
|
||||||
mockLoginAttemptValidation(nil, sc)
|
|
||||||
mockLoginUsingGrafanaDB(ErrInvalidCredentials, sc)
|
mockLoginUsingGrafanaDB(ErrInvalidCredentials, sc)
|
||||||
mockLoginUsingLDAP(true, ldap.ErrInvalidCredentials, sc)
|
mockLoginUsingLDAP(true, ldap.ErrInvalidCredentials, sc)
|
||||||
mockSaveInvalidLoginAttempt(sc)
|
|
||||||
|
|
||||||
a := AuthenticatorService{loginService: &logintest.LoginServiceFake{}}
|
loginAttemptService := &loginattempttest.MockLoginAttemptService{ExpectedValid: true}
|
||||||
|
a := AuthenticatorService{loginAttemptService: loginAttemptService, loginService: &logintest.LoginServiceFake{}}
|
||||||
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
err := a.AuthenticateUser(context.Background(), sc.loginUserQuery)
|
||||||
|
|
||||||
require.EqualError(t, err, ErrInvalidCredentials.Error())
|
require.EqualError(t, err, ErrInvalidCredentials.Error())
|
||||||
assert.True(t, sc.loginAttemptValidationWasCalled)
|
|
||||||
assert.True(t, sc.grafanaLoginWasCalled)
|
assert.True(t, sc.grafanaLoginWasCalled)
|
||||||
assert.True(t, sc.ldapLoginWasCalled)
|
assert.True(t, sc.ldapLoginWasCalled)
|
||||||
assert.True(t, sc.saveInvalidLoginAttemptWasCalled)
|
assert.True(t, loginAttemptService.AddCalled)
|
||||||
|
assert.True(t, loginAttemptService.ValidateCalled)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type authScenarioContext struct {
|
type authScenarioContext struct {
|
||||||
loginUserQuery *models.LoginUserQuery
|
loginUserQuery *models.LoginUserQuery
|
||||||
grafanaLoginWasCalled bool
|
grafanaLoginWasCalled bool
|
||||||
ldapLoginWasCalled bool
|
ldapLoginWasCalled bool
|
||||||
loginAttemptValidationWasCalled bool
|
|
||||||
saveInvalidLoginAttemptWasCalled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type authScenarioFunc func(sc *authScenarioContext)
|
type authScenarioFunc func(sc *authScenarioContext)
|
||||||
@ -212,28 +201,12 @@ func mockLoginUsingLDAP(enabled bool, err error, sc *authScenarioContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
|
|
||||||
validateLoginAttempts = func(context.Context, *models.LoginUserQuery, loginattempt.Service) error {
|
|
||||||
sc.loginAttemptValidationWasCalled = true
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
|
|
||||||
saveInvalidLoginAttempt = func(ctx context.Context, query *models.LoginUserQuery, _ loginattempt.Service) error {
|
|
||||||
sc.saveInvalidLoginAttemptWasCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func authScenario(t *testing.T, desc string, fn authScenarioFunc) {
|
func authScenario(t *testing.T, desc string, fn authScenarioFunc) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
t.Run(desc, func(t *testing.T) {
|
t.Run(desc, func(t *testing.T) {
|
||||||
origLoginUsingGrafanaDB := loginUsingGrafanaDB
|
origLoginUsingGrafanaDB := loginUsingGrafanaDB
|
||||||
origLoginUsingLDAP := loginUsingLDAP
|
origLoginUsingLDAP := loginUsingLDAP
|
||||||
origValidateLoginAttempts := validateLoginAttempts
|
|
||||||
origSaveInvalidLoginAttempt := saveInvalidLoginAttempt
|
|
||||||
cfg := setting.Cfg{DisableLogin: false}
|
cfg := setting.Cfg{DisableLogin: false}
|
||||||
sc := &authScenarioContext{
|
sc := &authScenarioContext{
|
||||||
loginUserQuery: &models.LoginUserQuery{
|
loginUserQuery: &models.LoginUserQuery{
|
||||||
@ -247,8 +220,6 @@ func authScenario(t *testing.T, desc string, fn authScenarioFunc) {
|
|||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
loginUsingGrafanaDB = origLoginUsingGrafanaDB
|
loginUsingGrafanaDB = origLoginUsingGrafanaDB
|
||||||
loginUsingLDAP = origLoginUsingLDAP
|
loginUsingLDAP = origLoginUsingLDAP
|
||||||
validateLoginAttempts = origValidateLoginAttempts
|
|
||||||
saveInvalidLoginAttempt = origSaveInvalidLoginAttempt
|
|
||||||
})
|
})
|
||||||
|
|
||||||
fn(sc)
|
fn(sc)
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
package login
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/grafana/grafana/pkg/services/loginattempt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
maxInvalidLoginAttempts int64 = 5
|
|
||||||
loginAttemptsWindow = time.Minute * 5
|
|
||||||
)
|
|
||||||
|
|
||||||
var validateLoginAttempts = func(ctx context.Context, query *models.LoginUserQuery, loginAttemptService loginattempt.Service) error {
|
|
||||||
if query.Cfg.DisableBruteForceLoginProtection {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
loginAttemptCountQuery := models.GetUserLoginAttemptCountQuery{
|
|
||||||
Username: query.Username,
|
|
||||||
Since: time.Now().Add(-loginAttemptsWindow),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := loginAttemptService.GetUserLoginAttemptCount(ctx, &loginAttemptCountQuery); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if loginAttemptCountQuery.Result >= maxInvalidLoginAttempts {
|
|
||||||
return ErrTooManyLoginAttempts
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var saveInvalidLoginAttempt = func(ctx context.Context, query *models.LoginUserQuery, loginAttemptService loginattempt.Service) error {
|
|
||||||
if query.Cfg.DisableBruteForceLoginProtection {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
loginAttemptCommand := models.CreateLoginAttemptCommand{
|
|
||||||
Username: query.Username,
|
|
||||||
IpAddress: query.IpAddress,
|
|
||||||
}
|
|
||||||
|
|
||||||
return loginAttemptService.CreateLoginAttempt(ctx, &loginAttemptCommand)
|
|
||||||
}
|
|
@ -1,116 +0,0 @@
|
|||||||
package login
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateLoginAttempts(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
loginAttempts int64
|
|
||||||
cfg *setting.Cfg
|
|
||||||
expected error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "When brute force protection enabled and user login attempt count is less than max",
|
|
||||||
loginAttempts: maxInvalidLoginAttempts - 1,
|
|
||||||
cfg: cfgWithBruteForceLoginProtectionEnabled(t),
|
|
||||||
expected: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "When brute force protection enabled and user login attempt count equals max",
|
|
||||||
loginAttempts: maxInvalidLoginAttempts,
|
|
||||||
cfg: cfgWithBruteForceLoginProtectionEnabled(t),
|
|
||||||
expected: ErrTooManyLoginAttempts,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "When brute force protection enabled and user login attempt count is greater than max",
|
|
||||||
loginAttempts: maxInvalidLoginAttempts + 1,
|
|
||||||
cfg: cfgWithBruteForceLoginProtectionEnabled(t),
|
|
||||||
expected: ErrTooManyLoginAttempts,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "When brute force protection disabled and user login attempt count is less than max",
|
|
||||||
loginAttempts: maxInvalidLoginAttempts - 1,
|
|
||||||
cfg: cfgWithBruteForceLoginProtectionDisabled(t),
|
|
||||||
expected: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "When brute force protection disabled and user login attempt count equals max",
|
|
||||||
loginAttempts: maxInvalidLoginAttempts,
|
|
||||||
cfg: cfgWithBruteForceLoginProtectionDisabled(t),
|
|
||||||
expected: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "When brute force protection disabled and user login attempt count is greater than max",
|
|
||||||
loginAttempts: maxInvalidLoginAttempts + 1,
|
|
||||||
cfg: cfgWithBruteForceLoginProtectionDisabled(t),
|
|
||||||
expected: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
store := mockstore.NewSQLStoreMock()
|
|
||||||
store.ExpectedLoginAttempts = tc.loginAttempts
|
|
||||||
|
|
||||||
query := &models.LoginUserQuery{Username: "user", Cfg: tc.cfg}
|
|
||||||
|
|
||||||
err := validateLoginAttempts(context.Background(), query, store)
|
|
||||||
require.Equal(t, tc.expected, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSaveInvalidLoginAttempt(t *testing.T) {
|
|
||||||
t.Run("When brute force protection enabled", func(t *testing.T) {
|
|
||||||
store := mockstore.NewSQLStoreMock()
|
|
||||||
err := saveInvalidLoginAttempt(context.Background(), &models.LoginUserQuery{
|
|
||||||
Username: "user",
|
|
||||||
Password: "pwd",
|
|
||||||
IpAddress: "192.168.1.1:56433",
|
|
||||||
Cfg: cfgWithBruteForceLoginProtectionEnabled(t),
|
|
||||||
}, store)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NotNil(t, store.LastLoginAttemptCommand)
|
|
||||||
assert.Equal(t, "user", store.LastLoginAttemptCommand.Username)
|
|
||||||
assert.Equal(t, "192.168.1.1:56433", store.LastLoginAttemptCommand.IpAddress)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("When brute force protection disabled", func(t *testing.T) {
|
|
||||||
store := mockstore.NewSQLStoreMock()
|
|
||||||
err := saveInvalidLoginAttempt(context.Background(), &models.LoginUserQuery{
|
|
||||||
Username: "user",
|
|
||||||
Password: "pwd",
|
|
||||||
IpAddress: "192.168.1.1:56433",
|
|
||||||
Cfg: cfgWithBruteForceLoginProtectionDisabled(t),
|
|
||||||
}, store)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Nil(t, store.LastLoginAttemptCommand)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func cfgWithBruteForceLoginProtectionDisabled(t *testing.T) *setting.Cfg {
|
|
||||||
t.Helper()
|
|
||||||
cfg := setting.NewCfg()
|
|
||||||
cfg.DisableBruteForceLoginProtection = true
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func cfgWithBruteForceLoginProtectionEnabled(t *testing.T) *setting.Cfg {
|
|
||||||
t.Helper()
|
|
||||||
cfg := setting.NewCfg()
|
|
||||||
require.False(t, cfg.DisableBruteForceLoginProtection)
|
|
||||||
return cfg
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LoginAttempt struct {
|
|
||||||
Id int64
|
|
||||||
Username string
|
|
||||||
IpAddress string
|
|
||||||
Created int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------
|
|
||||||
// COMMANDS
|
|
||||||
|
|
||||||
type CreateLoginAttemptCommand struct {
|
|
||||||
Username string
|
|
||||||
IpAddress string
|
|
||||||
|
|
||||||
Result LoginAttempt
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteOldLoginAttemptsCommand struct {
|
|
||||||
OlderThan time.Time
|
|
||||||
DeletedRows int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------
|
|
||||||
// QUERIES
|
|
||||||
|
|
||||||
type GetUserLoginAttemptCountQuery struct {
|
|
||||||
Username string
|
|
||||||
Since time.Time
|
|
||||||
Result int64
|
|
||||||
}
|
|
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/live"
|
"github.com/grafana/grafana/pkg/services/live"
|
||||||
"github.com/grafana/grafana/pkg/services/live/pushhttp"
|
"github.com/grafana/grafana/pkg/services/live/pushhttp"
|
||||||
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
|
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
|
||||||
|
"github.com/grafana/grafana/pkg/services/loginattempt/loginattemptimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||||
"github.com/grafana/grafana/pkg/services/notifications"
|
"github.com/grafana/grafana/pkg/services/notifications"
|
||||||
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
|
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
|
||||||
@ -46,7 +47,7 @@ func ProvideBackgroundServiceRegistry(
|
|||||||
thumbnailsService thumbs.Service, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService,
|
thumbnailsService thumbs.Service, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService,
|
||||||
saService *samanager.ServiceAccountsService, authInfoService *authinfoservice.Implementation,
|
saService *samanager.ServiceAccountsService, authInfoService *authinfoservice.Implementation,
|
||||||
grpcServerProvider grpcserver.Provider,
|
grpcServerProvider grpcserver.Provider,
|
||||||
secretMigrationProvider secretsMigrations.SecretMigrationProvider,
|
secretMigrationProvider secretsMigrations.SecretMigrationProvider, loginAttemptService *loginattemptimpl.Service,
|
||||||
// Need to make sure these are initialized, is there a better place to put them?
|
// Need to make sure these are initialized, is there a better place to put them?
|
||||||
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
|
||||||
_ serviceaccounts.Service, _ *guardian.Provider,
|
_ serviceaccounts.Service, _ *guardian.Provider,
|
||||||
@ -81,6 +82,7 @@ func ProvideBackgroundServiceRegistry(
|
|||||||
authInfoService,
|
authInfoService,
|
||||||
processManager,
|
processManager,
|
||||||
secretMigrationProvider,
|
secretMigrationProvider,
|
||||||
|
loginAttemptService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +84,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
|
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
|
||||||
authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
|
authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
|
||||||
"github.com/grafana/grafana/pkg/services/login/loginservice"
|
"github.com/grafana/grafana/pkg/services/login/loginservice"
|
||||||
|
"github.com/grafana/grafana/pkg/services/loginattempt"
|
||||||
"github.com/grafana/grafana/pkg/services/loginattempt/loginattemptimpl"
|
"github.com/grafana/grafana/pkg/services/loginattempt/loginattemptimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/navtree/navtreeimpl"
|
"github.com/grafana/grafana/pkg/services/navtree/navtreeimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||||
@ -363,6 +364,7 @@ var wireBasicSet = wire.NewSet(
|
|||||||
teamimpl.ProvideService,
|
teamimpl.ProvideService,
|
||||||
tempuserimpl.ProvideService,
|
tempuserimpl.ProvideService,
|
||||||
loginattemptimpl.ProvideService,
|
loginattemptimpl.ProvideService,
|
||||||
|
wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)),
|
||||||
secretsMigrations.ProvideDataSourceMigrationService,
|
secretsMigrations.ProvideDataSourceMigrationService,
|
||||||
secretsMigrations.ProvideMigrateToPluginService,
|
secretsMigrations.ProvideMigrateToPluginService,
|
||||||
secretsMigrations.ProvideMigrateFromPluginService,
|
secretsMigrations.ProvideMigrateFromPluginService,
|
||||||
|
@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||||
"github.com/grafana/grafana/pkg/services/loginattempt"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/image"
|
"github.com/grafana/grafana/pkg/services/ngalert/image"
|
||||||
"github.com/grafana/grafana/pkg/services/queryhistory"
|
"github.com/grafana/grafana/pkg/services/queryhistory"
|
||||||
"github.com/grafana/grafana/pkg/services/shorturls"
|
"github.com/grafana/grafana/pkg/services/shorturls"
|
||||||
@ -31,7 +30,7 @@ import (
|
|||||||
func ProvideService(cfg *setting.Cfg, serverLockService *serverlock.ServerLockService,
|
func ProvideService(cfg *setting.Cfg, serverLockService *serverlock.ServerLockService,
|
||||||
shortURLService shorturls.Service, sqlstore db.DB, queryHistoryService queryhistory.Service,
|
shortURLService shorturls.Service, sqlstore db.DB, queryHistoryService queryhistory.Service,
|
||||||
dashboardVersionService dashver.Service, dashSnapSvc dashboardsnapshots.Service, deleteExpiredImageService *image.DeleteExpiredService,
|
dashboardVersionService dashver.Service, dashSnapSvc dashboardsnapshots.Service, deleteExpiredImageService *image.DeleteExpiredService,
|
||||||
loginAttemptService loginattempt.Service, tempUserService tempuser.Service, tracer tracing.Tracer, annotationCleaner annotations.Cleaner) *CleanUpService {
|
tempUserService tempuser.Service, tracer tracing.Tracer, annotationCleaner annotations.Cleaner) *CleanUpService {
|
||||||
s := &CleanUpService{
|
s := &CleanUpService{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
ServerLockService: serverLockService,
|
ServerLockService: serverLockService,
|
||||||
@ -42,7 +41,6 @@ func ProvideService(cfg *setting.Cfg, serverLockService *serverlock.ServerLockSe
|
|||||||
dashboardVersionService: dashboardVersionService,
|
dashboardVersionService: dashboardVersionService,
|
||||||
dashboardSnapshotService: dashSnapSvc,
|
dashboardSnapshotService: dashSnapSvc,
|
||||||
deleteExpiredImageService: deleteExpiredImageService,
|
deleteExpiredImageService: deleteExpiredImageService,
|
||||||
loginAttemptService: loginAttemptService,
|
|
||||||
tempUserService: tempUserService,
|
tempUserService: tempUserService,
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
annotationCleaner: annotationCleaner,
|
annotationCleaner: annotationCleaner,
|
||||||
@ -61,7 +59,6 @@ type CleanUpService struct {
|
|||||||
dashboardVersionService dashver.Service
|
dashboardVersionService dashver.Service
|
||||||
dashboardSnapshotService dashboardsnapshots.Service
|
dashboardSnapshotService dashboardsnapshots.Service
|
||||||
deleteExpiredImageService *image.DeleteExpiredService
|
deleteExpiredImageService *image.DeleteExpiredService
|
||||||
loginAttemptService loginattempt.Service
|
|
||||||
tempUserService tempuser.Service
|
tempUserService tempuser.Service
|
||||||
annotationCleaner annotations.Cleaner
|
annotationCleaner annotations.Cleaner
|
||||||
}
|
}
|
||||||
@ -106,7 +103,6 @@ func (srv *CleanUpService) clean(ctx context.Context) {
|
|||||||
{"expire old user invites", srv.expireOldUserInvites},
|
{"expire old user invites", srv.expireOldUserInvites},
|
||||||
{"delete stale short URLs", srv.deleteStaleShortURLs},
|
{"delete stale short URLs", srv.deleteStaleShortURLs},
|
||||||
{"delete stale query history", srv.deleteStaleQueryHistory},
|
{"delete stale query history", srv.deleteStaleQueryHistory},
|
||||||
{"delete old login attempts", srv.deleteOldLoginAttempts},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := srv.log.FromContext(ctx)
|
logger := srv.log.FromContext(ctx)
|
||||||
@ -227,33 +223,6 @@ func (srv *CleanUpService) deleteExpiredImages(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *CleanUpService) deleteOldLoginAttempts(ctx context.Context) {
|
|
||||||
logger := srv.log.FromContext(ctx)
|
|
||||||
err := srv.ServerLockService.LockAndExecute(ctx, "delete old login attempts",
|
|
||||||
time.Minute*10, func(context.Context) {
|
|
||||||
srv.deleteOldLoginAttemptsWithoutLock(ctx)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to lock and execute cleanup of old login attempts", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *CleanUpService) deleteOldLoginAttemptsWithoutLock(ctx context.Context) {
|
|
||||||
logger := srv.log.FromContext(ctx)
|
|
||||||
if srv.Cfg.DisableBruteForceLoginProtection {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := models.DeleteOldLoginAttemptsCommand{
|
|
||||||
OlderThan: time.Now().Add(time.Minute * -10),
|
|
||||||
}
|
|
||||||
if err := srv.loginAttemptService.DeleteOldLoginAttempts(ctx, &cmd); err != nil {
|
|
||||||
logger.Error("Problem deleting expired login attempts", "error", err.Error())
|
|
||||||
} else {
|
|
||||||
logger.Debug("Deleted expired login attempts", "rows affected", cmd.DeletedRows)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *CleanUpService) expireOldUserInvites(ctx context.Context) {
|
func (srv *CleanUpService) expireOldUserInvites(ctx context.Context) {
|
||||||
logger := srv.log.FromContext(ctx)
|
logger := srv.log.FromContext(ctx)
|
||||||
maxInviteLifetime := srv.Cfg.UserInviteMaxLifetime
|
maxInviteLifetime := srv.Cfg.UserInviteMaxLifetime
|
||||||
|
@ -2,12 +2,19 @@ package loginattempt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error
|
// Add adds a new login attempt record for provided username
|
||||||
DeleteOldLoginAttempts(ctx context.Context, cmd *models.DeleteOldLoginAttemptsCommand) error
|
Add(ctx context.Context, username, IPAddress string) error
|
||||||
GetUserLoginAttemptCount(ctx context.Context, query *models.GetUserLoginAttemptCountQuery) error
|
// Validate checks if username has to many login attempts inside a window.
|
||||||
|
// Will return true if provided username do not have too many attempts.
|
||||||
|
Validate(ctx context.Context, username string) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginAttempt struct {
|
||||||
|
Id int64
|
||||||
|
Username string
|
||||||
|
IpAddress string
|
||||||
|
Created int64
|
||||||
}
|
}
|
||||||
|
@ -5,40 +5,95 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/loginattempt"
|
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
const (
|
||||||
store store
|
maxInvalidLoginAttempts int64 = 5
|
||||||
}
|
loginAttemptsWindow = time.Minute * 5
|
||||||
|
)
|
||||||
|
|
||||||
func ProvideService(db db.DB) loginattempt.Service {
|
func ProvideService(db db.DB, cfg *setting.Cfg, lock *serverlock.ServerLockService) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
store: &xormStore{db: db, now: time.Now},
|
&xormStore{db: db, now: time.Now},
|
||||||
|
cfg,
|
||||||
|
lock,
|
||||||
|
log.New("login_attempt"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error {
|
type Service struct {
|
||||||
err := s.store.CreateLoginAttempt(ctx, cmd)
|
store store
|
||||||
if err != nil {
|
cfg *setting.Cfg
|
||||||
return err
|
lock *serverlock.ServerLockService
|
||||||
}
|
logger log.Logger
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) DeleteOldLoginAttempts(ctx context.Context, cmd *models.DeleteOldLoginAttemptsCommand) error {
|
func (s *Service) Run(ctx context.Context) error {
|
||||||
err := s.store.DeleteOldLoginAttempts(ctx, cmd)
|
// no need to run clean up job if it is disabled
|
||||||
if err != nil {
|
if s.cfg.DisableBruteForceLoginProtection {
|
||||||
return err
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(time.Minute * 10)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
s.cleanup(ctx)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetUserLoginAttemptCount(ctx context.Context, cmd *models.GetUserLoginAttemptCountQuery) error {
|
func (s *Service) Add(ctx context.Context, username, IPAddress string) error {
|
||||||
err := s.store.GetUserLoginAttemptCount(ctx, cmd)
|
if s.cfg.DisableBruteForceLoginProtection {
|
||||||
if err != nil {
|
return nil
|
||||||
return err
|
}
|
||||||
|
|
||||||
|
return s.store.CreateLoginAttempt(ctx, CreateLoginAttemptCommand{
|
||||||
|
Username: username,
|
||||||
|
IpAddress: IPAddress,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Validate(ctx context.Context, username string) (bool, error) {
|
||||||
|
if s.cfg.DisableBruteForceLoginProtection {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
loginAttemptCountQuery := GetUserLoginAttemptCountQuery{
|
||||||
|
Username: username,
|
||||||
|
Since: time.Now().Add(-loginAttemptsWindow),
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := s.store.GetUserLoginAttemptCount(ctx, loginAttemptCountQuery)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count >= maxInvalidLoginAttempts {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) cleanup(ctx context.Context) {
|
||||||
|
err := s.lock.LockAndExecute(ctx, "delete old login attempts", time.Minute*10, func(context.Context) {
|
||||||
|
cmd := DeleteOldLoginAttemptsCommand{
|
||||||
|
OlderThan: time.Now().Add(time.Minute * -10),
|
||||||
|
}
|
||||||
|
if deletedLogs, err := s.store.DeleteOldLoginAttempts(ctx, cmd); err != nil {
|
||||||
|
s.logger.Error("Problem deleting expired login attempts", "error", err.Error())
|
||||||
|
} else {
|
||||||
|
s.logger.Debug("Deleted expired login attempts", "rows affected", deletedLogs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("failed to lock and execute cleanup of old login attempts", "error", err)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
package loginattemptimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_Validate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
loginAttempts int64
|
||||||
|
disabled bool
|
||||||
|
expected bool
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "When brute force protection enabled and user login attempt count is less than max",
|
||||||
|
loginAttempts: maxInvalidLoginAttempts - 1,
|
||||||
|
expected: true,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When brute force protection enabled and user login attempt count equals max",
|
||||||
|
loginAttempts: maxInvalidLoginAttempts,
|
||||||
|
expected: false,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When brute force protection enabled and user login attempt count is greater than max",
|
||||||
|
loginAttempts: maxInvalidLoginAttempts + 1,
|
||||||
|
expected: false,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "When brute force protection disabled and user login attempt count is less than max",
|
||||||
|
loginAttempts: maxInvalidLoginAttempts - 1,
|
||||||
|
disabled: true,
|
||||||
|
expected: true,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When brute force protection disabled and user login attempt count equals max",
|
||||||
|
loginAttempts: maxInvalidLoginAttempts,
|
||||||
|
disabled: true,
|
||||||
|
expected: true,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When brute force protection disabled and user login attempt count is greater than max",
|
||||||
|
loginAttempts: maxInvalidLoginAttempts + 1,
|
||||||
|
disabled: true,
|
||||||
|
expected: true,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := setting.NewCfg()
|
||||||
|
cfg.DisableBruteForceLoginProtection = tt.disabled
|
||||||
|
service := &Service{
|
||||||
|
store: fakeStore{
|
||||||
|
ExpectedCount: tt.loginAttempts,
|
||||||
|
ExpectedErr: tt.expectedErr,
|
||||||
|
},
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := service.Validate(context.Background(), "test")
|
||||||
|
assert.Equal(t, tt.expected, ok)
|
||||||
|
assert.Equal(t, tt.expectedErr, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ store = new(fakeStore)
|
||||||
|
|
||||||
|
type fakeStore struct {
|
||||||
|
ExpectedErr error
|
||||||
|
ExpectedCount int64
|
||||||
|
ExpectedDeletedRows int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeStore) GetUserLoginAttemptCount(ctx context.Context, query GetUserLoginAttemptCountQuery) (int64, error) {
|
||||||
|
return f.ExpectedCount, f.ExpectedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeStore) CreateLoginAttempt(ctx context.Context, command CreateLoginAttemptCommand) error {
|
||||||
|
return f.ExpectedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeStore) DeleteOldLoginAttempts(ctx context.Context, command DeleteOldLoginAttemptsCommand) (int64, error) {
|
||||||
|
return f.ExpectedDeletedRows, f.ExpectedErr
|
||||||
|
}
|
23
pkg/services/loginattempt/loginattemptimpl/models.go
Normal file
23
pkg/services/loginattempt/loginattemptimpl/models.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package loginattemptimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/loginattempt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateLoginAttemptCommand struct {
|
||||||
|
Username string
|
||||||
|
IpAddress string
|
||||||
|
|
||||||
|
Result loginattempt.LoginAttempt
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUserLoginAttemptCountQuery struct {
|
||||||
|
Username string
|
||||||
|
Since time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteOldLoginAttemptsCommand struct {
|
||||||
|
OlderThan time.Time
|
||||||
|
}
|
@ -6,7 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/services/loginattempt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type xormStore struct {
|
type xormStore struct {
|
||||||
@ -15,14 +15,14 @@ type xormStore struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type store interface {
|
type store interface {
|
||||||
CreateLoginAttempt(context.Context, *models.CreateLoginAttemptCommand) error
|
CreateLoginAttempt(ctx context.Context, cmd CreateLoginAttemptCommand) error
|
||||||
DeleteOldLoginAttempts(context.Context, *models.DeleteOldLoginAttemptsCommand) error
|
DeleteOldLoginAttempts(ctx context.Context, cmd DeleteOldLoginAttemptsCommand) (int64, error)
|
||||||
GetUserLoginAttemptCount(context.Context, *models.GetUserLoginAttemptCountQuery) error
|
GetUserLoginAttemptCount(ctx context.Context, query GetUserLoginAttemptCountQuery) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xs *xormStore) CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error {
|
func (xs *xormStore) CreateLoginAttempt(ctx context.Context, cmd CreateLoginAttemptCommand) error {
|
||||||
return xs.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
return xs.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||||
loginAttempt := models.LoginAttempt{
|
loginAttempt := loginattempt.LoginAttempt{
|
||||||
Username: cmd.Username,
|
Username: cmd.Username,
|
||||||
IpAddress: cmd.IpAddress,
|
IpAddress: cmd.IpAddress,
|
||||||
Created: xs.now().Unix(),
|
Created: xs.now().Unix(),
|
||||||
@ -38,8 +38,9 @@ func (xs *xormStore) CreateLoginAttempt(ctx context.Context, cmd *models.CreateL
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xs *xormStore) DeleteOldLoginAttempts(ctx context.Context, cmd *models.DeleteOldLoginAttemptsCommand) error {
|
func (xs *xormStore) DeleteOldLoginAttempts(ctx context.Context, cmd DeleteOldLoginAttemptsCommand) (int64, error) {
|
||||||
return xs.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
var deletedRows int64
|
||||||
|
err := xs.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||||
var maxId int64
|
var maxId int64
|
||||||
sql := "SELECT max(id) as id FROM login_attempt WHERE created < ?"
|
sql := "SELECT max(id) as id FROM login_attempt WHERE created < ?"
|
||||||
result, err := sess.Query(sql, cmd.OlderThan.Unix())
|
result, err := sess.Query(sql, cmd.OlderThan.Unix())
|
||||||
@ -59,31 +60,38 @@ func (xs *xormStore) DeleteOldLoginAttempts(ctx context.Context, cmd *models.Del
|
|||||||
|
|
||||||
sql = "DELETE FROM login_attempt WHERE id <= ?"
|
sql = "DELETE FROM login_attempt WHERE id <= ?"
|
||||||
|
|
||||||
if result, err := sess.Exec(sql, maxId); err != nil {
|
deleteResult, err := sess.Exec(sql, maxId)
|
||||||
return err
|
|
||||||
} else if cmd.DeletedRows, err = result.RowsAffected(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (xs *xormStore) GetUserLoginAttemptCount(ctx context.Context, query *models.GetUserLoginAttemptCountQuery) error {
|
|
||||||
return xs.db.WithDbSession(ctx, func(dbSession *db.Session) error {
|
|
||||||
loginAttempt := new(models.LoginAttempt)
|
|
||||||
total, err := dbSession.
|
|
||||||
Where("username = ?", query.Username).
|
|
||||||
And("created >= ?", query.Since.Unix()).
|
|
||||||
Count(loginAttempt)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
query.Result = total
|
deletedRows, err = deleteResult.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
return deletedRows, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xs *xormStore) GetUserLoginAttemptCount(ctx context.Context, query GetUserLoginAttemptCountQuery) (int64, error) {
|
||||||
|
var total int64
|
||||||
|
err := xs.db.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||||
|
var queryErr error
|
||||||
|
loginAttempt := new(loginattempt.LoginAttempt)
|
||||||
|
total, queryErr = dbSession.
|
||||||
|
Where("username = ?", query.Username).
|
||||||
|
And("created >= ?", query.Since.Unix()).
|
||||||
|
Count(loginAttempt)
|
||||||
|
|
||||||
|
if queryErr != nil {
|
||||||
|
return queryErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func toInt64(i interface{}) int64 {
|
func toInt64(i interface{}) int64 {
|
||||||
|
@ -8,15 +8,12 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/grafana/grafana/pkg/services/loginattempt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIntegrationLoginAttemptsQuery(t *testing.T) {
|
func TestIntegrationLoginAttemptsQuery(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping integration test")
|
t.Skip("skipping integration test")
|
||||||
}
|
}
|
||||||
var loginAttemptService loginattempt.Service
|
|
||||||
user := "user"
|
user := "user"
|
||||||
|
|
||||||
beginningOfTime := time.Date(2017, 10, 22, 8, 0, 0, 0, time.Local)
|
beginningOfTime := time.Date(2017, 10, 22, 8, 0, 0, 0, time.Local)
|
||||||
@ -25,58 +22,60 @@ func TestIntegrationLoginAttemptsQuery(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
Name string
|
Name string
|
||||||
Query models.GetUserLoginAttemptCountQuery
|
Query GetUserLoginAttemptCountQuery
|
||||||
Err error
|
Err error
|
||||||
Result int64
|
Result int64
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"Should return a total count of zero login attempts when comparing since beginning of time + 2min and 1s",
|
"Should return a total count of zero login attempts when comparing since beginning of time + 2min and 1s",
|
||||||
models.GetUserLoginAttemptCountQuery{Username: user, Since: timePlusTwoMinutes.Add(time.Second * 1)}, nil, 0,
|
GetUserLoginAttemptCountQuery{Username: user, Since: timePlusTwoMinutes.Add(time.Second * 1)}, nil, 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Should return a total count of zero login attempts when comparing since beginning of time + 2min and 1s",
|
"Should return a total count of zero login attempts when comparing since beginning of time + 2min and 1s",
|
||||||
models.GetUserLoginAttemptCountQuery{Username: user, Since: timePlusTwoMinutes.Add(time.Second * 1)}, nil, 0,
|
GetUserLoginAttemptCountQuery{Username: user, Since: timePlusTwoMinutes.Add(time.Second * 1)}, nil, 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Should return the total count of login attempts since beginning of time",
|
"Should return the total count of login attempts since beginning of time",
|
||||||
models.GetUserLoginAttemptCountQuery{Username: user, Since: beginningOfTime}, nil, 3,
|
GetUserLoginAttemptCountQuery{Username: user, Since: beginningOfTime}, nil, 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Should return the total count of login attempts since beginning of time + 1min",
|
"Should return the total count of login attempts since beginning of time + 1min",
|
||||||
models.GetUserLoginAttemptCountQuery{Username: user, Since: timePlusOneMinute}, nil, 2,
|
GetUserLoginAttemptCountQuery{Username: user, Since: timePlusOneMinute}, nil, 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Should return the total count of login attempts since beginning of time + 2min",
|
"Should return the total count of login attempts since beginning of time + 2min",
|
||||||
models.GetUserLoginAttemptCountQuery{Username: user, Since: timePlusTwoMinutes}, nil, 1,
|
GetUserLoginAttemptCountQuery{Username: user, Since: timePlusTwoMinutes}, nil, 1,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mockTime := beginningOfTime
|
mockTime := beginningOfTime
|
||||||
loginAttemptService = &Service{
|
s := &xormStore{
|
||||||
store: &xormStore{
|
db: db.InitTestDB(t),
|
||||||
db: db.InitTestDB(t),
|
now: func() time.Time { return mockTime },
|
||||||
now: func() time.Time { return mockTime },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
err := loginAttemptService.CreateLoginAttempt(context.Background(), &models.CreateLoginAttemptCommand{
|
|
||||||
|
err := s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{
|
||||||
Username: user,
|
Username: user,
|
||||||
IpAddress: "192.168.0.1",
|
IpAddress: "192.168.0.1",
|
||||||
})
|
})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
mockTime = timePlusOneMinute
|
mockTime = timePlusOneMinute
|
||||||
err = loginAttemptService.CreateLoginAttempt(context.Background(), &models.CreateLoginAttemptCommand{
|
err = s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{
|
||||||
Username: user,
|
Username: user,
|
||||||
IpAddress: "192.168.0.1",
|
IpAddress: "192.168.0.1",
|
||||||
})
|
})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
mockTime = timePlusTwoMinutes
|
mockTime = timePlusTwoMinutes
|
||||||
err = loginAttemptService.CreateLoginAttempt(context.Background(), &models.CreateLoginAttemptCommand{
|
err = s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{
|
||||||
Username: user,
|
Username: user,
|
||||||
IpAddress: "192.168.0.1",
|
IpAddress: "192.168.0.1",
|
||||||
})
|
})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
err = loginAttemptService.GetUserLoginAttemptCount(context.Background(), &test.Query)
|
|
||||||
|
count, err := s.GetUserLoginAttemptCount(context.Background(), test.Query)
|
||||||
require.Equal(t, test.Err, err, test.Name)
|
require.Equal(t, test.Err, err, test.Name)
|
||||||
require.Equal(t, test.Result, test.Query.Result, test.Name)
|
require.Equal(t, test.Result, count, test.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +83,6 @@ func TestIntegrationLoginAttemptsDelete(t *testing.T) {
|
|||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping integration test")
|
t.Skip("skipping integration test")
|
||||||
}
|
}
|
||||||
var loginAttemptService loginattempt.Service
|
|
||||||
user := "user"
|
user := "user"
|
||||||
|
|
||||||
beginningOfTime := time.Date(2017, 10, 22, 8, 0, 0, 0, time.Local)
|
beginningOfTime := time.Date(2017, 10, 22, 8, 0, 0, 0, time.Local)
|
||||||
@ -93,53 +91,55 @@ func TestIntegrationLoginAttemptsDelete(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
Name string
|
Name string
|
||||||
Cmd models.DeleteOldLoginAttemptsCommand
|
Cmd DeleteOldLoginAttemptsCommand
|
||||||
Err error
|
Err error
|
||||||
DeletedRows int64
|
DeletedRows int64
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"Should return deleted rows older than beginning of time",
|
"Should return deleted rows older than beginning of time",
|
||||||
models.DeleteOldLoginAttemptsCommand{OlderThan: beginningOfTime}, nil, 0,
|
DeleteOldLoginAttemptsCommand{OlderThan: beginningOfTime}, nil, 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Should return deleted rows older than beginning of time + 1min",
|
"Should return deleted rows older than beginning of time + 1min",
|
||||||
models.DeleteOldLoginAttemptsCommand{OlderThan: timePlusOneMinute}, nil, 1,
|
DeleteOldLoginAttemptsCommand{OlderThan: timePlusOneMinute}, nil, 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Should return deleted rows older than beginning of time + 2min",
|
"Should return deleted rows older than beginning of time + 2min",
|
||||||
models.DeleteOldLoginAttemptsCommand{OlderThan: timePlusTwoMinutes}, nil, 2,
|
DeleteOldLoginAttemptsCommand{OlderThan: timePlusTwoMinutes}, nil, 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Should return deleted rows older than beginning of time + 2min and 1s",
|
"Should return deleted rows older than beginning of time + 2min and 1s",
|
||||||
models.DeleteOldLoginAttemptsCommand{OlderThan: timePlusTwoMinutes.Add(time.Second * 1)}, nil, 3,
|
DeleteOldLoginAttemptsCommand{OlderThan: timePlusTwoMinutes.Add(time.Second * 1)}, nil, 3,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
mockTime := beginningOfTime
|
mockTime := beginningOfTime
|
||||||
loginAttemptService = &Service{
|
s := &xormStore{
|
||||||
store: &xormStore{
|
db: db.InitTestDB(t),
|
||||||
db: db.InitTestDB(t),
|
now: func() time.Time { return mockTime },
|
||||||
now: func() time.Time { return mockTime },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
err := loginAttemptService.CreateLoginAttempt(context.Background(), &models.CreateLoginAttemptCommand{
|
|
||||||
|
err := s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{
|
||||||
Username: user,
|
Username: user,
|
||||||
IpAddress: "192.168.0.1",
|
IpAddress: "192.168.0.1",
|
||||||
})
|
})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
mockTime = timePlusOneMinute
|
mockTime = timePlusOneMinute
|
||||||
err = loginAttemptService.CreateLoginAttempt(context.Background(), &models.CreateLoginAttemptCommand{
|
err = s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{
|
||||||
Username: user,
|
Username: user,
|
||||||
IpAddress: "192.168.0.1",
|
IpAddress: "192.168.0.1",
|
||||||
})
|
})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
mockTime = timePlusTwoMinutes
|
mockTime = timePlusTwoMinutes
|
||||||
err = loginAttemptService.CreateLoginAttempt(context.Background(), &models.CreateLoginAttemptCommand{
|
err = s.CreateLoginAttempt(context.Background(), CreateLoginAttemptCommand{
|
||||||
Username: user,
|
Username: user,
|
||||||
IpAddress: "192.168.0.1",
|
IpAddress: "192.168.0.1",
|
||||||
})
|
})
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
err = loginAttemptService.DeleteOldLoginAttempts(context.Background(), &test.Cmd)
|
|
||||||
|
deletedRows, err := s.DeleteOldLoginAttempts(context.Background(), test.Cmd)
|
||||||
require.Equal(t, test.Err, err, test.Name)
|
require.Equal(t, test.Err, err, test.Name)
|
||||||
require.Equal(t, test.DeletedRows, test.Cmd.DeletedRows, test.Name)
|
require.Equal(t, test.DeletedRows, deletedRows, test.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
pkg/services/loginattempt/loginattempttest/fake.go
Normal file
22
pkg/services/loginattempt/loginattempttest/fake.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package loginattempttest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/loginattempt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ loginattempt.Service = new(FakeLoginAttemptService)
|
||||||
|
|
||||||
|
type FakeLoginAttemptService struct {
|
||||||
|
ExpectedValid bool
|
||||||
|
ExpectedErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FakeLoginAttemptService) Add(ctx context.Context, username, IPAddress string) error {
|
||||||
|
return f.ExpectedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FakeLoginAttemptService) Validate(ctx context.Context, username string) (bool, error) {
|
||||||
|
return f.ExpectedValid, f.ExpectedErr
|
||||||
|
}
|
27
pkg/services/loginattempt/loginattempttest/mock.go
Normal file
27
pkg/services/loginattempt/loginattempttest/mock.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package loginattempttest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/loginattempt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ loginattempt.Service = new(MockLoginAttemptService)
|
||||||
|
|
||||||
|
type MockLoginAttemptService struct {
|
||||||
|
AddCalled bool
|
||||||
|
ValidateCalled bool
|
||||||
|
|
||||||
|
ExpectedValid bool
|
||||||
|
ExpectedErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MockLoginAttemptService) Add(ctx context.Context, username, IPAddress string) error {
|
||||||
|
f.AddCalled = true
|
||||||
|
return f.ExpectedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MockLoginAttemptService) Validate(ctx context.Context, username string) (bool, error) {
|
||||||
|
f.ValidateCalled = true
|
||||||
|
return f.ExpectedValid, f.ExpectedErr
|
||||||
|
}
|
@ -17,8 +17,7 @@ type OrgListResponse []struct {
|
|||||||
Response error
|
Response error
|
||||||
}
|
}
|
||||||
type SQLStoreMock struct {
|
type SQLStoreMock struct {
|
||||||
LastGetAlertsQuery *models.GetAlertsQuery
|
LastGetAlertsQuery *models.GetAlertsQuery
|
||||||
LastLoginAttemptCommand *models.CreateLoginAttemptCommand
|
|
||||||
|
|
||||||
ExpectedUser *user.User
|
ExpectedUser *user.User
|
||||||
ExpectedTeamsByUser []*models.TeamDTO
|
ExpectedTeamsByUser []*models.TeamDTO
|
||||||
@ -28,7 +27,6 @@ type SQLStoreMock struct {
|
|||||||
ExpectedDataSourcesAccessStats []*models.DataSourceAccessStats
|
ExpectedDataSourcesAccessStats []*models.DataSourceAccessStats
|
||||||
ExpectedNotifierUsageStats []*models.NotifierUsageStats
|
ExpectedNotifierUsageStats []*models.NotifierUsageStats
|
||||||
ExpectedSignedInUser *user.SignedInUser
|
ExpectedSignedInUser *user.SignedInUser
|
||||||
ExpectedLoginAttempts int64
|
|
||||||
|
|
||||||
ExpectedError error
|
ExpectedError error
|
||||||
}
|
}
|
||||||
@ -130,11 +128,6 @@ func (m *SQLStoreMock) GetSqlxSession() *session.SessionDB {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SQLStoreMock) CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error {
|
|
||||||
m.LastLoginAttemptCommand = cmd
|
|
||||||
return m.ExpectedError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *SQLStoreMock) GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error {
|
func (m *SQLStoreMock) GetAlertById(ctx context.Context, query *models.GetAlertByIdQuery) error {
|
||||||
query.Result = m.ExpectedAlert
|
query.Result = m.ExpectedAlert
|
||||||
return m.ExpectedError
|
return m.ExpectedError
|
||||||
@ -144,19 +137,10 @@ func (m *SQLStoreMock) GetAlertNotificationUidWithId(ctx context.Context, query
|
|||||||
return m.ExpectedError
|
return m.ExpectedError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SQLStoreMock) DeleteOldLoginAttempts(ctx context.Context, cmd *models.DeleteOldLoginAttemptsCommand) error {
|
|
||||||
return m.ExpectedError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *SQLStoreMock) GetAlertNotificationsWithUidToSend(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error {
|
func (m *SQLStoreMock) GetAlertNotificationsWithUidToSend(ctx context.Context, query *models.GetAlertNotificationsWithUidToSendQuery) error {
|
||||||
return m.ExpectedError
|
return m.ExpectedError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SQLStoreMock) GetUserLoginAttemptCount(ctx context.Context, query *models.GetUserLoginAttemptCountQuery) error {
|
|
||||||
query.Result = m.ExpectedLoginAttempts
|
|
||||||
return m.ExpectedError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *SQLStoreMock) GetAlertStatesForDashboard(ctx context.Context, query *models.GetAlertStatesForDashboardQuery) error {
|
func (m *SQLStoreMock) GetAlertStatesForDashboard(ctx context.Context, query *models.GetAlertStatesForDashboardQuery) error {
|
||||||
return m.ExpectedError
|
return m.ExpectedError
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user