diff --git a/auth/auth_test.go b/auth/auth_test.go index a5890bd69..e49815230 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -1,9 +1,40 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ package auth import ( "testing" + "time" ) -func TestMain(t *testing.T) { -} +var l = NewUserLock(2 * time.Second) +func TestLock(t *testing.T) { + t.Log("Locking john") + l.Lock("john") + if !l.IsLocked("john") { + t.Errorf("John should be locked") + } + t.Log("Locking jack") + l.Lock("jack") + t.Log("Sleep for 2 seconds and check...") + time.Sleep(2 * time.Second) + if l.IsLocked("jack") { + t.Errorf("After 2 seconds, jack shouldn't be locked") + } + if l.IsLocked("daniel") { + t.Errorf("daniel has never been locked, he should not be locked") + } +} diff --git a/auth/authenticator.go b/auth/authenticator.go index 1640a842e..66f6bae36 100644 --- a/auth/authenticator.go +++ b/auth/authenticator.go @@ -17,13 +17,18 @@ package auth import ( "fmt" - "os" - "github.com/vmware/harbor/utils/log" + "os" + "time" "github.com/vmware/harbor/models" ) +// 1.5 seconds +const frozenTime time.Duration = 1500 * time.Millisecond + +var lock = NewUserLock(frozenTime) + // Authenticator provides interface to authenticate user credentials. type Authenticator interface { @@ -55,5 +60,15 @@ func Login(m models.AuthModel) (*models.User, error) { if !ok { return nil, fmt.Errorf("Unrecognized auth_mode: %s", authMode) } - return authenticator.Authenticate(m) + if lock.IsLocked(m.Principal) { + log.Debugf("%s is locked due to login failure, login failed", m.Principal) + return nil, nil + } + user, err := authenticator.Authenticate(m) + if user == nil && err == nil { + log.Debugf("Login failed, locking %s, and sleep for %v", m.Principal, frozenTime) + lock.Lock(m.Principal) + time.Sleep(frozenTime) + } + return user, err } diff --git a/auth/lock.go b/auth/lock.go new file mode 100644 index 000000000..2d8930987 --- /dev/null +++ b/auth/lock.go @@ -0,0 +1,52 @@ +/* + Copyright (c) 2016 VMware, Inc. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package auth + +import ( + "sync" + "time" +) + +// UserLock maintains a lock to block user from logging in within a short period of time. +type UserLock struct { + failures map[string]time.Time + d time.Duration + rw *sync.RWMutex +} + +// NewUserLock ... +func NewUserLock(freeze time.Duration) *UserLock { + return &UserLock{ + make(map[string]time.Time), + freeze, + &sync.RWMutex{}, + } +} + +// Lock marks a new login failure with the time it happens +func (ul *UserLock) Lock(username string) { + ul.rw.Lock() + defer ul.rw.Unlock() + ul.failures[username] = time.Now() +} + +// IsLocked checks whether a login request is happened within a period of time or not +// if it is, the authenticator should ignore the login request and return a failure immediately +func (ul *UserLock) IsLocked(username string) bool { + ul.rw.RLock() + defer ul.rw.RUnlock() + return time.Now().Sub(ul.failures[username]) <= ul.d +}