mirror of https://github.com/grafana/grafana.git
				
				
				
			WIP: Protect against brute force (frequent) login attempts (#10031)
* db: add login attempt migrations * db: add possibility to create login attempts * db: add possibility to retrieve login attempt count per username * auth: validation and update of login attempts for invalid credentials If login attempt count for user authenticating is 5 or more the last 5 minutes we temporarily block the user access to login * db: add possibility to delete expired login attempts * cleanup: Delete login attempts older than 10 minutes The cleanup job are running continuously and triggering each 10 minute * fix typo: rename consequent to consequent * auth: enable login attempt validation for ldap logins * auth: disable login attempts validation by configuration Setting is named DisableLoginAttemptsValidation and is false by default Config disable_login_attempts_validation is placed under security section #7616 * auth: don't run cleanup of login attempts if feature is disabled #7616 * auth: rename settings.go to ldap_settings.go * auth: refactor AuthenticateUser Extract grafana login, ldap login and login attemp validation together with their tests to separate files. Enables testing of many more aspects when authenticating a user. #7616 * auth: rename login attempt validation to brute force login protection Setting DisableLoginAttemptsValidation => DisableBruteForceLoginProtection Configuration disable_login_attempts_validation => disable_brute_force_login_protection #7616
This commit is contained in:
		
							parent
							
								
									475febd004
								
							
						
					
					
						commit
						3d1c624c12
					
				|  | @ -174,6 +174,9 @@ disable_gravatar = false | |||
| # data source proxy whitelist (ip_or_domain:port separated by spaces) | ||||
| data_source_proxy_whitelist = | ||||
| 
 | ||||
| # disable protection against brute force login attempts | ||||
| disable_brute_force_login_protection = false | ||||
| 
 | ||||
| #################################### Snapshots ########################### | ||||
| [snapshots] | ||||
| # snapshot sharing options | ||||
|  |  | |||
|  | @ -162,6 +162,9 @@ log_queries = | |||
| # data source proxy whitelist (ip_or_domain:port separated by spaces) | ||||
| ;data_source_proxy_whitelist = | ||||
| 
 | ||||
| # disable protection against brute force login attempts | ||||
| ;disable_brute_force_login_protection = false | ||||
| 
 | ||||
| #################################### Snapshots ########################### | ||||
| [snapshots] | ||||
| # snapshot sharing options | ||||
|  |  | |||
|  | @ -104,10 +104,11 @@ func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response { | |||
| 	authQuery := login.LoginUserQuery{ | ||||
| 		Username:  cmd.User, | ||||
| 		Password:  cmd.Password, | ||||
| 		IpAddress: c.Req.RemoteAddr, | ||||
| 	} | ||||
| 
 | ||||
| 	if err := bus.Dispatch(&authQuery); err != nil { | ||||
| 		if err == login.ErrInvalidCredentials { | ||||
| 		if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts { | ||||
| 			return ApiError(401, "Invalid username or password", err) | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,21 +3,20 @@ package login | |||
| import ( | ||||
| 	"errors" | ||||
| 
 | ||||
| 	"crypto/subtle" | ||||
| 	"github.com/grafana/grafana/pkg/bus" | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| 	"github.com/grafana/grafana/pkg/util" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrInvalidCredentials   = errors.New("Invalid Username or Password") | ||||
| 	ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked") | ||||
| ) | ||||
| 
 | ||||
| type LoginUserQuery struct { | ||||
| 	Username  string | ||||
| 	Password  string | ||||
| 	User      *m.User | ||||
| 	IpAddress string | ||||
| } | ||||
| 
 | ||||
| func Init() { | ||||
|  | @ -26,41 +25,31 @@ func Init() { | |||
| } | ||||
| 
 | ||||
| func AuthenticateUser(query *LoginUserQuery) error { | ||||
| 	if err := validateLoginAttempts(query.Username); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	err := loginUsingGrafanaDB(query) | ||||
| 	if err == nil || err != ErrInvalidCredentials { | ||||
| 	if err == nil || (err != m.ErrUserNotFound && err != ErrInvalidCredentials) { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if setting.LdapEnabled { | ||||
| 		for _, server := range LdapCfg.Servers { | ||||
| 			author := NewLdapAuthenticator(server) | ||||
| 			err = author.Login(query) | ||||
| 			if err == nil || err != ErrInvalidCredentials { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	ldapEnabled, ldapErr := loginUsingLdap(query) | ||||
| 	if ldapEnabled { | ||||
| 		if ldapErr == nil || ldapErr != ErrInvalidCredentials { | ||||
| 			return ldapErr | ||||
| 		} | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
| 		err = ldapErr | ||||
| 	} | ||||
| 
 | ||||
| func loginUsingGrafanaDB(query *LoginUserQuery) error { | ||||
| 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username} | ||||
| 	if err == ErrInvalidCredentials { | ||||
| 		saveInvalidLoginAttempt(query) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := bus.Dispatch(&userQuery); err != nil { | ||||
| 	if err == m.ErrUserNotFound { | ||||
| 		return ErrInvalidCredentials | ||||
| 	} | ||||
| 
 | ||||
| 	return err | ||||
| 	} | ||||
| 
 | ||||
| 	user := userQuery.Result | ||||
| 
 | ||||
| 	passwordHashed := util.EncodePassword(query.Password, user.Salt) | ||||
| 	if subtle.ConstantTimeCompare([]byte(passwordHashed), []byte(user.Password)) != 1 { | ||||
| 		return ErrInvalidCredentials | ||||
| 	} | ||||
| 
 | ||||
| 	query.User = user | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,214 @@ | |||
| package login | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| ) | ||||
| 
 | ||||
| func TestAuthenticateUser(t *testing.T) { | ||||
| 	Convey("Authenticate user", t, func() { | ||||
| 		authScenario("When a user authenticates having too many login attempts", func(sc *authScenarioContext) { | ||||
| 			mockLoginAttemptValidation(ErrTooManyLoginAttempts, sc) | ||||
| 			mockLoginUsingGrafanaDB(nil, sc) | ||||
| 			mockLoginUsingLdap(true, nil, sc) | ||||
| 			mockSaveInvalidLoginAttempt(sc) | ||||
| 
 | ||||
| 			err := AuthenticateUser(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should result in", func() { | ||||
| 				So(err, ShouldEqual, ErrTooManyLoginAttempts) | ||||
| 				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue) | ||||
| 				So(sc.grafanaLoginWasCalled, ShouldBeFalse) | ||||
| 				So(sc.ldapLoginWasCalled, ShouldBeFalse) | ||||
| 				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		authScenario("When grafana user authenticate with valid credentials", func(sc *authScenarioContext) { | ||||
| 			mockLoginAttemptValidation(nil, sc) | ||||
| 			mockLoginUsingGrafanaDB(nil, sc) | ||||
| 			mockLoginUsingLdap(true, ErrInvalidCredentials, sc) | ||||
| 			mockSaveInvalidLoginAttempt(sc) | ||||
| 
 | ||||
| 			err := AuthenticateUser(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should result in", func() { | ||||
| 				So(err, ShouldEqual, nil) | ||||
| 				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue) | ||||
| 				So(sc.grafanaLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.ldapLoginWasCalled, ShouldBeFalse) | ||||
| 				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		authScenario("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) | ||||
| 
 | ||||
| 			err := AuthenticateUser(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should result in", func() { | ||||
| 				So(err, ShouldEqual, customErr) | ||||
| 				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue) | ||||
| 				So(sc.grafanaLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.ldapLoginWasCalled, ShouldBeFalse) | ||||
| 				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		authScenario("When a non-existing grafana user authenticate and ldap disabled", func(sc *authScenarioContext) { | ||||
| 			mockLoginAttemptValidation(nil, sc) | ||||
| 			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc) | ||||
| 			mockLoginUsingLdap(false, nil, sc) | ||||
| 			mockSaveInvalidLoginAttempt(sc) | ||||
| 
 | ||||
| 			err := AuthenticateUser(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should result in", func() { | ||||
| 				So(err, ShouldEqual, ErrInvalidCredentials) | ||||
| 				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue) | ||||
| 				So(sc.grafanaLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.ldapLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		authScenario("When a non-existing grafana user authenticate and invalid ldap credentials", func(sc *authScenarioContext) { | ||||
| 			mockLoginAttemptValidation(nil, sc) | ||||
| 			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc) | ||||
| 			mockLoginUsingLdap(true, ErrInvalidCredentials, sc) | ||||
| 			mockSaveInvalidLoginAttempt(sc) | ||||
| 
 | ||||
| 			err := AuthenticateUser(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should result in", func() { | ||||
| 				So(err, ShouldEqual, ErrInvalidCredentials) | ||||
| 				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue) | ||||
| 				So(sc.grafanaLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.ldapLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeTrue) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		authScenario("When a non-existing grafana user authenticate and valid ldap credentials", func(sc *authScenarioContext) { | ||||
| 			mockLoginAttemptValidation(nil, sc) | ||||
| 			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc) | ||||
| 			mockLoginUsingLdap(true, nil, sc) | ||||
| 			mockSaveInvalidLoginAttempt(sc) | ||||
| 
 | ||||
| 			err := AuthenticateUser(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should result in", func() { | ||||
| 				So(err, ShouldBeNil) | ||||
| 				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue) | ||||
| 				So(sc.grafanaLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.ldapLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		authScenario("When a non-existing grafana user authenticate and ldap returns unexpected error", func(sc *authScenarioContext) { | ||||
| 			customErr := errors.New("custom") | ||||
| 			mockLoginAttemptValidation(nil, sc) | ||||
| 			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc) | ||||
| 			mockLoginUsingLdap(true, customErr, sc) | ||||
| 			mockSaveInvalidLoginAttempt(sc) | ||||
| 
 | ||||
| 			err := AuthenticateUser(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should result in", func() { | ||||
| 				So(err, ShouldEqual, customErr) | ||||
| 				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue) | ||||
| 				So(sc.grafanaLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.ldapLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		authScenario("When grafana user authenticate with invalid credentials and invalid ldap credentials", func(sc *authScenarioContext) { | ||||
| 			mockLoginAttemptValidation(nil, sc) | ||||
| 			mockLoginUsingGrafanaDB(ErrInvalidCredentials, sc) | ||||
| 			mockLoginUsingLdap(true, ErrInvalidCredentials, sc) | ||||
| 			mockSaveInvalidLoginAttempt(sc) | ||||
| 
 | ||||
| 			err := AuthenticateUser(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should result in", func() { | ||||
| 				So(err, ShouldEqual, ErrInvalidCredentials) | ||||
| 				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue) | ||||
| 				So(sc.grafanaLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.ldapLoginWasCalled, ShouldBeTrue) | ||||
| 				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeTrue) | ||||
| 			}) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| type authScenarioContext struct { | ||||
| 	loginUserQuery                   *LoginUserQuery | ||||
| 	grafanaLoginWasCalled            bool | ||||
| 	ldapLoginWasCalled               bool | ||||
| 	loginAttemptValidationWasCalled  bool | ||||
| 	saveInvalidLoginAttemptWasCalled bool | ||||
| } | ||||
| 
 | ||||
| type authScenarioFunc func(sc *authScenarioContext) | ||||
| 
 | ||||
| func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) { | ||||
| 	loginUsingGrafanaDB = func(query *LoginUserQuery) error { | ||||
| 		sc.grafanaLoginWasCalled = true | ||||
| 		return err | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) { | ||||
| 	loginUsingLdap = func(query *LoginUserQuery) (bool, error) { | ||||
| 		sc.ldapLoginWasCalled = true | ||||
| 		return enabled, err | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func mockLoginAttemptValidation(err error, sc *authScenarioContext) { | ||||
| 	validateLoginAttempts = func(username string) error { | ||||
| 		sc.loginAttemptValidationWasCalled = true | ||||
| 		return err | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func mockSaveInvalidLoginAttempt(sc *authScenarioContext) { | ||||
| 	saveInvalidLoginAttempt = func(query *LoginUserQuery) { | ||||
| 		sc.saveInvalidLoginAttemptWasCalled = true | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func authScenario(desc string, fn authScenarioFunc) { | ||||
| 	Convey(desc, func() { | ||||
| 		origLoginUsingGrafanaDB := loginUsingGrafanaDB | ||||
| 		origLoginUsingLdap := loginUsingLdap | ||||
| 		origValidateLoginAttempts := validateLoginAttempts | ||||
| 		origSaveInvalidLoginAttempt := saveInvalidLoginAttempt | ||||
| 
 | ||||
| 		sc := &authScenarioContext{ | ||||
| 			loginUserQuery: &LoginUserQuery{ | ||||
| 				Username:  "user", | ||||
| 				Password:  "pwd", | ||||
| 				IpAddress: "192.168.1.1:56433", | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		defer func() { | ||||
| 			loginUsingGrafanaDB = origLoginUsingGrafanaDB | ||||
| 			loginUsingLdap = origLoginUsingLdap | ||||
| 			validateLoginAttempts = origValidateLoginAttempts | ||||
| 			saveInvalidLoginAttempt = origSaveInvalidLoginAttempt | ||||
| 		}() | ||||
| 
 | ||||
| 		fn(sc) | ||||
| 	}) | ||||
| } | ||||
|  | @ -0,0 +1,48 @@ | |||
| package login | ||||
| 
 | ||||
| import ( | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/bus" | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	maxInvalidLoginAttempts int64         = 5 | ||||
| 	loginAttemptsWindow     time.Duration = time.Minute * 5 | ||||
| ) | ||||
| 
 | ||||
| var validateLoginAttempts = func(username string) error { | ||||
| 	if setting.DisableBruteForceLoginProtection { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	loginAttemptCountQuery := m.GetUserLoginAttemptCountQuery{ | ||||
| 		Username: username, | ||||
| 		Since:    time.Now().Add(-loginAttemptsWindow), | ||||
| 	} | ||||
| 
 | ||||
| 	if err := bus.Dispatch(&loginAttemptCountQuery); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if loginAttemptCountQuery.Result >= maxInvalidLoginAttempts { | ||||
| 		return ErrTooManyLoginAttempts | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| var saveInvalidLoginAttempt = func(query *LoginUserQuery) { | ||||
| 	if setting.DisableBruteForceLoginProtection { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	loginAttemptCommand := m.CreateLoginAttemptCommand{ | ||||
| 		Username:  query.Username, | ||||
| 		IpAddress: query.IpAddress, | ||||
| 	} | ||||
| 
 | ||||
| 	bus.Dispatch(&loginAttemptCommand) | ||||
| } | ||||
|  | @ -0,0 +1,125 @@ | |||
| package login | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/bus" | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| ) | ||||
| 
 | ||||
| func TestLoginAttemptsValidation(t *testing.T) { | ||||
| 	Convey("Validate login attempts", t, func() { | ||||
| 		Convey("Given brute force login protection enabled", func() { | ||||
| 			setting.DisableBruteForceLoginProtection = false | ||||
| 
 | ||||
| 			Convey("When user login attempt count equals max-1 ", func() { | ||||
| 				withLoginAttempts(maxInvalidLoginAttempts - 1) | ||||
| 				err := validateLoginAttempts("user") | ||||
| 
 | ||||
| 				Convey("it should not result in error", func() { | ||||
| 					So(err, ShouldBeNil) | ||||
| 				}) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("When user login attempt count equals max ", func() { | ||||
| 				withLoginAttempts(maxInvalidLoginAttempts) | ||||
| 				err := validateLoginAttempts("user") | ||||
| 
 | ||||
| 				Convey("it should result in too many login attempts error", func() { | ||||
| 					So(err, ShouldEqual, ErrTooManyLoginAttempts) | ||||
| 				}) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("When user login attempt count is greater than max ", func() { | ||||
| 				withLoginAttempts(maxInvalidLoginAttempts + 5) | ||||
| 				err := validateLoginAttempts("user") | ||||
| 
 | ||||
| 				Convey("it should result in too many login attempts error", func() { | ||||
| 					So(err, ShouldEqual, ErrTooManyLoginAttempts) | ||||
| 				}) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("When saving invalid login attempt", func() { | ||||
| 				defer bus.ClearBusHandlers() | ||||
| 				createLoginAttemptCmd := &m.CreateLoginAttemptCommand{} | ||||
| 
 | ||||
| 				bus.AddHandler("test", func(cmd *m.CreateLoginAttemptCommand) error { | ||||
| 					createLoginAttemptCmd = cmd | ||||
| 					return nil | ||||
| 				}) | ||||
| 
 | ||||
| 				saveInvalidLoginAttempt(&LoginUserQuery{ | ||||
| 					Username:  "user", | ||||
| 					Password:  "pwd", | ||||
| 					IpAddress: "192.168.1.1:56433", | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should dispatch command", func() { | ||||
| 					So(createLoginAttemptCmd, ShouldNotBeNil) | ||||
| 					So(createLoginAttemptCmd.Username, ShouldEqual, "user") | ||||
| 					So(createLoginAttemptCmd.IpAddress, ShouldEqual, "192.168.1.1:56433") | ||||
| 				}) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Given brute force login protection disabled", func() { | ||||
| 			setting.DisableBruteForceLoginProtection = true | ||||
| 
 | ||||
| 			Convey("When user login attempt count equals max-1 ", func() { | ||||
| 				withLoginAttempts(maxInvalidLoginAttempts - 1) | ||||
| 				err := validateLoginAttempts("user") | ||||
| 
 | ||||
| 				Convey("it should not result in error", func() { | ||||
| 					So(err, ShouldBeNil) | ||||
| 				}) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("When user login attempt count equals max ", func() { | ||||
| 				withLoginAttempts(maxInvalidLoginAttempts) | ||||
| 				err := validateLoginAttempts("user") | ||||
| 
 | ||||
| 				Convey("it should not result in error", func() { | ||||
| 					So(err, ShouldBeNil) | ||||
| 				}) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("When user login attempt count is greater than max ", func() { | ||||
| 				withLoginAttempts(maxInvalidLoginAttempts + 5) | ||||
| 				err := validateLoginAttempts("user") | ||||
| 
 | ||||
| 				Convey("it should not result in error", func() { | ||||
| 					So(err, ShouldBeNil) | ||||
| 				}) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("When saving invalid login attempt", func() { | ||||
| 				defer bus.ClearBusHandlers() | ||||
| 				createLoginAttemptCmd := (*m.CreateLoginAttemptCommand)(nil) | ||||
| 
 | ||||
| 				bus.AddHandler("test", func(cmd *m.CreateLoginAttemptCommand) error { | ||||
| 					createLoginAttemptCmd = cmd | ||||
| 					return nil | ||||
| 				}) | ||||
| 
 | ||||
| 				saveInvalidLoginAttempt(&LoginUserQuery{ | ||||
| 					Username:  "user", | ||||
| 					Password:  "pwd", | ||||
| 					IpAddress: "192.168.1.1:56433", | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should not dispatch command", func() { | ||||
| 					So(createLoginAttemptCmd, ShouldBeNil) | ||||
| 				}) | ||||
| 			}) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func withLoginAttempts(loginAttempts int64) { | ||||
| 	bus.AddHandler("test", func(query *m.GetUserLoginAttemptCountQuery) error { | ||||
| 		query.Result = loginAttempts | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
|  | @ -0,0 +1,35 @@ | |||
| package login | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/subtle" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/bus" | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/util" | ||||
| ) | ||||
| 
 | ||||
| var validatePassword = func(providedPassword string, userPassword string, userSalt string) error { | ||||
| 	passwordHashed := util.EncodePassword(providedPassword, userSalt) | ||||
| 	if subtle.ConstantTimeCompare([]byte(passwordHashed), []byte(userPassword)) != 1 { | ||||
| 		return ErrInvalidCredentials | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| var loginUsingGrafanaDB = func(query *LoginUserQuery) error { | ||||
| 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username} | ||||
| 
 | ||||
| 	if err := bus.Dispatch(&userQuery); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	user := userQuery.Result | ||||
| 
 | ||||
| 	if err := validatePassword(query.Password, user.Password, user.Salt); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	query.User = user | ||||
| 	return nil | ||||
| } | ||||
|  | @ -0,0 +1,139 @@ | |||
| package login | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/bus" | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| ) | ||||
| 
 | ||||
| func TestGrafanaLogin(t *testing.T) { | ||||
| 	Convey("Login using Grafana DB", t, func() { | ||||
| 		grafanaLoginScenario("When login with non-existing user", func(sc *grafanaLoginScenarioContext) { | ||||
| 			sc.withNonExistingUser() | ||||
| 			err := loginUsingGrafanaDB(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should result in user not found error", func() { | ||||
| 				So(err, ShouldEqual, m.ErrUserNotFound) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("it should not call password validation", func() { | ||||
| 				So(sc.validatePasswordCalled, ShouldBeFalse) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("it should not pupulate user object", func() { | ||||
| 				So(sc.loginUserQuery.User, ShouldBeNil) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		grafanaLoginScenario("When login with invalid credentials", func(sc *grafanaLoginScenarioContext) { | ||||
| 			sc.withInvalidPassword() | ||||
| 			err := loginUsingGrafanaDB(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should result in invalid credentials error", func() { | ||||
| 				So(err, ShouldEqual, ErrInvalidCredentials) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("it should call password validation", func() { | ||||
| 				So(sc.validatePasswordCalled, ShouldBeTrue) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("it should not pupulate user object", func() { | ||||
| 				So(sc.loginUserQuery.User, ShouldBeNil) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		grafanaLoginScenario("When login with valid credentials", func(sc *grafanaLoginScenarioContext) { | ||||
| 			sc.withValidCredentials() | ||||
| 			err := loginUsingGrafanaDB(sc.loginUserQuery) | ||||
| 
 | ||||
| 			Convey("it should not result in error", func() { | ||||
| 				So(err, ShouldBeNil) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("it should call password validation", func() { | ||||
| 				So(sc.validatePasswordCalled, ShouldBeTrue) | ||||
| 			}) | ||||
| 
 | ||||
| 			Convey("it should pupulate user object", func() { | ||||
| 				So(sc.loginUserQuery.User, ShouldNotBeNil) | ||||
| 				So(sc.loginUserQuery.User.Login, ShouldEqual, sc.loginUserQuery.Username) | ||||
| 				So(sc.loginUserQuery.User.Password, ShouldEqual, sc.loginUserQuery.Password) | ||||
| 			}) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| type grafanaLoginScenarioContext struct { | ||||
| 	loginUserQuery         *LoginUserQuery | ||||
| 	validatePasswordCalled bool | ||||
| } | ||||
| 
 | ||||
| type grafanaLoginScenarioFunc func(c *grafanaLoginScenarioContext) | ||||
| 
 | ||||
| func grafanaLoginScenario(desc string, fn grafanaLoginScenarioFunc) { | ||||
| 	Convey(desc, func() { | ||||
| 		origValidatePassword := validatePassword | ||||
| 
 | ||||
| 		sc := &grafanaLoginScenarioContext{ | ||||
| 			loginUserQuery: &LoginUserQuery{ | ||||
| 				Username:  "user", | ||||
| 				Password:  "pwd", | ||||
| 				IpAddress: "192.168.1.1:56433", | ||||
| 			}, | ||||
| 			validatePasswordCalled: false, | ||||
| 		} | ||||
| 
 | ||||
| 		defer func() { | ||||
| 			validatePassword = origValidatePassword | ||||
| 		}() | ||||
| 
 | ||||
| 		fn(sc) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func mockPasswordValidation(valid bool, sc *grafanaLoginScenarioContext) { | ||||
| 	validatePassword = func(providedPassword string, userPassword string, userSalt string) error { | ||||
| 		sc.validatePasswordCalled = true | ||||
| 
 | ||||
| 		if !valid { | ||||
| 			return ErrInvalidCredentials | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (sc *grafanaLoginScenarioContext) getUserByLoginQueryReturns(user *m.User) { | ||||
| 	bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error { | ||||
| 		if user == nil { | ||||
| 			return m.ErrUserNotFound | ||||
| 		} | ||||
| 
 | ||||
| 		query.Result = user | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (sc *grafanaLoginScenarioContext) withValidCredentials() { | ||||
| 	sc.getUserByLoginQueryReturns(&m.User{ | ||||
| 		Id:       1, | ||||
| 		Login:    sc.loginUserQuery.Username, | ||||
| 		Password: sc.loginUserQuery.Password, | ||||
| 		Salt:     "salt", | ||||
| 	}) | ||||
| 	mockPasswordValidation(true, sc) | ||||
| } | ||||
| 
 | ||||
| func (sc *grafanaLoginScenarioContext) withNonExistingUser() { | ||||
| 	sc.getUserByLoginQueryReturns(nil) | ||||
| } | ||||
| 
 | ||||
| func (sc *grafanaLoginScenarioContext) withInvalidPassword() { | ||||
| 	sc.getUserByLoginQueryReturns(&m.User{ | ||||
| 		Password: sc.loginUserQuery.Password, | ||||
| 		Salt:     "salt", | ||||
| 	}) | ||||
| 	mockPasswordValidation(false, sc) | ||||
| } | ||||
|  | @ -0,0 +1,21 @@ | |||
| package login | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| ) | ||||
| 
 | ||||
| var loginUsingLdap = func(query *LoginUserQuery) (bool, error) { | ||||
| 	if !setting.LdapEnabled { | ||||
| 		return false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	for _, server := range LdapCfg.Servers { | ||||
| 		author := NewLdapAuthenticator(server) | ||||
| 		err := author.Login(query) | ||||
| 		if err == nil || err != ErrInvalidCredentials { | ||||
| 			return true, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return true, ErrInvalidCredentials | ||||
| } | ||||
|  | @ -0,0 +1,172 @@ | |||
| package login | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| ) | ||||
| 
 | ||||
| func TestLdapLogin(t *testing.T) { | ||||
| 	Convey("Login using ldap", t, func() { | ||||
| 		Convey("Given ldap enabled and a server configured", func() { | ||||
| 			setting.LdapEnabled = true | ||||
| 			LdapCfg.Servers = append(LdapCfg.Servers, | ||||
| 				&LdapServerConf{ | ||||
| 					Host: "", | ||||
| 				}) | ||||
| 
 | ||||
| 			ldapLoginScenario("When login with invalid credentials", func(sc *ldapLoginScenarioContext) { | ||||
| 				sc.withLoginResult(false) | ||||
| 				enabled, err := loginUsingLdap(sc.loginUserQuery) | ||||
| 
 | ||||
| 				Convey("it should return true", func() { | ||||
| 					So(enabled, ShouldBeTrue) | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should return invalid credentials error", func() { | ||||
| 					So(err, ShouldEqual, ErrInvalidCredentials) | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should call ldap login", func() { | ||||
| 					So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeTrue) | ||||
| 				}) | ||||
| 			}) | ||||
| 
 | ||||
| 			ldapLoginScenario("When login with valid credentials", func(sc *ldapLoginScenarioContext) { | ||||
| 				sc.withLoginResult(true) | ||||
| 				enabled, err := loginUsingLdap(sc.loginUserQuery) | ||||
| 
 | ||||
| 				Convey("it should return true", func() { | ||||
| 					So(enabled, ShouldBeTrue) | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should not return error", func() { | ||||
| 					So(err, ShouldBeNil) | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should call ldap login", func() { | ||||
| 					So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeTrue) | ||||
| 				}) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Given ldap enabled and no server configured", func() { | ||||
| 			setting.LdapEnabled = true | ||||
| 			LdapCfg.Servers = make([]*LdapServerConf, 0) | ||||
| 
 | ||||
| 			ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) { | ||||
| 				sc.withLoginResult(true) | ||||
| 				enabled, err := loginUsingLdap(sc.loginUserQuery) | ||||
| 
 | ||||
| 				Convey("it should return true", func() { | ||||
| 					So(enabled, ShouldBeTrue) | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should return invalid credentials error", func() { | ||||
| 					So(err, ShouldEqual, ErrInvalidCredentials) | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should not call ldap login", func() { | ||||
| 					So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeFalse) | ||||
| 				}) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Given ldap disabled", func() { | ||||
| 			setting.LdapEnabled = false | ||||
| 
 | ||||
| 			ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) { | ||||
| 				sc.withLoginResult(false) | ||||
| 				enabled, err := loginUsingLdap(&LoginUserQuery{ | ||||
| 					Username: "user", | ||||
| 					Password: "pwd", | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should return false", func() { | ||||
| 					So(enabled, ShouldBeFalse) | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should not return error", func() { | ||||
| 					So(err, ShouldBeNil) | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("it should not call ldap login", func() { | ||||
| 					So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeFalse) | ||||
| 				}) | ||||
| 			}) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func mockLdapAuthenticator(valid bool) *mockLdapAuther { | ||||
| 	mock := &mockLdapAuther{ | ||||
| 		validLogin: valid, | ||||
| 	} | ||||
| 
 | ||||
| 	NewLdapAuthenticator = func(server *LdapServerConf) ILdapAuther { | ||||
| 		return mock | ||||
| 	} | ||||
| 
 | ||||
| 	return mock | ||||
| } | ||||
| 
 | ||||
| type mockLdapAuther struct { | ||||
| 	validLogin  bool | ||||
| 	loginCalled bool | ||||
| } | ||||
| 
 | ||||
| func (a *mockLdapAuther) Login(query *LoginUserQuery) error { | ||||
| 	a.loginCalled = true | ||||
| 
 | ||||
| 	if !a.validLogin { | ||||
| 		return ErrInvalidCredentials | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (a *mockLdapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (a *mockLdapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) { | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| func (a *mockLdapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type ldapLoginScenarioContext struct { | ||||
| 	loginUserQuery        *LoginUserQuery | ||||
| 	ldapAuthenticatorMock *mockLdapAuther | ||||
| } | ||||
| 
 | ||||
| type ldapLoginScenarioFunc func(c *ldapLoginScenarioContext) | ||||
| 
 | ||||
| func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) { | ||||
| 	Convey(desc, func() { | ||||
| 		origNewLdapAuthenticator := NewLdapAuthenticator | ||||
| 
 | ||||
| 		sc := &ldapLoginScenarioContext{ | ||||
| 			loginUserQuery: &LoginUserQuery{ | ||||
| 				Username:  "user", | ||||
| 				Password:  "pwd", | ||||
| 				IpAddress: "192.168.1.1:56433", | ||||
| 			}, | ||||
| 			ldapAuthenticatorMock: &mockLdapAuther{}, | ||||
| 		} | ||||
| 
 | ||||
| 		defer func() { | ||||
| 			NewLdapAuthenticator = origNewLdapAuthenticator | ||||
| 		}() | ||||
| 
 | ||||
| 		fn(sc) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (sc *ldapLoginScenarioContext) withLoginResult(valid bool) { | ||||
| 	sc.ldapAuthenticatorMock = mockLdapAuthenticator(valid) | ||||
| } | ||||
|  | @ -0,0 +1,36 @@ | |||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type LoginAttempt struct { | ||||
| 	Id        int64 | ||||
| 	Username  string | ||||
| 	IpAddress string | ||||
| 	Created   time.Time | ||||
| } | ||||
| 
 | ||||
| // ---------------------
 | ||||
| // 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 | ||||
| } | ||||
|  | @ -46,6 +46,7 @@ func (service *CleanUpService) start(ctx context.Context) error { | |||
| 			service.cleanUpTmpFiles() | ||||
| 			service.deleteExpiredSnapshots() | ||||
| 			service.deleteExpiredDashboardVersions() | ||||
| 			service.deleteOldLoginAttempts() | ||||
| 		case <-ctx.Done(): | ||||
| 			return ctx.Err() | ||||
| 		} | ||||
|  | @ -88,3 +89,18 @@ func (service *CleanUpService) deleteExpiredSnapshots() { | |||
| func (service *CleanUpService) deleteExpiredDashboardVersions() { | ||||
| 	bus.Dispatch(&m.DeleteExpiredVersionsCommand{}) | ||||
| } | ||||
| 
 | ||||
| func (service *CleanUpService) deleteOldLoginAttempts() { | ||||
| 	if setting.DisableBruteForceLoginProtection { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	cmd := m.DeleteOldLoginAttemptsCommand{ | ||||
| 		OlderThan: time.Now().Add(time.Minute * -10), | ||||
| 	} | ||||
| 	if err := bus.Dispatch(&cmd); err != nil { | ||||
| 		service.log.Error("Problem deleting expired login attempts", "error", err.Error()) | ||||
| 	} else { | ||||
| 		service.log.Debug("Deleted expired login attempts", "rows affected", cmd.DeletedRows) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,91 @@ | |||
| package sqlstore | ||||
| 
 | ||||
| import ( | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/bus" | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| ) | ||||
| 
 | ||||
| var getTimeNow = time.Now | ||||
| 
 | ||||
| func init() { | ||||
| 	bus.AddHandler("sql", CreateLoginAttempt) | ||||
| 	bus.AddHandler("sql", DeleteOldLoginAttempts) | ||||
| 	bus.AddHandler("sql", GetUserLoginAttemptCount) | ||||
| } | ||||
| 
 | ||||
| func CreateLoginAttempt(cmd *m.CreateLoginAttemptCommand) error { | ||||
| 	return inTransaction(func(sess *DBSession) error { | ||||
| 		loginAttempt := m.LoginAttempt{ | ||||
| 			Username:  cmd.Username, | ||||
| 			IpAddress: cmd.IpAddress, | ||||
| 			Created:   getTimeNow(), | ||||
| 		} | ||||
| 
 | ||||
| 		if _, err := sess.Insert(&loginAttempt); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		cmd.Result = loginAttempt | ||||
| 
 | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error { | ||||
| 	return inTransaction(func(sess *DBSession) error { | ||||
| 		var maxId int64 | ||||
| 		sql := "SELECT max(id) as id FROM login_attempt WHERE created < " + dialect.DateTimeFunc("?") | ||||
| 		result, err := sess.Query(sql, cmd.OlderThan) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		maxId = toInt64(result[0]["id"]) | ||||
| 
 | ||||
| 		if maxId == 0 { | ||||
| 			return nil | ||||
| 		} | ||||
| 
 | ||||
| 		sql = "DELETE FROM login_attempt WHERE id <= ?" | ||||
| 
 | ||||
| 		if result, err := sess.Exec(sql, maxId); err != nil { | ||||
| 			return err | ||||
| 		} else if cmd.DeletedRows, err = result.RowsAffected(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func GetUserLoginAttemptCount(query *m.GetUserLoginAttemptCountQuery) error { | ||||
| 	loginAttempt := new(m.LoginAttempt) | ||||
| 	total, err := x. | ||||
| 		Where("username = ?", query.Username). | ||||
| 		And("created >="+dialect.DateTimeFunc("?"), query.Since). | ||||
| 		Count(loginAttempt) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	query.Result = total | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func toInt64(i interface{}) int64 { | ||||
| 	switch i.(type) { | ||||
| 	case []byte: | ||||
| 		n, _ := strconv.ParseInt(string(i.([]byte)), 10, 64) | ||||
| 		return n | ||||
| 	case int: | ||||
| 		return int64(i.(int)) | ||||
| 	case int64: | ||||
| 		return i.(int64) | ||||
| 	} | ||||
| 	return 0 | ||||
| } | ||||
|  | @ -0,0 +1,125 @@ | |||
| package sqlstore | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| ) | ||||
| 
 | ||||
| func mockTime(mock time.Time) time.Time { | ||||
| 	getTimeNow = func() time.Time { return mock } | ||||
| 	return mock | ||||
| } | ||||
| 
 | ||||
| func TestLoginAttempts(t *testing.T) { | ||||
| 	Convey("Testing Login Attempts DB Access", t, func() { | ||||
| 		InitTestDB(t) | ||||
| 
 | ||||
| 		user := "user" | ||||
| 		beginningOfTime := mockTime(time.Date(2017, 10, 22, 8, 0, 0, 0, time.Local)) | ||||
| 
 | ||||
| 		err := CreateLoginAttempt(&m.CreateLoginAttemptCommand{ | ||||
| 			Username:  user, | ||||
| 			IpAddress: "192.168.0.1", | ||||
| 		}) | ||||
| 		So(err, ShouldBeNil) | ||||
| 
 | ||||
| 		timePlusOneMinute := mockTime(beginningOfTime.Add(time.Minute * 1)) | ||||
| 
 | ||||
| 		err = CreateLoginAttempt(&m.CreateLoginAttemptCommand{ | ||||
| 			Username:  user, | ||||
| 			IpAddress: "192.168.0.1", | ||||
| 		}) | ||||
| 		So(err, ShouldBeNil) | ||||
| 
 | ||||
| 		timePlusTwoMinutes := mockTime(beginningOfTime.Add(time.Minute * 2)) | ||||
| 
 | ||||
| 		err = CreateLoginAttempt(&m.CreateLoginAttemptCommand{ | ||||
| 			Username:  user, | ||||
| 			IpAddress: "192.168.0.1", | ||||
| 		}) | ||||
| 		So(err, ShouldBeNil) | ||||
| 
 | ||||
| 		Convey("Should return a total count of zero login attempts when comparing since beginning of time + 2min and 1s", func() { | ||||
| 			query := m.GetUserLoginAttemptCountQuery{ | ||||
| 				Username: user, | ||||
| 				Since:    timePlusTwoMinutes.Add(time.Second * 1), | ||||
| 			} | ||||
| 			err := GetUserLoginAttemptCount(&query) | ||||
| 			So(err, ShouldBeNil) | ||||
| 			So(query.Result, ShouldEqual, 0) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Should return the total count of login attempts since beginning of time", func() { | ||||
| 			query := m.GetUserLoginAttemptCountQuery{ | ||||
| 				Username: user, | ||||
| 				Since:    beginningOfTime, | ||||
| 			} | ||||
| 			err := GetUserLoginAttemptCount(&query) | ||||
| 			So(err, ShouldBeNil) | ||||
| 			So(query.Result, ShouldEqual, 3) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Should return the total count of login attempts since beginning of time + 1min", func() { | ||||
| 			query := m.GetUserLoginAttemptCountQuery{ | ||||
| 				Username: user, | ||||
| 				Since:    timePlusOneMinute, | ||||
| 			} | ||||
| 			err := GetUserLoginAttemptCount(&query) | ||||
| 			So(err, ShouldBeNil) | ||||
| 			So(query.Result, ShouldEqual, 2) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Should return the total count of login attempts since beginning of time + 2min", func() { | ||||
| 			query := m.GetUserLoginAttemptCountQuery{ | ||||
| 				Username: user, | ||||
| 				Since:    timePlusTwoMinutes, | ||||
| 			} | ||||
| 			err := GetUserLoginAttemptCount(&query) | ||||
| 			So(err, ShouldBeNil) | ||||
| 			So(query.Result, ShouldEqual, 1) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Should return deleted rows older than beginning of time", func() { | ||||
| 			cmd := m.DeleteOldLoginAttemptsCommand{ | ||||
| 				OlderThan: beginningOfTime, | ||||
| 			} | ||||
| 			err := DeleteOldLoginAttempts(&cmd) | ||||
| 
 | ||||
| 			So(err, ShouldBeNil) | ||||
| 			So(cmd.DeletedRows, ShouldEqual, 0) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Should return deleted rows older than beginning of time + 1min", func() { | ||||
| 			cmd := m.DeleteOldLoginAttemptsCommand{ | ||||
| 				OlderThan: timePlusOneMinute, | ||||
| 			} | ||||
| 			err := DeleteOldLoginAttempts(&cmd) | ||||
| 
 | ||||
| 			So(err, ShouldBeNil) | ||||
| 			So(cmd.DeletedRows, ShouldEqual, 1) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Should return deleted rows older than beginning of time + 2min", func() { | ||||
| 			cmd := m.DeleteOldLoginAttemptsCommand{ | ||||
| 				OlderThan: timePlusTwoMinutes, | ||||
| 			} | ||||
| 			err := DeleteOldLoginAttempts(&cmd) | ||||
| 
 | ||||
| 			So(err, ShouldBeNil) | ||||
| 			So(cmd.DeletedRows, ShouldEqual, 2) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Should return deleted rows older than beginning of time + 2min and 1s", func() { | ||||
| 			cmd := m.DeleteOldLoginAttemptsCommand{ | ||||
| 				OlderThan: timePlusTwoMinutes.Add(time.Second * 1), | ||||
| 			} | ||||
| 			err := DeleteOldLoginAttempts(&cmd) | ||||
| 
 | ||||
| 			So(err, ShouldBeNil) | ||||
| 			So(cmd.DeletedRows, ShouldEqual, 3) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
|  | @ -0,0 +1,23 @@ | |||
| package migrations | ||||
| 
 | ||||
| import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" | ||||
| 
 | ||||
| func addLoginAttemptMigrations(mg *Migrator) { | ||||
| 	loginAttemptV1 := Table{ | ||||
| 		Name: "login_attempt", | ||||
| 		Columns: []*Column{ | ||||
| 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, | ||||
| 			{Name: "username", Type: DB_NVarchar, Length: 190, Nullable: false}, | ||||
| 			{Name: "ip_address", Type: DB_NVarchar, Length: 30, Nullable: false}, | ||||
| 			{Name: "created", Type: DB_DateTime, Nullable: false}, | ||||
| 		}, | ||||
| 		Indices: []*Index{ | ||||
| 			{Cols: []string{"username"}}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// create table
 | ||||
| 	mg.AddMigration("create login attempt table", NewAddTableMigration(loginAttemptV1)) | ||||
| 	// add indices
 | ||||
| 	mg.AddMigration("add index login_attempt.username", NewAddIndexMigration(loginAttemptV1, loginAttemptV1.Indices[0])) | ||||
| } | ||||
|  | @ -29,6 +29,7 @@ func AddMigrations(mg *Migrator) { | |||
| 	addTeamMigrations(mg) | ||||
| 	addDashboardAclMigrations(mg) | ||||
| 	addTagMigration(mg) | ||||
| 	addLoginAttemptMigrations(mg) | ||||
| } | ||||
| 
 | ||||
| func addMigrationLogMigrations(mg *Migrator) { | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ type Dialect interface { | |||
| 	LikeStr() string | ||||
| 	Default(col *Column) string | ||||
| 	BooleanStr(bool) string | ||||
| 	DateTimeFunc(string) string | ||||
| 
 | ||||
| 	CreateIndexSql(tableName string, index *Index) string | ||||
| 	CreateTableSql(table *Table) string | ||||
|  | @ -78,6 +79,10 @@ func (b *BaseDialect) Default(col *Column) string { | |||
| 	return col.Default | ||||
| } | ||||
| 
 | ||||
| func (db *BaseDialect) DateTimeFunc(value string) string { | ||||
| 	return value | ||||
| } | ||||
| 
 | ||||
| func (b *BaseDialect) CreateTableSql(table *Table) string { | ||||
| 	var sql string | ||||
| 	sql = "CREATE TABLE IF NOT EXISTS " | ||||
|  |  | |||
|  | @ -36,6 +36,10 @@ func (db *Sqlite3) BooleanStr(value bool) string { | |||
| 	return "0" | ||||
| } | ||||
| 
 | ||||
| func (db *Sqlite3) DateTimeFunc(value string) string { | ||||
| 	return "datetime(" + value + ")" | ||||
| } | ||||
| 
 | ||||
| func (db *Sqlite3) SqlType(c *Column) string { | ||||
| 	switch c.Type { | ||||
| 	case DB_Date, DB_DateTime, DB_TimeStamp, DB_Time: | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ type TestDB struct { | |||
| } | ||||
| 
 | ||||
| var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:?_loc=Local"} | ||||
| var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"} | ||||
| var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci&loc=Local"} | ||||
| var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"} | ||||
| 
 | ||||
| func CleanDB(x *xorm.Engine) { | ||||
|  |  | |||
|  | @ -82,6 +82,7 @@ var ( | |||
| 	DisableGravatar                  bool | ||||
| 	EmailCodeValidMinutes            int | ||||
| 	DataProxyWhiteList               map[string]bool | ||||
| 	DisableBruteForceLoginProtection bool | ||||
| 
 | ||||
| 	// Snapshots
 | ||||
| 	ExternalSnapshotUrl   string | ||||
|  | @ -514,6 +515,7 @@ func NewConfigContext(args *CommandLineArgs) error { | |||
| 	CookieUserName = security.Key("cookie_username").String() | ||||
| 	CookieRememberName = security.Key("cookie_remember_name").String() | ||||
| 	DisableGravatar = security.Key("disable_gravatar").MustBool(true) | ||||
| 	DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false) | ||||
| 
 | ||||
| 	// read snapshots settings
 | ||||
| 	snapshots := Cfg.Section("snapshots") | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue