You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@servicecomb.apache.org by li...@apache.org on 2021/06/17 01:37:31 UTC

[servicecomb-service-center] branch master updated: add account lock to manage login failure (#1056)

This is an automated email from the ASF dual-hosted git repository.

littlecui pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/servicecomb-service-center.git


The following commit(s) were added to refs/heads/master by this push:
     new 6d91dd8  add account lock to manage login failure (#1056)
6d91dd8 is described below

commit 6d91dd8410b40babab8b020615eb870d549c4cc8
Author: Shawn <xi...@gmail.com>
AuthorDate: Thu Jun 17 09:37:22 2021 +0800

    add account lock to manage login failure (#1056)
---
 datasource/account.go                  | 18 +++++++
 datasource/datasource.go               |  1 +
 datasource/etcd/account_lock.go        | 87 ++++++++++++++++++++++++++++++++++
 datasource/etcd/etcd.go                | 22 ++++++---
 datasource/etcd/path/key_generator.go  |  7 +++
 datasource/manager.go                  |  3 ++
 datasource/mongo/account_lock.go       | 60 +++++++++++++++++++++++
 datasource/mongo/mongo.go              | 18 ++++---
 datasource/options.go                  |  5 +-
 etc/conf/app.yaml                      |  2 +-
 server/server.go                       | 14 ++++--
 server/service/account/account.go      | 37 +++++++++++++++
 server/service/account/account_test.go | 34 +++++++++++++
 server/service/rbac/authr_plugin.go    |  4 +-
 server/service/rbac/blocker.go         | 72 +++++++++++-----------------
 server/service/rbac/blocker_test.go    | 15 +++---
 server/service/rbac/password.go        |  2 +-
 test/test.go                           |  6 ++-
 18 files changed, 330 insertions(+), 77 deletions(-)

diff --git a/datasource/account.go b/datasource/account.go
index 89f5c63..1c04cd7 100644
--- a/datasource/account.go
+++ b/datasource/account.go
@@ -28,12 +28,18 @@ var (
 	ErrAccountDuplicated   = errors.New("account is duplicated")
 	ErrAccountCanNotEdit   = errors.New("account can not be edited")
 	ErrDLockNotFound       = errors.New("dlock not found")
+	ErrCannotReleaseLock   = errors.New("can not release account")
+	ErrAccountLockNotExist = errors.New("account not exist")
 	ErrDeleteAccountFailed = errors.New("failed to delete account")
 	ErrQueryAccountFailed  = errors.New("failed to query account")
 	ErrAccountNotExist     = errors.New("account not exist")
 	ErrRoleBindingExist    = errors.New("role is bind to account")
 )
 
+const (
+	StatusBanned = "banned"
+)
+
 // AccountManager contains the RBAC CRUD
 type AccountManager interface {
 	CreateAccount(ctx context.Context, a *rbac.Account) error
@@ -43,3 +49,15 @@ type AccountManager interface {
 	DeleteAccount(ctx context.Context, names []string) (bool, error)
 	UpdateAccount(ctx context.Context, name string, account *rbac.Account) error
 }
+
+// AccountLockManager saves login failure status
+type AccountLockManager interface {
+	GetLock(ctx context.Context, key string) (*AccountLock, error)
+	DeleteLock(ctx context.Context, key string) error
+	Ban(ctx context.Context, key string) error
+}
+type AccountLock struct {
+	Key       string `json:"key,omitempty"`
+	Status    string `json:"status,omitempty"`
+	ReleaseAt int64  `json:"releaseAt,omitempty"`
+}
diff --git a/datasource/datasource.go b/datasource/datasource.go
index d229bb4..7909178 100644
--- a/datasource/datasource.go
+++ b/datasource/datasource.go
@@ -21,6 +21,7 @@ package datasource
 type DataSource interface {
 	SystemManager() SystemManager
 	AccountManager() AccountManager
+	AccountLockManager() AccountLockManager
 	RoleManager() RoleManager
 	DependencyManager() DependencyManager
 	MetadataManager() MetadataManager
diff --git a/datasource/etcd/account_lock.go b/datasource/etcd/account_lock.go
new file mode 100644
index 0000000..24cfd5c
--- /dev/null
+++ b/datasource/etcd/account_lock.go
@@ -0,0 +1,87 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 etcd
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/apache/servicecomb-service-center/datasource"
+	"github.com/apache/servicecomb-service-center/datasource/etcd/client"
+	"github.com/apache/servicecomb-service-center/datasource/etcd/path"
+	"github.com/apache/servicecomb-service-center/pkg/log"
+)
+
+type AccountLockManager struct {
+	releaseAfter time.Duration
+}
+
+func (al AccountLockManager) GetLock(ctx context.Context, key string) (*datasource.AccountLock, error) {
+	resp, err := client.Instance().Do(ctx, client.GET,
+		client.WithStrKey(path.GenerateAccountLockKey(key)))
+	if err != nil {
+		return nil, err
+	}
+	if resp.Count == 0 {
+		return nil, datasource.ErrAccountLockNotExist
+	}
+	lock := &datasource.AccountLock{}
+	err = json.Unmarshal(resp.Kvs[0].Value, lock)
+	if err != nil {
+		log.Errorf(err, "format invalid")
+		return nil, err
+	}
+	return lock, nil
+}
+
+func (al AccountLockManager) DeleteLock(ctx context.Context, key string) error {
+	ok, err := client.Delete(ctx, key)
+	if err != nil {
+		log.Errorf(err, "remove lock failed")
+		return datasource.ErrCannotReleaseLock
+	}
+	if ok {
+		log.Info(fmt.Sprintf("%s is released", key))
+		return nil
+	}
+	return datasource.ErrCannotReleaseLock
+}
+
+func NewAccountLockManager(ReleaseAfter time.Duration) datasource.AccountLockManager {
+	return &AccountLockManager{releaseAfter: ReleaseAfter}
+}
+
+func (al AccountLockManager) Ban(ctx context.Context, key string) error {
+	l := &datasource.AccountLock{}
+	l.Key = key
+	l.Status = datasource.StatusBanned
+	l.ReleaseAt = time.Now().Add(al.releaseAfter).Unix()
+	value, err := json.Marshal(l)
+	if err != nil {
+		log.Errorf(err, "account lock is invalid")
+		return err
+	}
+	etcdKey := path.GenerateAccountLockKey(key)
+	err = client.PutBytes(ctx, etcdKey, value)
+	if err != nil {
+		log.Errorf(err, "can not save account lock")
+		return err
+	}
+	log.Info(fmt.Sprintf("%s is locked, release at %d", key, l.ReleaseAt))
+	return nil
+}
diff --git a/datasource/etcd/etcd.go b/datasource/etcd/etcd.go
index d7223c0..d3975e0 100644
--- a/datasource/etcd/etcd.go
+++ b/datasource/etcd/etcd.go
@@ -42,12 +42,17 @@ func init() {
 }
 
 type DataSource struct {
-	accountManager  datasource.AccountManager
-	metadataManager datasource.MetadataManager
-	roleManager     datasource.RoleManager
-	sysManager      datasource.SystemManager
-	depManager      datasource.DependencyManager
-	scManager       datasource.SCManager
+	accountLockManager datasource.AccountLockManager
+	accountManager     datasource.AccountManager
+	metadataManager    datasource.MetadataManager
+	roleManager        datasource.RoleManager
+	sysManager         datasource.SystemManager
+	depManager         datasource.DependencyManager
+	scManager          datasource.SCManager
+}
+
+func (ds *DataSource) AccountLockManager() datasource.AccountLockManager {
+	return ds.accountLockManager
 }
 
 func (ds *DataSource) SystemManager() datasource.SystemManager {
@@ -86,8 +91,11 @@ func NewDataSource(opts datasource.Options) (datasource.DataSource, error) {
 	if err := inst.initialize(opts); err != nil {
 		return nil, err
 	}
-
+	if opts.ReleaseAccountAfter == 0 {
+		opts.ReleaseAccountAfter = 15 * time.Minute
+	}
 	inst.accountManager = &AccountManager{}
+	inst.accountLockManager = NewAccountLockManager(opts.ReleaseAccountAfter)
 	inst.roleManager = &RoleManager{}
 	inst.metadataManager = newMetadataManager(opts.SchemaEditable, opts.InstanceTTL)
 	inst.sysManager = newSysManager()
diff --git a/datasource/etcd/path/key_generator.go b/datasource/etcd/path/key_generator.go
index f010a1a..228f808 100644
--- a/datasource/etcd/path/key_generator.go
+++ b/datasource/etcd/path/key_generator.go
@@ -373,6 +373,13 @@ func GenerateAccountKey(name string) string {
 		name,
 	}, SPLIT)
 }
+func GenerateAccountLockKey(key string) string {
+	return util.StringJoin([]string{
+		GetRootKey(),
+		"account-locks",
+		key,
+	}, SPLIT)
+}
 func GenerateRBACSecretKey() string {
 	return util.StringJoin([]string{
 		GetRootKey(),
diff --git a/datasource/manager.go b/datasource/manager.go
index 3f39f45..9cd2f23 100644
--- a/datasource/manager.go
+++ b/datasource/manager.go
@@ -81,6 +81,9 @@ func GetRoleManager() RoleManager {
 func GetAccountManager() AccountManager {
 	return dataSourceInst.AccountManager()
 }
+func GetAccountLockManager() AccountLockManager {
+	return dataSourceInst.AccountLockManager()
+}
 func GetDependencyManager() DependencyManager {
 	return dataSourceInst.DependencyManager()
 }
diff --git a/datasource/mongo/account_lock.go b/datasource/mongo/account_lock.go
new file mode 100644
index 0000000..7d21333
--- /dev/null
+++ b/datasource/mongo/account_lock.go
@@ -0,0 +1,60 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 mongo
+
+import (
+	"context"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/apache/servicecomb-service-center/datasource"
+	"github.com/apache/servicecomb-service-center/pkg/log"
+)
+
+type AccountLockManager struct {
+	releaseAfter time.Duration
+	locks        sync.Map
+}
+
+func (al *AccountLockManager) GetLock(ctx context.Context, key string) (*datasource.AccountLock, error) {
+	l, ok := al.locks.Load(key)
+	if !ok {
+		log.Debug(fmt.Sprintf("%s is not locked", key))
+		return nil, datasource.ErrAccountLockNotExist
+	}
+	return l.(*datasource.AccountLock), nil
+}
+
+func (al *AccountLockManager) DeleteLock(ctx context.Context, key string) error {
+	al.locks.Delete(key)
+	log.Warn(fmt.Sprintf("%s is released", key))
+	return nil
+}
+
+func NewAccountLockManager(ReleaseAfter time.Duration) datasource.AccountLockManager {
+	return &AccountLockManager{releaseAfter: ReleaseAfter}
+}
+
+func (al *AccountLockManager) Ban(ctx context.Context, key string) error {
+	l := &datasource.AccountLock{}
+	l.Key = key
+	l.Status = datasource.StatusBanned
+	l.ReleaseAt = time.Now().Add(al.releaseAfter).Unix()
+	al.locks.Store(key, l)
+	log.Warn(fmt.Sprintf("%s is locked, release at %d", key, l.ReleaseAt))
+	return nil
+}
diff --git a/datasource/mongo/mongo.go b/datasource/mongo/mongo.go
index ca16277..6d167da 100644
--- a/datasource/mongo/mongo.go
+++ b/datasource/mongo/mongo.go
@@ -43,12 +43,17 @@ func init() {
 }
 
 type DataSource struct {
-	accountManager  datasource.AccountManager
-	metadataManager datasource.MetadataManager
-	roleManager     datasource.RoleManager
-	sysManager      datasource.SystemManager
-	depManager      datasource.DependencyManager
-	scManager       datasource.SCManager
+	accountLockManager datasource.AccountLockManager
+	accountManager     datasource.AccountManager
+	metadataManager    datasource.MetadataManager
+	roleManager        datasource.RoleManager
+	sysManager         datasource.SystemManager
+	depManager         datasource.DependencyManager
+	scManager          datasource.SCManager
+}
+
+func (ds *DataSource) AccountLockManager() datasource.AccountLockManager {
+	return ds.accountLockManager
 }
 
 func (ds *DataSource) SystemManager() datasource.SystemManager {
@@ -88,6 +93,7 @@ func NewDataSource(opts datasource.Options) (datasource.DataSource, error) {
 	inst.roleManager = &RoleManager{}
 	inst.metadataManager = &MetadataManager{SchemaEditable: opts.SchemaEditable, InstanceTTL: opts.InstanceTTL}
 	inst.accountManager = &AccountManager{}
+	inst.accountLockManager = NewAccountLockManager(opts.ReleaseAccountAfter)
 	return inst, nil
 }
 
diff --git a/datasource/options.go b/datasource/options.go
index b8022b4..0e97b5c 100644
--- a/datasource/options.go
+++ b/datasource/options.go
@@ -17,12 +17,15 @@
 
 package datasource
 
+import "time"
+
 //Options contains configuration for plugins
 type Options struct {
 	Kind           Kind
 	SslEnabled     bool
 	SchemaEditable bool
 	// InstanceTTL: the default ttl of instance lease
-	InstanceTTL int64
+	InstanceTTL         int64
+	ReleaseAccountAfter time.Duration
 	// TODO: pay attention to more net config like TLSConfig when coding
 }
diff --git a/etc/conf/app.yaml b/etc/conf/app.yaml
index 72de775..eb5a36e 100644
--- a/etc/conf/app.yaml
+++ b/etc/conf/app.yaml
@@ -158,7 +158,7 @@ rbac:
   enable: false
   privateKeyFile: ./private.key
   publicKeyFile: ./public.key
-
+  releaseLockAfter: 15m # failure login attempt causes account blocking, that is block duration
 metrics:
   # enable to start metrics gather
   enable: true
diff --git a/server/server.go b/server/server.go
index e81729e..7f3954f 100644
--- a/server/server.go
+++ b/server/server.go
@@ -111,11 +111,17 @@ func (s *ServiceCenterServer) initEndpoints() {
 func (s *ServiceCenterServer) initDatasource() {
 	// init datasource
 	kind := datasource.Kind(config.GetString("registry.kind", "", config.WithStandby("registry_plugin")))
+	ReleaseAccountAfter := config.GetString("rbac.releaseLockAfter", "15m")
+	d, err := time.ParseDuration(ReleaseAccountAfter)
+	if err != nil {
+		log.Warn("releaseAfter is invalid, use default config")
+	}
 	if err := datasource.Init(datasource.Options{
-		Kind:           kind,
-		SslEnabled:     config.GetSSL().SslEnabled,
-		InstanceTTL:    config.GetRegistry().InstanceTTL,
-		SchemaEditable: config.GetRegistry().SchemaEditable,
+		Kind:                kind,
+		SslEnabled:          config.GetSSL().SslEnabled,
+		InstanceTTL:         config.GetRegistry().InstanceTTL,
+		SchemaEditable:      config.GetRegistry().SchemaEditable,
+		ReleaseAccountAfter: d,
 	}); err != nil {
 		log.Fatal("init datasource failed", err)
 	}
diff --git a/server/service/account/account.go b/server/service/account/account.go
new file mode 100644
index 0000000..306b72b
--- /dev/null
+++ b/server/service/account/account.go
@@ -0,0 +1,37 @@
+package account
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/apache/servicecomb-service-center/pkg/log"
+
+	"github.com/apache/servicecomb-service-center/datasource"
+)
+
+func IsBanned(ctx context.Context, key string) (bool, error) {
+	lock, err := datasource.GetAccountLockManager().GetLock(ctx, key)
+	if err != nil {
+		if err == datasource.ErrAccountLockNotExist {
+			return false, nil
+		}
+		return false, err
+	}
+	if lock.ReleaseAt < time.Now().Unix() {
+		err := datasource.GetAccountLockManager().DeleteLock(ctx, key)
+		if err != nil {
+			log.Errorf(err, "remove lock failed")
+			return false, datasource.ErrCannotReleaseLock
+		}
+		log.Info(fmt.Sprintf("release lock for %s", key))
+		return false, nil
+	}
+	if lock.Status == datasource.StatusBanned {
+		return true, nil
+	}
+	return false, nil
+}
+func Ban(ctx context.Context, key string) error {
+	return datasource.GetAccountLockManager().Ban(ctx, key)
+}
diff --git a/server/service/account/account_test.go b/server/service/account/account_test.go
new file mode 100644
index 0000000..eacbaab
--- /dev/null
+++ b/server/service/account/account_test.go
@@ -0,0 +1,34 @@
+package account_test
+
+import (
+	"context"
+
+	"testing"
+	"time"
+
+	_ "github.com/apache/servicecomb-service-center/test"
+
+	"github.com/apache/servicecomb-service-center/server/service/account"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsBanned(t *testing.T) {
+	t.Run("ban a key and check status, it should be banned, check other key should not be banned",
+		func(t *testing.T) {
+			err := account.Ban(context.TODO(), "dev_guy::127.0.0.1")
+			assert.NoError(t, err)
+
+			ok, err := account.IsBanned(context.TODO(), "dev_guy::127.0.0.1")
+			assert.NoError(t, err)
+			assert.True(t, ok)
+
+			ok, err = account.IsBanned(context.TODO(), "test_guy::127.0.0.1")
+			assert.NoError(t, err)
+			assert.False(t, ok)
+
+			time.Sleep(4 * time.Second)
+			ok, err = account.IsBanned(context.TODO(), "dev_guy::127.0.0.1")
+			assert.NoError(t, err)
+			assert.False(t, ok)
+		})
+}
diff --git a/server/service/rbac/authr_plugin.go b/server/service/rbac/authr_plugin.go
index f22de7e..cfd8df5 100644
--- a/server/service/rbac/authr_plugin.go
+++ b/server/service/rbac/authr_plugin.go
@@ -59,14 +59,14 @@ func (a *EmbeddedAuthenticator) Login(ctx context.Context, user string, password
 	account, err := GetAccount(ctx, user)
 	if err != nil {
 		if errsvc.IsErrEqualCode(err, rbac.ErrAccountNotExist) {
-			CountFailure(MakeBanKey(user, ip))
+			TryLockAccount(MakeBanKey(user, ip))
 			return "", rbac.NewError(rbac.ErrUserOrPwdWrong, "")
 		}
 		return "", err
 	}
 	same := privacy.SamePassword(account.Password, password)
 	if !same {
-		CountFailure(MakeBanKey(user, ip))
+		TryLockAccount(MakeBanKey(user, ip))
 		return "", rbac.NewError(rbac.ErrUserOrPwdWrong, "")
 	}
 
diff --git a/server/service/rbac/blocker.go b/server/service/rbac/blocker.go
index dce5053..a8766dd 100644
--- a/server/service/rbac/blocker.go
+++ b/server/service/rbac/blocker.go
@@ -18,9 +18,14 @@
 package rbac
 
 import (
+	"context"
+	"fmt"
 	"sync"
 	"time"
 
+	"github.com/apache/servicecomb-service-center/pkg/log"
+	accountsvc "github.com/apache/servicecomb-service-center/server/service/account"
+
 	"golang.org/x/time/rate"
 )
 
@@ -32,53 +37,36 @@ const (
 
 var BanTime = 1 * time.Hour
 
-type Client struct {
-	limiter   *rate.Limiter
-	Key       string
-	Banned    bool
-	ReleaseAt time.Time //at this time client can be allow to attempt to do something
+type LoginFailureLimiter struct {
+	limiter *rate.Limiter
+	Key     string
 }
 
 var clients sync.Map
 
-func BannedList() []*Client {
-	cs := make([]*Client, 0)
-	clients.Range(func(key, value interface{}) bool {
-		client := value.(*Client)
-		if client.Banned && time.Now().After(client.ReleaseAt) {
-			client.Banned = false
-			client.ReleaseAt = time.Time{}
-			return true
-		}
-		cs = append(cs, client)
-		return true
-	})
-	return cs
-}
-
-//CountFailure can cause a client banned
+//TryLockAccount try to lock the account login attempt
 // it use time/rate to allow certainty failure,
-//but will ban client if rate limiter can not accept failures
-func CountFailure(key string) {
+//it will ban client if rate limiter can not accept failures
+func TryLockAccount(key string) {
 	var c interface{}
-	var client *Client
+	var l *LoginFailureLimiter
 	var ok bool
-	now := time.Now()
 	if c, ok = clients.Load(key); !ok {
-		client = &Client{
-			Key:       key,
-			limiter:   rate.NewLimiter(rate.Every(BlockInterval), MaxAttempts),
-			ReleaseAt: time.Time{},
+		l = &LoginFailureLimiter{
+			Key:     key,
+			limiter: rate.NewLimiter(rate.Every(BlockInterval), MaxAttempts),
 		}
-		clients.Store(key, client)
+		clients.Store(key, l)
 	} else {
-		client = c.(*Client)
+		l = c.(*LoginFailureLimiter)
 	}
 
-	allow := client.limiter.AllowN(time.Now(), 1)
+	allow := l.limiter.AllowN(time.Now(), 1)
 	if !allow {
-		client.Banned = true
-		client.ReleaseAt = now.Add(BanTime)
+		err := accountsvc.Ban(context.TODO(), key)
+		if err != nil {
+			log.Error(fmt.Sprintf("can not ban account %s", key), err)
+		}
 	}
 }
 
@@ -86,16 +74,10 @@ func CountFailure(key string) {
 //it will release the client from banned status
 //use account name plus ip as key will maximum reduce the client conflicts
 func IsBanned(key string) bool {
-	var c interface{}
-	var client *Client
-	var ok bool
-	if c, ok = clients.Load(key); !ok {
-		return false
-	}
-	client = c.(*Client)
-	if client.Banned && time.Now().After(client.ReleaseAt) {
-		client.Banned = false
-		client.ReleaseAt = time.Time{}
+	IsBanned, err := accountsvc.IsBanned(context.TODO(), key)
+	if err != nil {
+		log.Error("can not check lock list, so return banned for security concern", err)
+		return true
 	}
-	return client.Banned
+	return IsBanned
 }
diff --git a/server/service/rbac/blocker_test.go b/server/service/rbac/blocker_test.go
index 7a92c89..59624b7 100644
--- a/server/service/rbac/blocker_test.go
+++ b/server/service/rbac/blocker_test.go
@@ -33,28 +33,25 @@ func TestCountFailure(t *testing.T) {
 	key1 := v4.MakeBanKey("root", "127.0.0.1")
 	key2 := v4.MakeBanKey("root", "10.0.0.1")
 	t.Run("ban root@IP, will not affect other root@another_IP", func(t *testing.T) {
-		rbac.CountFailure(key1)
+		rbac.TryLockAccount(key1)
 		assert.False(t, rbac.IsBanned(key1))
 
-		rbac.CountFailure(key1)
+		rbac.TryLockAccount(key1)
 		assert.False(t, rbac.IsBanned(key1))
 
-		rbac.CountFailure(key1)
+		rbac.TryLockAccount(key1)
 		assert.True(t, rbac.IsBanned(key1))
 
-		rbac.CountFailure(key2)
+		rbac.TryLockAccount(key2)
 		assert.False(t, rbac.IsBanned(key2))
 
-		rbac.CountFailure(key2)
+		rbac.TryLockAccount(key2)
 		assert.False(t, rbac.IsBanned(key2))
 
-		rbac.CountFailure(key2)
+		rbac.TryLockAccount(key2)
 		assert.True(t, rbac.IsBanned(key2))
 	})
-	t.Log(rbac.BannedList()[0].ReleaseAt)
-	assert.Equal(t, 2, len(rbac.BannedList()))
 	time.Sleep(4 * time.Second)
-	assert.Equal(t, 0, len(rbac.BannedList()))
 	assert.False(t, rbac.IsBanned(key1))
 	assert.False(t, rbac.IsBanned(key2))
 
diff --git a/server/service/rbac/password.go b/server/service/rbac/password.go
index 856058f..d751057 100644
--- a/server/service/rbac/password.go
+++ b/server/service/rbac/password.go
@@ -88,7 +88,7 @@ func changePassword(ctx context.Context, name, currentPassword, pwd string) erro
 	same := privacy.SamePassword(old.Password, currentPassword)
 	if !same {
 		log.Error("current password is wrong", nil)
-		CountFailure(MakeBanKey(name, ip))
+		TryLockAccount(MakeBanKey(name, ip))
 		return rbac.NewError(rbac.ErrOldPwdWrong, "")
 	}
 	return doChangePassword(ctx, old, pwd)
diff --git a/test/test.go b/test/test.go
index caf37f2..4e2e9be 100644
--- a/test/test.go
+++ b/test/test.go
@@ -19,6 +19,8 @@
 package test
 
 import (
+	"time"
+
 	_ "github.com/apache/servicecomb-service-center/server/init"
 	"github.com/apache/servicecomb-service-center/server/service/disco"
 
@@ -34,6 +36,7 @@ func init() {
 	if t == nil {
 		t = "etcd"
 	}
+	archaius.Set("rbac.releaseLockAfter", "3s")
 	if t == "etcd" {
 		archaius.Set("registry.cache.mode", 0)
 		archaius.Set("discovery.kind", "etcd")
@@ -41,6 +44,7 @@ func init() {
 	} else {
 		archaius.Set("registry.heartbeat.kind", "checker")
 	}
-	datasource.Init(datasource.Options{Kind: datasource.Kind(t.(string))})
+	datasource.Init(datasource.Options{Kind: datasource.Kind(t.(string)),
+		ReleaseAccountAfter: 3 * time.Second})
 	core.ServiceAPI = disco.AssembleResources()
 }