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:
committed by
Torkel Ödegaard
parent
475febd004
commit
3d1c624c12
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user