Skip to content

Commit

Permalink
LoginAttempt: Move logic around login attempts into the service (#58962)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kalleep committed Nov 22, 2022
1 parent 082c8ba commit 189bf10
Show file tree
Hide file tree
Showing 18 changed files with 376 additions and 403 deletions.
2 changes: 2 additions & 0 deletions pkg/cmd/grafana-cli/runner/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import (
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
"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/ngalert"
ngmetrics "github.com/grafana/grafana/pkg/services/ngalert/metrics"
Expand Down Expand Up @@ -227,6 +228,7 @@ var wireSet = wire.NewSet(
loginpkg.ProvideService,
wire.Bind(new(loginpkg.Authenticator), new(*loginpkg.AuthenticatorService)),
loginattemptimpl.ProvideService,
wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)),
datasourceproxy.ProvideService,
search.ProvideService,
searchV2.ProvideService,
Expand Down
9 changes: 6 additions & 3 deletions pkg/login/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,19 @@ func ProvideService(store db.DB, loginService login.Service, loginAttemptService

// AuthenticateUser authenticates the user via username & password
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
}
if !ok {
return ErrTooManyLoginAttempts
}

if err := validatePasswordSet(query.Password); err != nil {
return err
}

isGrafanaLoginEnabled := !query.Cfg.DisableLogin
var err error

if isGrafanaLoginEnabled {
err = loginUsingGrafanaDB(ctx, query, a.userService)
Expand All @@ -84,7 +87,7 @@ func (a *AuthenticatorService) AuthenticateUser(ctx context.Context, query *mode
}

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)
}

Expand Down
117 changes: 44 additions & 73 deletions pkg/login/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/login"
"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/setting"
"github.com/stretchr/testify/assert"
Expand All @@ -18,16 +18,15 @@ import (

func TestAuthenticateUser(t *testing.T) {
authScenario(t, "When a user authenticates without setting a password", func(sc *authScenarioContext) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(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",
Password: "",
}
a := AuthenticatorService{loginAttemptService: nil, loginService: &logintest.LoginServiceFake{}}
err := a.AuthenticateUser(context.Background(), &loginQuery)
})

require.EqualError(t, err, ErrPasswordEmpty.Error())
assert.False(t, sc.grafanaLoginWasCalled)
Expand All @@ -36,164 +35,154 @@ func TestAuthenticateUser(t *testing.T) {
})

authScenario(t, "When user authenticates with no auth provider enabled", func(sc *authScenarioContext) {
mockLoginAttemptValidation(nil, sc)
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)

require.EqualError(t, err, ErrNoAuthProvider.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
assert.False(t, sc.grafanaLoginWasCalled)
assert.False(t, sc.ldapLoginWasCalled)
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
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) {
mockLoginAttemptValidation(ErrTooManyLoginAttempts, sc)
mockLoginUsingGrafanaDB(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)

require.EqualError(t, err, ErrTooManyLoginAttempts.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
assert.False(t, sc.grafanaLoginWasCalled)
assert.False(t, sc.ldapLoginWasCalled)
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
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) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(nil, 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)

require.NoError(t, err)
assert.True(t, sc.loginAttemptValidationWasCalled)
assert.True(t, sc.grafanaLoginWasCalled)
assert.False(t, sc.ldapLoginWasCalled)
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
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) {
customErr := errors.New("custom")
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(customErr, 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)

require.EqualError(t, err, customErr.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
assert.True(t, sc.grafanaLoginWasCalled)
assert.False(t, sc.ldapLoginWasCalled)
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
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) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(user.ErrUserNotFound, 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)

require.EqualError(t, err, user.ErrUserNotFound.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
assert.True(t, sc.grafanaLoginWasCalled)
assert.True(t, sc.ldapLoginWasCalled)
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
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) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(user.ErrUserNotFound, 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)

require.EqualError(t, err, ErrInvalidCredentials.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
assert.True(t, sc.grafanaLoginWasCalled)
assert.True(t, sc.ldapLoginWasCalled)
assert.True(t, sc.saveInvalidLoginAttemptWasCalled)
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) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(user.ErrUserNotFound, 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)

require.NoError(t, err)
assert.True(t, sc.loginAttemptValidationWasCalled)
assert.True(t, sc.grafanaLoginWasCalled)
assert.True(t, sc.ldapLoginWasCalled)
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
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) {
customErr := errors.New("custom")
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(user.ErrUserNotFound, 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)

require.EqualError(t, err, customErr.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
assert.True(t, sc.grafanaLoginWasCalled)
assert.True(t, sc.ldapLoginWasCalled)
assert.False(t, sc.saveInvalidLoginAttemptWasCalled)
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) {
mockLoginAttemptValidation(nil, sc)
mockLoginUsingGrafanaDB(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)

require.EqualError(t, err, ErrInvalidCredentials.Error())
assert.True(t, sc.loginAttemptValidationWasCalled)
assert.True(t, sc.grafanaLoginWasCalled)
assert.True(t, sc.ldapLoginWasCalled)
assert.True(t, sc.saveInvalidLoginAttemptWasCalled)
assert.True(t, loginAttemptService.AddCalled)
assert.True(t, loginAttemptService.ValidateCalled)
})
}

type authScenarioContext struct {
loginUserQuery *models.LoginUserQuery
grafanaLoginWasCalled bool
ldapLoginWasCalled bool
loginAttemptValidationWasCalled bool
saveInvalidLoginAttemptWasCalled bool
loginUserQuery *models.LoginUserQuery
grafanaLoginWasCalled bool
ldapLoginWasCalled bool
}

type authScenarioFunc func(sc *authScenarioContext)
Expand All @@ -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) {
t.Helper()

t.Run(desc, func(t *testing.T) {
origLoginUsingGrafanaDB := loginUsingGrafanaDB
origLoginUsingLDAP := loginUsingLDAP
origValidateLoginAttempts := validateLoginAttempts
origSaveInvalidLoginAttempt := saveInvalidLoginAttempt
cfg := setting.Cfg{DisableLogin: false}
sc := &authScenarioContext{
loginUserQuery: &models.LoginUserQuery{
Expand All @@ -247,8 +220,6 @@ func authScenario(t *testing.T, desc string, fn authScenarioFunc) {
t.Cleanup(func() {
loginUsingGrafanaDB = origLoginUsingGrafanaDB
loginUsingLDAP = origLoginUsingLDAP
validateLoginAttempts = origValidateLoginAttempts
saveInvalidLoginAttempt = origSaveInvalidLoginAttempt
})

fn(sc)
Expand Down
48 changes: 0 additions & 48 deletions pkg/login/brute_force_login_protection.go

This file was deleted.

0 comments on commit 189bf10

Please sign in to comment.