Merge pull request #3161 from influxdb/auth-cache-3102

fix #3102: add authentication cache
pull/3201/head
Philip O'Toole 2015-06-30 18:32:20 -04:00
commit ca417e19f6
5 changed files with 125 additions and 37 deletions

View File

@ -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]

View File

@ -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() {}

View File

@ -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

View File

@ -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 {

View File

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