Merge pull request #3161 from influxdb/auth-cache-3102
fix #3102: add authentication cachepull/3201/head
commit
ca417e19f6
|
@ -55,6 +55,7 @@
|
|||
- [#2608](https://github.com/influxdb/influxdb/issues/2608): drop measurement while writing points to that measurement has race condition that can panic
|
||||
- [#3183](https://github.com/influxdb/influxdb/issues/3183): using line protocol measurement names cannot contain commas
|
||||
- [#3193](https://github.com/influxdb/influxdb/pull/3193): Fix panic for SHOW STATS and in collectd
|
||||
- [#3102](https://github.com/influxdb/influxdb/issues/3102): Add authentication cache
|
||||
|
||||
## v0.9.0 [2015-06-11]
|
||||
|
||||
|
|
|
@ -92,6 +92,9 @@ func (*DropRetentionPolicyStatement) node() {}
|
|||
func (*DropSeriesStatement) node() {}
|
||||
func (*DropUserStatement) node() {}
|
||||
func (*GrantStatement) node() {}
|
||||
func (*RevokeStatement) node() {}
|
||||
func (*SelectStatement) node() {}
|
||||
func (*SetPasswordUserStatement) node() {}
|
||||
func (*ShowContinuousQueriesStatement) node() {}
|
||||
func (*ShowGrantsForUserStatement) node() {}
|
||||
func (*ShowServersStatement) node() {}
|
||||
|
@ -105,9 +108,6 @@ func (*ShowDiagnosticsStatement) node() {}
|
|||
func (*ShowTagKeysStatement) node() {}
|
||||
func (*ShowTagValuesStatement) node() {}
|
||||
func (*ShowUsersStatement) node() {}
|
||||
func (*RevokeStatement) node() {}
|
||||
func (*SelectStatement) node() {}
|
||||
func (*SetPasswordUserStatement) node() {}
|
||||
|
||||
func (*BinaryExpr) node() {}
|
||||
func (*BooleanLiteral) node() {}
|
||||
|
|
|
@ -94,7 +94,7 @@ func (p *Parser) ParseStatement() (Statement, error) {
|
|||
case ALTER:
|
||||
return p.parseAlterStatement()
|
||||
case SET:
|
||||
return p.parseSetStatement()
|
||||
return p.parseSetPasswordUserStatement()
|
||||
default:
|
||||
return nil, newParseError(tokstr(tok, lit), []string{"SELECT", "DELETE", "SHOW", "CREATE", "DROP", "GRANT", "REVOKE", "ALTER", "SET"}, pos)
|
||||
}
|
||||
|
@ -207,19 +207,14 @@ func (p *Parser) parseAlterStatement() (Statement, error) {
|
|||
return nil, newParseError(tokstr(tok, lit), []string{"RETENTION"}, pos)
|
||||
}
|
||||
|
||||
// parseSetStatement parses a string and returns a set statement.
|
||||
// parseSetPasswordUserStatement parses a string and returns a set statement.
|
||||
// This function assumes the SET token has already been consumed.
|
||||
func (p *Parser) parseSetStatement() (*SetPasswordUserStatement, error) {
|
||||
func (p *Parser) parseSetPasswordUserStatement() (*SetPasswordUserStatement, error) {
|
||||
stmt := &SetPasswordUserStatement{}
|
||||
|
||||
// Consume the required PASSWORD token.
|
||||
if tok, pos, lit := p.scanIgnoreWhitespace(); tok != PASSWORD {
|
||||
return nil, newParseError(tokstr(tok, lit), []string{"PASSWORD"}, pos)
|
||||
}
|
||||
|
||||
// Consume the required FOR token.
|
||||
if tok, pos, lit := p.scanIgnoreWhitespace(); tok != FOR {
|
||||
return nil, newParseError(tokstr(tok, lit), []string{"FOR"}, pos)
|
||||
// Consume the required PASSWORD FOR tokens.
|
||||
if err := p.parseTokens([]Token{PASSWORD, FOR}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse username
|
||||
|
|
|
@ -96,6 +96,13 @@ type Store struct {
|
|||
// The amount of time without an apply before sending a heartbeat.
|
||||
CommitTimeout time.Duration
|
||||
|
||||
// Authentication cache.
|
||||
authCache map[string]string
|
||||
|
||||
// hashPassword generates a cryptographically secure hash for password.
|
||||
// Returns an error if the password is invalid or a hash cannot be generated.
|
||||
hashPassword HashPasswordFn
|
||||
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
|
@ -116,7 +123,11 @@ func NewStore(c Config) *Store {
|
|||
ElectionTimeout: time.Duration(c.ElectionTimeout),
|
||||
LeaderLeaseTimeout: time.Duration(c.LeaderLeaseTimeout),
|
||||
CommitTimeout: time.Duration(c.CommitTimeout),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
authCache: make(map[string]string, 0),
|
||||
hashPassword: func(password string) ([]byte, error) {
|
||||
return bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
|
||||
},
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -985,20 +996,38 @@ func (s *Store) AdminUserExists() (exists bool, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// ErrAuthenticate is returned when authentication fails.
|
||||
var ErrAuthenticate = errors.New("authentication failed")
|
||||
|
||||
// Authenticate retrieves a user with a matching username and password.
|
||||
func (s *Store) Authenticate(username, password string) (ui *UserInfo, err error) {
|
||||
err = s.read(func(data *Data) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Find user.
|
||||
u := data.User(username)
|
||||
if u == nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
// Check the local auth cache first.
|
||||
if p, ok := s.authCache[username]; ok {
|
||||
if p == password {
|
||||
ui = u
|
||||
return nil
|
||||
} else {
|
||||
return ErrAuthenticate
|
||||
}
|
||||
}
|
||||
|
||||
// Compare password with user hash.
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(password)); err != nil {
|
||||
return err
|
||||
return ErrAuthenticate
|
||||
}
|
||||
|
||||
s.authCache[username] = password
|
||||
|
||||
ui = u
|
||||
return nil
|
||||
})
|
||||
|
@ -1008,7 +1037,7 @@ func (s *Store) Authenticate(username, password string) (ui *UserInfo, err error
|
|||
// CreateUser creates a new user in the store.
|
||||
func (s *Store) CreateUser(name, password string, admin bool) (*UserInfo, error) {
|
||||
// Hash the password before serializing it.
|
||||
hash, err := HashPassword(password)
|
||||
hash, err := s.hashPassword(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1038,7 +1067,7 @@ func (s *Store) DropUser(name string) error {
|
|||
// UpdateUser updates an existing user in the store.
|
||||
func (s *Store) UpdateUser(name, password string) error {
|
||||
// Hash the password before serializing it.
|
||||
hash, err := HashPassword(password)
|
||||
hash, err := s.hashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1301,6 +1330,27 @@ func (s *Store) sync(index uint64, timeout time.Duration) error {
|
|||
}
|
||||
}
|
||||
|
||||
// BcryptCost is the cost associated with generating password with Bcrypt.
|
||||
// This setting is lowered during testing to improve test suite performance.
|
||||
var BcryptCost = 10
|
||||
|
||||
// HashPasswordFn represnets a password hashing function.
|
||||
type HashPasswordFn func(password string) ([]byte, error)
|
||||
|
||||
// GetHashPasswordFn returns the current password hashing function.
|
||||
func (s *Store) GetHashPasswordFn() HashPasswordFn {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.hashPassword
|
||||
}
|
||||
|
||||
// SetHashPasswordFn sets the password hashing function.
|
||||
func (s *Store) SetHashPasswordFn(fn HashPasswordFn) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.hashPassword = fn
|
||||
}
|
||||
|
||||
// storeFSM represents the finite state machine used by Store to interact with Raft.
|
||||
type storeFSM Store
|
||||
|
||||
|
@ -1578,6 +1628,7 @@ func (fsm *storeFSM) applyDropUserCommand(cmd *internal.Command) interface{} {
|
|||
return err
|
||||
}
|
||||
fsm.data = other
|
||||
delete(fsm.authCache, v.GetName())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1591,6 +1642,7 @@ func (fsm *storeFSM) applyUpdateUserCommand(cmd *internal.Command) interface{} {
|
|||
return err
|
||||
}
|
||||
fsm.data = other
|
||||
delete(fsm.authCache, v.GetName())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1737,18 +1789,6 @@ func (rpu *RetentionPolicyUpdate) SetName(v string) { rpu.Name = &v }
|
|||
func (rpu *RetentionPolicyUpdate) SetDuration(v time.Duration) { rpu.Duration = &v }
|
||||
func (rpu *RetentionPolicyUpdate) SetReplicaN(v int) { rpu.ReplicaN = &v }
|
||||
|
||||
// BcryptCost is the cost associated with generating password with Bcrypt.
|
||||
// This setting is lowered during testing to improve test suite performance.
|
||||
var BcryptCost = 10
|
||||
|
||||
// HashPassword generates a cryptographically secure hash for password.
|
||||
// Returns an error if the password is invalid or a hash cannot be generated.
|
||||
var HashPassword = func(password string) ([]byte, error) {
|
||||
// The second arg is the cost of the hashing, higher is slower but makes
|
||||
// it harder to brute force, since it will be really slow and impractical
|
||||
return bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
|
||||
}
|
||||
|
||||
// assert will panic with a given formatted message if the given condition is false.
|
||||
func assert(condition bool, msg string, v ...interface{}) {
|
||||
if !condition {
|
||||
|
|
|
@ -17,15 +17,9 @@ import (
|
|||
"github.com/influxdb/influxdb/meta"
|
||||
"github.com/influxdb/influxdb/tcp"
|
||||
"github.com/influxdb/influxdb/toml"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Disable password hashing to speed up testing.
|
||||
meta.HashPassword = func(password string) ([]byte, error) {
|
||||
return []byte(password), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the store returns an error
|
||||
func TestStore_Open_ErrStoreOpen(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -656,6 +650,58 @@ func TestStore_UpdateUser(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure Authentication works.
|
||||
func TestStore_Authentication(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
s := MustOpenStore()
|
||||
defer s.Close()
|
||||
|
||||
// Set the password hash function to the real thing for this test.
|
||||
s.SetHashPasswordFn(func(password string) ([]byte, error) {
|
||||
return bcrypt.GenerateFromPassword([]byte(password), 4)
|
||||
})
|
||||
|
||||
// Create user.
|
||||
s.CreateUser("susy", "pass", true)
|
||||
|
||||
// Authenticate user.
|
||||
if ui, err := s.Authenticate("susy", "pass"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if ui.Name != "susy" {
|
||||
t.Fatalf(`expected "susy", got "%s"`, ui.Name)
|
||||
}
|
||||
|
||||
// Update user's password.
|
||||
s.UpdateUser("susy", "pass2")
|
||||
|
||||
// Make sure authentication with old password does NOT work.
|
||||
if _, err := s.Authenticate("susy", "pass"); err == nil {
|
||||
t.Fatal("expected authentication error")
|
||||
}
|
||||
|
||||
// Authenticate user with new password
|
||||
if ui, err := s.Authenticate("susy", "pass2"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if ui.Name != "susy" {
|
||||
t.Fatalf(`expected "susy", got "%s"`, ui.Name)
|
||||
}
|
||||
|
||||
// Drop user.
|
||||
s.DropUser("susy")
|
||||
|
||||
// Make sure authentication with both old passwords does NOT work.
|
||||
if _, err := s.Authenticate("susy", "pass"); err == nil {
|
||||
t.Fatal("expected authentication error")
|
||||
} else if _, err := s.Authenticate("susy", "pass2"); err == nil {
|
||||
t.Fatal("expected authentication error")
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the store can return the count of users in it.
|
||||
func TestStore_UserCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -771,6 +817,7 @@ func NewStore(c meta.Config) *Store {
|
|||
Store: meta.NewStore(c),
|
||||
}
|
||||
s.Logger = log.New(&s.Stderr, "", log.LstdFlags)
|
||||
s.SetHashPasswordFn(mockHashPassword)
|
||||
return s
|
||||
}
|
||||
|
||||
|
@ -935,3 +982,8 @@ func MustTempFile() string {
|
|||
os.Remove(f.Name())
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
// mockHashPassword is used for most tests to avoid slow calls to bcrypt.
|
||||
func mockHashPassword(password string) ([]byte, error) {
|
||||
return []byte(password), nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue