mirror of https://github.com/grafana/grafana
commit
0fb05bcf59
@ -1,3 +1,3 @@ |
||||
FROM prom/prometheus |
||||
FROM prom/prometheus:v1.8.2 |
||||
ADD prometheus.yml /etc/prometheus/ |
||||
ADD alert.rules /etc/prometheus/ |
||||
|
@ -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 |
||||
} |
@ -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])) |
||||
} |
@ -0,0 +1,47 @@ |
||||
import gfunc from '../gfunc'; |
||||
import GraphiteQuery from '../graphite_query'; |
||||
|
||||
describe('Graphite query model', () => { |
||||
let ctx: any = { |
||||
datasource: { |
||||
getFuncDef: gfunc.getFuncDef, |
||||
getFuncDefs: jest.fn().mockReturnValue(Promise.resolve(gfunc.getFuncDefs('1.0'))), |
||||
waitForFuncDefsLoaded: jest.fn().mockReturnValue(Promise.resolve(null)), |
||||
createFuncInstance: gfunc.createFuncInstance, |
||||
}, |
||||
templateSrv: {}, |
||||
targets: [], |
||||
}; |
||||
|
||||
beforeEach(() => { |
||||
ctx.target = { refId: 'A', target: 'scaleToSeconds(#A, 60)' }; |
||||
ctx.queryModel = new GraphiteQuery(ctx.datasource, ctx.target, ctx.templateSrv); |
||||
}); |
||||
|
||||
describe('when updating targets with nested queries', () => { |
||||
beforeEach(() => { |
||||
ctx.target = { refId: 'D', target: 'asPercent(#A, #C)' }; |
||||
ctx.targets = [ |
||||
{ refId: 'A', target: 'first.query.count' }, |
||||
{ refId: 'B', target: 'second.query.count' }, |
||||
{ refId: 'C', target: 'diffSeries(#A, #B)' }, |
||||
{ refId: 'D', target: 'asPercent(#A, #C)' }, |
||||
]; |
||||
ctx.queryModel = new GraphiteQuery(ctx.datasource, ctx.target, ctx.templateSrv); |
||||
}); |
||||
|
||||
it('targetFull should include nested queries', () => { |
||||
ctx.queryModel.updateRenderedTarget(ctx.target, ctx.targets); |
||||
const targetFullExpected = 'asPercent(first.query.count, diffSeries(first.query.count, second.query.count))'; |
||||
expect(ctx.queryModel.target.targetFull).toBe(targetFullExpected); |
||||
}); |
||||
|
||||
it('should not hang on circular references', () => { |
||||
ctx.target.target = 'asPercent(#A, #B)'; |
||||
ctx.targets = [{ refId: 'A', target: 'asPercent(#B, #C)' }, { refId: 'B', target: 'asPercent(#A, #C)' }]; |
||||
ctx.queryModel.updateRenderedTarget(ctx.target, ctx.targets); |
||||
// Just ensure updateRenderedTarget() is completed and doesn't hang
|
||||
expect(ctx.queryModel.target.targetFull).toBeDefined(); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,86 @@ |
||||
(function() { |
||||
'use strict'; |
||||
|
||||
var page = require('webpage').create(); |
||||
var args = require('system').args; |
||||
var params = {}; |
||||
var regexp = /^([^=]+)=([^$]+)/; |
||||
|
||||
args.forEach(function(arg) { |
||||
var parts = arg.match(regexp); |
||||
if (!parts) { return; } |
||||
params[parts[1]] = parts[2]; |
||||
}); |
||||
|
||||
var usage = "url=<url> png=<filename> width=<width> height=<height> renderKey=<key>"; |
||||
|
||||
if (!params.url || !params.png || !params.renderKey || !params.domain) { |
||||
console.log(usage); |
||||
phantom.exit(); |
||||
} |
||||
|
||||
phantom.addCookie({ |
||||
'name': 'renderKey', |
||||
'value': params.renderKey, |
||||
'domain': params.domain, |
||||
}); |
||||
|
||||
page.viewportSize = { |
||||
width: params.width || '800', |
||||
height: params.height || '400' |
||||
}; |
||||
|
||||
var timeoutMs = (parseInt(params.timeout) || 10) * 1000; |
||||
var waitBetweenReadyCheckMs = 50; |
||||
var totalWaitMs = 0; |
||||
|
||||
page.open(params.url, function (status) { |
||||
console.log('Loading a web page: ' + params.url + ' status: ' + status, timeoutMs); |
||||
|
||||
page.onError = function(msg, trace) { |
||||
var msgStack = ['ERROR: ' + msg]; |
||||
if (trace && trace.length) { |
||||
msgStack.push('TRACE:'); |
||||
trace.forEach(function(t) { |
||||
msgStack.push(' -> ' + t.file + ': ' + t.line + (t.function ? ' (in function "' + t.function +'")' : '')); |
||||
}); |
||||
} |
||||
console.error(msgStack.join('\n')); |
||||
}; |
||||
|
||||
function checkIsReady() { |
||||
var panelsRendered = page.evaluate(function() { |
||||
if (!window.angular) { return false; } |
||||
var body = window.angular.element(document.body); |
||||
if (!body.injector) { return false; } |
||||
if (!body.injector()) { return false; } |
||||
|
||||
var rootScope = body.injector().get('$rootScope'); |
||||
if (!rootScope) {return false;} |
||||
var panels = angular.element('div.panel:visible').length; |
||||
return rootScope.panelsRendered >= panels; |
||||
}); |
||||
|
||||
if (panelsRendered || totalWaitMs > timeoutMs) { |
||||
var bb = page.evaluate(function () { |
||||
return document.getElementsByClassName("main-view")[0].getBoundingClientRect(); |
||||
}); |
||||
|
||||
page.clipRect = { |
||||
top: bb.top, |
||||
left: bb.left, |
||||
width: bb.width, |
||||
height: bb.height |
||||
}; |
||||
|
||||
page.render(params.png); |
||||
phantom.exit(); |
||||
} else { |
||||
totalWaitMs += waitBetweenReadyCheckMs; |
||||
setTimeout(checkIsReady, waitBetweenReadyCheckMs); |
||||
} |
||||
} |
||||
|
||||
setTimeout(checkIsReady, waitBetweenReadyCheckMs); |
||||
}); |
||||
})(); |
Loading…
Reference in new issue