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 c0df5c36 Feature: add batch create accounts API (#1344)
c0df5c36 is described below

commit c0df5c3666074a63a97af43164ae1764ea9f7eeb
Author: little-cui <[email protected]>
AuthorDate: Sat Oct 22 15:04:52 2022 +0800

    Feature: add batch create accounts API (#1344)
---
 go.mod                                      |  2 +-
 go.sum                                      |  4 +-
 pkg/errors/text.go                          | 21 ++++---
 server/resource/rbac/auth_resource.go       | 23 +++++++
 server/resource/rbac/auth_resource_test.go  | 95 ++++++++++++++---------------
 server/service/rbac/account_service.go      | 28 +++++++++
 server/service/rbac/account_service_test.go | 38 ++++++++++++
 server/service/validator/rbac_validator.go  | 12 +++-
 8 files changed, 160 insertions(+), 63 deletions(-)

diff --git a/go.mod b/go.mod
index c6f9fad9..acb41602 100644
--- a/go.mod
+++ b/go.mod
@@ -20,7 +20,7 @@ require (
        github.com/cheggaaa/pb v1.0.25
        github.com/deckarep/golang-set v1.8.0
        github.com/elithrar/simple-scrypt v1.3.0
-       github.com/go-chassis/cari v0.8.0
+       github.com/go-chassis/cari v0.9.0
        github.com/go-chassis/foundation v0.4.0
        github.com/go-chassis/go-archaius v1.5.6
        github.com/go-chassis/go-chassis-extension/codec/gojson 
v0.0.0-20220816060440-fe98a5615d3f
diff --git a/go.sum b/go.sum
index ae213cfc..01be0afa 100644
--- a/go.sum
+++ b/go.sum
@@ -235,8 +235,8 @@ github.com/go-chassis/cari v0.4.0/go.mod 
h1:av/19fqwEP4eOC8unL/z67AAbFDwXUCko6SK
 github.com/go-chassis/cari v0.5.0/go.mod 
h1:av/19fqwEP4eOC8unL/z67AAbFDwXUCko6SKa4Avrd8=
 github.com/go-chassis/cari v0.5.1-0.20210823023004-74041d1363c4/go.mod 
h1:av/19fqwEP4eOC8unL/z67AAbFDwXUCko6SKa4Avrd8=
 github.com/go-chassis/cari v0.6.0/go.mod 
h1:mSDRCOQXGmlD69A6NG0hsv0UP1xbVPtL6HCGI6X1tqs=
-github.com/go-chassis/cari v0.8.0 
h1:rtFHRVxOBtdfkXN0KaAxr00F6DAcDcoWZRTMBgOPis8=
-github.com/go-chassis/cari v0.8.0/go.mod 
h1:vM13BN0TT505ZKqeJ+hUfzZvfn4nN0vgE6IpBOTWcTc=
+github.com/go-chassis/cari v0.9.0 
h1:skvo2PX8nLyu26CCg7qUMv7yP2DY73GrBW9M5tWj63c=
+github.com/go-chassis/cari v0.9.0/go.mod 
h1:vM13BN0TT505ZKqeJ+hUfzZvfn4nN0vgE6IpBOTWcTc=
 github.com/go-chassis/foundation v0.2.2-0.20201210043510-9f6d3de40234/go.mod 
h1:2PjwqpVwYEVaAldl5A58a08viH8p27pNeYaiE3ZxOBA=
 github.com/go-chassis/foundation v0.2.2/go.mod 
h1:2PjwqpVwYEVaAldl5A58a08viH8p27pNeYaiE3ZxOBA=
 github.com/go-chassis/foundation v0.3.0/go.mod 
h1:2PjwqpVwYEVaAldl5A58a08viH8p27pNeYaiE3ZxOBA=
diff --git a/pkg/errors/text.go b/pkg/errors/text.go
index ee84eef8..b002e3bb 100644
--- a/pkg/errors/text.go
+++ b/pkg/errors/text.go
@@ -20,14 +20,15 @@ package errors
 const (
        MsgJSON = "json is invalid"
 
-       MsgOperateAccountFailed   = "operate account failed"
-       MsgCantOperateRoot        = "root can not be operated"
-       MsgCantOperateBuildInRole = "build-in role can not be operated"
-       MsgCantOperateYour        = "your account can not be operated"
-       MsgOperateRoleFailed      = "operate role failed"
-       MsgGetAccountFailed       = "get account failed"
-       MsgGetRoleFailed          = "get role failed"
-       MsgRolePerm               = "check role permissions failed"
-       MsgNoPerm                 = "no permission to operate"
-       MsgListSelfPermsFailed    = "list self permissions failed"
+       MsgOperateAccountFailed      = "operate account failed"
+       MsgBatchCreateAccountsFailed = "batch create accounts failed"
+       MsgCantOperateRoot           = "root can not be operated"
+       MsgCantOperateBuildInRole    = "build-in role can not be operated"
+       MsgCantOperateYour           = "your account can not be operated"
+       MsgOperateRoleFailed         = "operate role failed"
+       MsgGetAccountFailed          = "get account failed"
+       MsgGetRoleFailed             = "get role failed"
+       MsgRolePerm                  = "check role permissions failed"
+       MsgNoPerm                    = "no permission to operate"
+       MsgListSelfPermsFailed       = "list self permissions failed"
 )
diff --git a/server/resource/rbac/auth_resource.go 
b/server/resource/rbac/auth_resource.go
index 91ae358a..cb9245b4 100644
--- a/server/resource/rbac/auth_resource.go
+++ b/server/resource/rbac/auth_resource.go
@@ -45,6 +45,7 @@ func (ar *AuthResource) URLPatterns() []rest.Route {
                {Method: http.MethodPost, Path: "/v4/token", Func: ar.Login},
                {Method: http.MethodGet, Path: "/v4/self-perms", Func: 
ar.ListSelfPerms},
                {Method: http.MethodPost, Path: "/v4/accounts", Func: 
ar.CreateAccount},
+               {Method: http.MethodPost, Path: "/v4/accounts/batch-create", 
Func: ar.BatchCreateAccount},
                {Method: http.MethodGet, Path: "/v4/accounts", Func: 
ar.ListAccount},
                {Method: http.MethodGet, Path: "/v4/accounts/:name", Func: 
ar.GetAccount},
                {Method: http.MethodDelete, Path: "/v4/accounts/:name", Func: 
ar.DeleteAccount},
@@ -218,6 +219,28 @@ func (ar *AuthResource) ListLock(w http.ResponseWriter, r 
*http.Request) {
        rest.WriteResponse(w, r, nil, resp)
 }
 
+func (ar *AuthResource) BatchCreateAccount(w http.ResponseWriter, r 
*http.Request) {
+       body, err := io.ReadAll(r.Body)
+       if err != nil {
+               log.Error("read body err", err)
+               rest.WriteError(w, discovery.ErrInternal, err.Error())
+               return
+       }
+       a := &rbacmodel.BatchCreateAccountsRequest{}
+       if err = json.Unmarshal(body, a); err != nil {
+               log.Error("json err", err)
+               rest.WriteError(w, discovery.ErrInvalidParams, err.Error())
+               return
+       }
+       resp, err := rbacsvc.BatchCreateAccounts(r.Context(), a)
+       if err != nil {
+               log.Error(errorsEx.MsgBatchCreateAccountsFailed, err)
+               rest.WriteServiceError(w, err)
+               return
+       }
+       rest.WriteResponse(w, r, nil, resp)
+}
+
 func MakeBanKey(name, ip string) string {
        return name + "::" + ip
 }
diff --git a/server/resource/rbac/auth_resource_test.go 
b/server/resource/rbac/auth_resource_test.go
index a0f46593..e9074e0f 100644
--- a/server/resource/rbac/auth_resource_test.go
+++ b/server/resource/rbac/auth_resource_test.go
@@ -84,6 +84,7 @@ func init() {
        rest.RegisterServant(&v4.AuthResource{})
        rest.RegisterServant(&v4.RoleResource{})
 }
+
 func TestAuthResource_Login(t *testing.T) {
        ctx := context.TODO()
 
@@ -179,17 +180,11 @@ func TestAuthResource_Login(t *testing.T) {
        })
 
 }
+
 func TestAuthResource_DeleteAccount(t *testing.T) {
-       rootToken := &rbacmodel.Token{}
+       rootToken := getToken(t, "root", devPwd1)
 
        t.Run("given root, try to delete it, should fail ", func(t *testing.T) {
-               b, _ := json.Marshal(&rbacmodel.Account{Name: "root", Password: 
devPwd1})
-
-               r, _ := http.NewRequest(http.MethodPost, "/v4/token", 
bytes.NewBuffer(b))
-               w := httptest.NewRecorder()
-               rest.GetRouter().ServeHTTP(w, r)
-               assert.Equal(t, http.StatusOK, w.Code)
-               json.Unmarshal(w.Body.Bytes(), rootToken)
 
                r2, _ := http.NewRequest(http.MethodDelete, 
"/v4/accounts/root", nil)
                r2.Header.Set(restful.HeaderAuth, "Bearer "+rootToken.TokenStr)
@@ -246,18 +241,9 @@ func TestAuthResource_DeleteAccount(t *testing.T) {
                assert.Equal(t, http.StatusOK, w3.Code)
        })
        t.Run("root can delete account", func(t *testing.T) {
-               var err error
-               b, err := json.Marshal(&rbacmodel.Account{Name: "root", 
Password: devPwd1})
-               assert.NoError(t, err)
-               r, err := http.NewRequest(http.MethodPost, "/v4/token", 
bytes.NewBuffer(b))
-               assert.NoError(t, err)
-               w := httptest.NewRecorder()
-               rest.GetRouter().ServeHTTP(w, r)
-               assert.Equal(t, http.StatusOK, w.Code)
-               to := &rbacmodel.Token{}
-               json.Unmarshal(w.Body.Bytes(), to)
+               to := getToken(t, "root", devPwd1)
 
-               b, _ = json.Marshal(&rbacmodel.Account{Name: "delete_account", 
Password: devPwd1, Roles: []string{rbacmodel.RoleDeveloper}})
+               b, _ := json.Marshal(&rbacmodel.Account{Name: "delete_account", 
Password: devPwd1, Roles: []string{rbacmodel.RoleDeveloper}})
                r2, _ := http.NewRequest(http.MethodPost, "/v4/accounts", 
bytes.NewBuffer(b))
                r2.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
                w2 := httptest.NewRecorder()
@@ -271,15 +257,10 @@ func TestAuthResource_DeleteAccount(t *testing.T) {
                assert.Equal(t, http.StatusOK, w3.Code)
        })
 }
+
 func TestAuthResource_GetAccount(t *testing.T) {
        t.Run("get account", func(t *testing.T) {
-               b, _ := json.Marshal(&rbacmodel.Account{Name: "root", Password: 
devPwd1})
-               r, _ := http.NewRequest(http.MethodPost, "/v4/token", 
bytes.NewBuffer(b))
-               w := httptest.NewRecorder()
-               rest.GetRouter().ServeHTTP(w, r)
-               assert.Equal(t, http.StatusOK, w.Code)
-               to := &rbacmodel.Token{}
-               json.Unmarshal(w.Body.Bytes(), to)
+               to := getToken(t, "root", devPwd1)
 
                r3, _ := http.NewRequest(http.MethodGet, 
"/v4/accounts/"+devAccount, nil)
                r3.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
@@ -293,14 +274,9 @@ func TestAuthResource_GetAccount(t *testing.T) {
                assert.Equal(t, []string{"developer"}, a.Roles)
                assert.Empty(t, a.Password)
        })
+
        t.Run("list account", func(t *testing.T) {
-               b, _ := json.Marshal(&rbacmodel.Account{Name: "root", Password: 
devPwd1})
-               r, _ := http.NewRequest(http.MethodPost, "/v4/token", 
bytes.NewBuffer(b))
-               w := httptest.NewRecorder()
-               rest.GetRouter().ServeHTTP(w, r)
-               assert.Equal(t, http.StatusOK, w.Code)
-               to := &rbacmodel.Token{}
-               json.Unmarshal(w.Body.Bytes(), to)
+               to := getToken(t, "root", devPwd1)
 
                r3, _ := http.NewRequest(http.MethodGet, "/v4/accounts", nil)
                r3.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
@@ -429,13 +405,7 @@ func TestAuthResource_SelfPerms(t *testing.T) {
 
 func TestAuthResource_ListLock(t *testing.T) {
        t.Run("admin list account, should pass", func(t *testing.T) {
-               b, _ := json.Marshal(&rbacmodel.Account{Name: "root", Password: 
devPwd1})
-               r, _ := http.NewRequest(http.MethodPost, "/v4/token", 
bytes.NewBuffer(b))
-               w := httptest.NewRecorder()
-               rest.GetRouter().ServeHTTP(w, r)
-               assert.Equal(t, http.StatusOK, w.Code)
-               to := &rbacmodel.Token{}
-               json.Unmarshal(w.Body.Bytes(), to)
+               to := getToken(t, "root", devPwd1)
 
                r3, _ := http.NewRequest(http.MethodGet, "/v4/account-locks", 
nil)
                r3.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
@@ -443,7 +413,7 @@ func TestAuthResource_ListLock(t *testing.T) {
                rest.GetRouter().ServeHTTP(w3, r3)
                assert.Equal(t, http.StatusOK, w3.Code)
                resp := &rbac.LockResponse{}
-               err := json.Unmarshal(w.Body.Bytes(), resp)
+               err := json.Unmarshal(w3.Body.Bytes(), resp)
                assert.NoError(t, err)
        })
        t.Run("not admin list account, should return 403", func(t *testing.T) {
@@ -451,13 +421,9 @@ func TestAuthResource_ListLock(t *testing.T) {
                err := rbacsvc.CreateAccount(context.TODO(), 
&rbacmodel.Account{Name: testListLock, Password: devPwd1, Roles: 
[]string{"developer"}})
                assert.NoError(t, err)
 
-               b, _ := json.Marshal(&rbacmodel.Account{Name: testListLock, 
Password: devPwd1})
-               r, _ := http.NewRequest(http.MethodPost, "/v4/token", 
bytes.NewBuffer(b))
-               w := httptest.NewRecorder()
-               rest.GetRouter().ServeHTTP(w, r)
-               assert.Equal(t, http.StatusOK, w.Code)
-               to := &rbacmodel.Token{}
-               json.Unmarshal(w.Body.Bytes(), to)
+               defer rbacsvc.DeleteAccount(context.TODO(), testListLock)
+
+               to := getToken(t, testListLock, devPwd1)
 
                r3, _ := http.NewRequest(http.MethodGet, "/v4/account-locks", 
nil)
                r3.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
@@ -467,6 +433,39 @@ func TestAuthResource_ListLock(t *testing.T) {
        })
 }
 
+func TestAuthResource_BatchCreateAccount(t *testing.T) {
+       to := getToken(t, "root", devPwd1)
+
+       b, _ := json.Marshal(&rbacmodel.BatchCreateAccountsRequest{Accounts: 
[]*rbacmodel.Account{
+               {Name: "TestAuthResource_BatchCreateAccount_1", Password: 
devPwd1, Roles: []string{"developer"}},
+       }})
+       r, _ := http.NewRequest(http.MethodPost, "/v4/accounts/batch-create", 
bytes.NewBuffer(b))
+       r.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
+       w := httptest.NewRecorder()
+       rest.GetRouter().ServeHTTP(w, r)
+       assert.Equal(t, http.StatusOK, w.Code)
+
+       defer rbacsvc.DeleteAccount(context.TODO(), 
"TestAuthResource_BatchCreateAccount_1")
+
+       a := &rbacmodel.BatchCreateAccountsResponse{}
+       json.Unmarshal(w.Body.Bytes(), a)
+
+       item := a.Accounts[0]
+       assert.Empty(t, item.Error)
+       assert.Equal(t, "TestAuthResource_BatchCreateAccount_1", item.Name)
+}
+
+func getToken(t *testing.T, name, pwd string) *rbacmodel.Token {
+       b, _ := json.Marshal(&rbacmodel.Account{Name: name, Password: pwd})
+       r, _ := http.NewRequest(http.MethodPost, "/v4/token", 
bytes.NewBuffer(b))
+       w := httptest.NewRecorder()
+       rest.GetRouter().ServeHTTP(w, r)
+       assert.Equal(t, http.StatusOK, w.Code)
+       to := &rbacmodel.Token{}
+       json.Unmarshal(w.Body.Bytes(), to)
+       return to
+}
+
 func BenchmarkAuthResource_LoginP(b *testing.B) {
        body, _ := json.Marshal(&rbacmodel.Account{Name: "root", Password: 
devPwd1})
        b.RunParallel(func(pb *testing.PB) {
diff --git a/server/service/rbac/account_service.go 
b/server/service/rbac/account_service.go
index 2dd87abc..264405ef 100644
--- a/server/service/rbac/account_service.go
+++ b/server/service/rbac/account_service.go
@@ -33,6 +33,7 @@ import (
        "github.com/apache/servicecomb-service-center/server/service/validator"
        "github.com/go-chassis/cari/discovery"
        "github.com/go-chassis/cari/dlock"
+       "github.com/go-chassis/cari/pkg/errsvc"
        rbacmodel "github.com/go-chassis/cari/rbac"
 )
 
@@ -217,3 +218,30 @@ func AccountUsage(ctx context.Context) (int64, error) {
        }
        return used, nil
 }
+
+func BatchCreateAccounts(ctx context.Context, req 
*rbacmodel.BatchCreateAccountsRequest) (*rbacmodel.BatchCreateAccountsResponse, 
error) {
+       err := validator.ValidateBatchCreateAccountsRequest(req)
+       if err != nil {
+               log.Error("batch create accounts failed", err)
+               return nil, discovery.NewError(discovery.ErrInvalidParams, 
err.Error())
+       }
+
+       var resp rbacmodel.BatchCreateAccountsResponse
+       var failed int
+       for _, account := range req.Accounts {
+               err := CreateAccount(ctx, account)
+               errEx, ok := err.(*errsvc.Error)
+               if err != nil && !ok {
+                       errEx = discovery.NewError(discovery.ErrInternal, 
err.Error())
+               }
+               if errEx != nil {
+                       failed++
+               }
+               resp.Accounts = append(resp.Accounts, 
&rbacmodel.BatchCreateAccountItemResponse{
+                       Name:  account.Name,
+                       Error: errEx,
+               })
+       }
+       log.Info(fmt.Sprintf("batch create accounts finish, succeed: %d, 
failed: %d", len(resp.Accounts)-failed, failed))
+       return &resp, nil
+}
diff --git a/server/service/rbac/account_service_test.go 
b/server/service/rbac/account_service_test.go
index a30e2b5c..0ccc6354 100644
--- a/server/service/rbac/account_service_test.go
+++ b/server/service/rbac/account_service_test.go
@@ -219,6 +219,7 @@ func TestGetAccount(t *testing.T) {
                assert.Equal(t, rbac.ErrAccountNotExist, svcErr.Code)
        })
 }
+
 func TestListAccount(t *testing.T) {
        t.Run("list account, should succeed", func(t *testing.T) {
                accounts, n, err := rbacsvc.ListAccount(context.TODO())
@@ -227,3 +228,40 @@ func TestListAccount(t *testing.T) {
                assert.Equal(t, n, int64(len(accounts)))
        })
 }
+
+func TestBatchCreateAccounts(t *testing.T) {
+       ctx := context.TODO()
+
+       t.Run("batch create invalid accounts, should failed", func(t 
*testing.T) {
+               resp, err := rbacsvc.BatchCreateAccounts(ctx, 
&rbac.BatchCreateAccountsRequest{})
+               assert.Nil(t, resp)
+               assert.Error(t, err)
+               svcErr := err.(*errsvc.Error)
+               assert.Equal(t, discovery.ErrInvalidParams, svcErr.Code)
+       })
+       t.Run("batch create accounts, should succeed", func(t *testing.T) {
+               a1 := newAccount("TestBatchCreateAccounts_account_1")
+               a2 := newAccount("TestBatchCreateAccounts_account_no_pwd")
+               a2.Password = ""
+
+               defer func() {
+                       rbacsvc.DeleteAccount(ctx, 
"TestBatchCreateAccounts_account_1")
+                       rbacsvc.DeleteAccount(ctx, 
"TestBatchCreateAccounts_account_no_pwd")
+               }()
+
+               resp, err := rbacsvc.BatchCreateAccounts(ctx, 
&rbac.BatchCreateAccountsRequest{
+                       Accounts: []*rbac.Account{a1, a2},
+               })
+               assert.NotNil(t, resp)
+               assert.NoError(t, err)
+               assert.Equal(t, 2, len(resp.Accounts))
+
+               item := resp.Accounts[0]
+               assert.Equal(t, "TestBatchCreateAccounts_account_1", item.Name)
+               assert.Nil(t, item.Error)
+
+               item = resp.Accounts[1]
+               assert.Equal(t, "TestBatchCreateAccounts_account_no_pwd", 
item.Name)
+               assert.NotEmpty(t, item.Code)
+       })
+}
diff --git a/server/service/validator/rbac_validator.go 
b/server/service/validator/rbac_validator.go
index 425beb4d..acb18706 100644
--- a/server/service/validator/rbac_validator.go
+++ b/server/service/validator/rbac_validator.go
@@ -25,7 +25,7 @@ import (
 var createAccountValidator = &validate.Validator{}
 var updateAccountValidator = &validate.Validator{}
 var createRoleValidator = &validate.Validator{}
-
+var batchCreateAccountsRequestValidator = &validate.Validator{}
 var changePWDValidator = &validate.Validator{}
 var accountLoginValidator = &validate.Validator{}
 
@@ -35,6 +35,8 @@ func init() {
        createAccountValidator.AddRule("Password", &validate.Rule{Regexp: 
&validate.PasswordChecker{}})
        createAccountValidator.AddRule("Status", &validate.Rule{Regexp: 
accountStatusRegex})
 
+       batchCreateAccountsRequestValidator.AddRule("Accounts", 
&validate.Rule{Min: 1, Max: 20})
+
        updateAccountValidator.AddRule("Roles", 
createAccountValidator.GetRule("Roles"))
        updateAccountValidator.AddRule("Status", 
createAccountValidator.GetRule("Status"))
 
@@ -53,7 +55,13 @@ func ValidateCreateAccount(a *rbac.Account) error {
        }
        return createAccountValidator.Validate(a)
 }
-
+func ValidateBatchCreateAccountsRequest(a *rbac.BatchCreateAccountsRequest) 
error {
+       err := baseCheck(a)
+       if err != nil {
+               return err
+       }
+       return batchCreateAccountsRequestValidator.Validate(a)
+}
 func ValidateUpdateAccount(a *rbac.Account) error {
        err := baseCheck(a)
        if err != nil {

Reply via email to