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/05/31 09:08:30 UTC

[servicecomb-service-center] branch master updated: role binding check (#1027)

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 7f9ac68  role binding check (#1027)
7f9ac68 is described below

commit 7f9ac68d6a91b42b87f9673bb7ab692896956db3
Author: Shawn <xi...@gmail.com>
AuthorDate: Mon May 31 17:06:35 2021 +0800

    role binding check (#1027)
---
 datasource/account.go                    |  1 +
 datasource/account_test.go               |  4 +--
 datasource/etcd/account.go               | 48 ++++++++++++++++++++++++++++----
 datasource/etcd/path/key_generator.go    | 15 +++++++++-
 datasource/etcd/role.go                  | 16 +++++++++++
 datasource/mongo/client/mongo.go         | 38 ++++++++++++++++++++++++-
 datasource/mongo/role.go                 |  8 ++++--
 examples/etcd_data_struct.yaml           |  4 ++-
 server/resource/v4/role_resource.go      |  6 ++++
 server/resource/v4/role_resource_test.go | 38 +++++++++++++++++++------
 10 files changed, 157 insertions(+), 21 deletions(-)

diff --git a/datasource/account.go b/datasource/account.go
index 0c66730..89f5c63 100644
--- a/datasource/account.go
+++ b/datasource/account.go
@@ -31,6 +31,7 @@ var (
 	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")
 )
 
 // AccountManager contains the RBAC CRUD
diff --git a/datasource/account_test.go b/datasource/account_test.go
index b61845d..759ad0f 100644
--- a/datasource/account_test.go
+++ b/datasource/account_test.go
@@ -83,9 +83,9 @@ func TestAccount(t *testing.T) {
 		a2.Password = "new-password"
 		err = datasource.Instance().UpdateAccount(context.Background(), a2.Name, &a2)
 		assert.NoError(t, err)
-		_, n, err := datasource.Instance().ListAccount(context.Background())
+		accounts, _, err := datasource.Instance().ListAccount(context.Background())
 		assert.NoError(t, err)
-		assert.Equal(t, int64(2), n)
+		assert.GreaterOrEqual(t, len(accounts), 2)
 		_, err = datasource.Instance().DeleteAccount(context.Background(), []string{a1.Name})
 		assert.NoError(t, err)
 		_, err = datasource.Instance().DeleteAccount(context.Background(), []string{a2.Name})
diff --git a/datasource/etcd/account.go b/datasource/etcd/account.go
index 99bb296..bcf37e4 100644
--- a/datasource/etcd/account.go
+++ b/datasource/etcd/account.go
@@ -19,6 +19,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"github.com/go-chassis/foundation/stringutil"
 	"strconv"
 	"time"
 
@@ -44,7 +45,6 @@ func (ds *DataSource) CreateAccount(ctx context.Context, a *rbac.Account) error
 			log.Errorf(err, "can not release account lock")
 		}
 	}()
-	name := path.GenerateRBACAccountKey(a.Name)
 	exist, err := datasource.Instance().AccountExist(ctx, a.Name)
 	if err != nil {
 		log.Errorf(err, "can not save account info")
@@ -63,12 +63,12 @@ func (ds *DataSource) CreateAccount(ctx context.Context, a *rbac.Account) error
 	a.ID = util.GenerateUUID()
 	a.CreateTime = strconv.FormatInt(time.Now().Unix(), 10)
 	a.UpdateTime = a.CreateTime
-	value, err := json.Marshal(a)
+	opts, err := GenAccountOpts(a, client.ActionPut)
 	if err != nil {
-		log.Errorf(err, "account info is invalid")
+		log.Error("", err)
 		return err
 	}
-	err = client.PutBytes(ctx, name, value)
+	err = client.BatchCommit(ctx, opts)
 	if err != nil {
 		log.Errorf(err, "can not save account info")
 		return err
@@ -76,7 +76,28 @@ func (ds *DataSource) CreateAccount(ctx context.Context, a *rbac.Account) error
 	log.Info("create new account: " + a.ID)
 	return nil
 }
+func GenAccountOpts(a *rbac.Account, action client.ActionType) ([]client.PluginOp, error) {
+	opts := make([]client.PluginOp, 0)
+	value, err := json.Marshal(a)
+	if err != nil {
+		log.Errorf(err, "account info is invalid")
+		return nil, err
+	}
+	opts = append(opts, client.PluginOp{
+		Key:    stringutil.Str2bytes(path.GenerateAccountKey(a.Name)),
+		Value:  value,
+		Action: action,
+	})
+	for _, r := range a.Roles {
+		opt := client.PluginOp{
+			Key:    stringutil.Str2bytes(path.GenRoleAccountIdxKey(r, a.Name)),
+			Action: action,
+		}
+		opts = append(opts, opt)
+	}
 
+	return opts, nil
+}
 func (ds *DataSource) AccountExist(ctx context.Context, name string) (bool, error) {
 	resp, err := client.Instance().Do(ctx, client.GET,
 		client.WithStrKey(path.GenerateRBACAccountKey(name)))
@@ -142,8 +163,23 @@ func (ds *DataSource) DeleteAccount(ctx context.Context, names []string) (bool,
 		return false, nil
 	}
 	for _, name := range names {
-		_, err := client.Instance().Do(ctx, client.DEL,
-			client.WithStrKey(path.GenerateRBACAccountKey(name)))
+		a, err := ds.GetAccount(ctx, name)
+		if err != nil {
+			log.Error("", err)
+			continue //do not fail if some account is invalid
+
+		}
+		if a == nil {
+			log.Warn("can not find account")
+			continue
+		}
+		opts, err := GenAccountOpts(a, client.ActionDelete)
+		if err != nil {
+			log.Error("", err)
+			continue //do not fail if some account is invalid
+
+		}
+		err = client.BatchCommit(ctx, opts)
 		if err != nil {
 			log.Error(datasource.ErrDeleteAccountFailed.Error(), err)
 			return false, err
diff --git a/datasource/etcd/path/key_generator.go b/datasource/etcd/path/key_generator.go
index 8d6ce8b..f010a1a 100644
--- a/datasource/etcd/path/key_generator.go
+++ b/datasource/etcd/path/key_generator.go
@@ -76,7 +76,20 @@ func GenerateRBACRoleKey(name string) string {
 		name,
 	}, SPLIT)
 }
-
+func GenRoleAccountIdxKey(role, account string) string {
+	return util.StringJoin([]string{
+		GetRootKey(),
+		"idx-role-account",
+		role, account,
+	}, SPLIT)
+}
+func GenRoleAccountPrefixIdxKey(role string) string {
+	return util.StringJoin([]string{
+		GetRootKey(),
+		"idx-role-account",
+		role,
+	}, SPLIT)
+}
 func GenerateETCDProjectKey(domain, project string) string {
 	return util.StringJoin([]string{
 		GetProjectRootKey(domain),
diff --git a/datasource/etcd/role.go b/datasource/etcd/role.go
index 1cd803c..867f035 100644
--- a/datasource/etcd/role.go
+++ b/datasource/etcd/role.go
@@ -122,6 +122,14 @@ func (ds *DataSource) ListRole(ctx context.Context) ([]*rbac.Role, int64, error)
 	return roles, resp.Count, nil
 }
 func (ds *DataSource) DeleteRole(ctx context.Context, name string) (bool, error) {
+	exists, err := RoleBindingExists(ctx, name)
+	if err != nil {
+		log.Error("", err)
+		return false, err
+	}
+	if exists {
+		return false, datasource.ErrRoleBindingExist
+	}
 	resp, err := client.Instance().Do(ctx, client.DEL,
 		client.WithStrKey(path.GenerateRBACRoleKey(name)))
 	if err != nil {
@@ -129,6 +137,14 @@ func (ds *DataSource) DeleteRole(ctx context.Context, name string) (bool, error)
 	}
 	return resp.Succeeded, nil
 }
+func RoleBindingExists(ctx context.Context, role string) (bool, error) {
+	_, total, err := client.List(ctx, path.GenRoleAccountPrefixIdxKey(role))
+	if err != nil {
+		log.Error("", err)
+		return false, err
+	}
+	return total > 0, nil
+}
 func (ds *DataSource) UpdateRole(ctx context.Context, name string, role *rbac.Role) error {
 	role.UpdateTime = strconv.FormatInt(time.Now().Unix(), 10)
 	value, err := json.Marshal(role)
diff --git a/datasource/mongo/client/mongo.go b/datasource/mongo/client/mongo.go
index 4f7e761..55dec95 100644
--- a/datasource/mongo/client/mongo.go
+++ b/datasource/mongo/client/mongo.go
@@ -61,7 +61,15 @@ type MongoOperation struct {
 func GetMongoClient() *MongoClient {
 	return mc
 }
-
+func DeleteDoc(ctx context.Context, Table string, filter interface{}, opts ...*options.DeleteOptions) (*mongo.DeleteResult, error) {
+	return mc.Delete(ctx, Table, filter, opts...)
+}
+func Find(ctx context.Context, Table string, filter interface{}, opts ...*options.FindOptions) (*mongo.Cursor, error) {
+	return mc.Find(ctx, Table, filter, opts...)
+}
+func Count(ctx context.Context, Table string, filter interface{}, opts ...*options.CountOptions) (int64, error) {
+	return mc.db.Collection(Table).CountDocuments(ctx, filter, opts...)
+}
 func NewMongoClient(config storage.Options) {
 	inst := &MongoClient{}
 	if err := inst.Initialize(config); err != nil {
@@ -85,6 +93,34 @@ func (mc *MongoClient) Initialize(config storage.Options) (err error) {
 	return nil
 }
 
+// ExecTxn execute a transaction command
+// want to abort transaction, return error in cmd fn impl, otherwise it will commit transaction
+func (mc *MongoClient) ExecTxn(ctx context.Context, cmd func(sessionContext mongo.SessionContext) error) error {
+	session, err := mc.client.StartSession()
+	if err != nil {
+		return err
+	}
+	if err = session.StartTransaction(); err != nil {
+		return err
+	}
+	defer session.EndSession(ctx)
+	if err = mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error {
+		if err = cmd(sc); err != nil {
+			if err = session.AbortTransaction(sc); err != nil {
+				return err
+			}
+			return nil
+		} else {
+			if err = session.CommitTransaction(sc); err != nil {
+				return err
+			}
+			return nil
+		}
+	}); err != nil {
+		return err
+	}
+	return nil
+}
 func (mc *MongoClient) Err() <-chan error {
 	return mc.err
 }
diff --git a/datasource/mongo/role.go b/datasource/mongo/role.go
index 7ab4ce6..c4ab393 100644
--- a/datasource/mongo/role.go
+++ b/datasource/mongo/role.go
@@ -19,8 +19,8 @@ package mongo
 
 import (
 	"context"
-
 	"github.com/go-chassis/cari/rbac"
+	"go.mongodb.org/mongo-driver/bson"
 
 	"github.com/apache/servicecomb-service-center/datasource"
 	"github.com/apache/servicecomb-service-center/datasource/mongo/client"
@@ -102,8 +102,12 @@ func (ds *DataSource) ListRole(ctx context.Context) ([]*rbac.Role, int64, error)
 }
 
 func (ds *DataSource) DeleteRole(ctx context.Context, name string) (bool, error) {
+	n, err := client.Count(ctx, model.CollectionAccount, bson.M{"roles": bson.M{"$in": []string{name}}})
+	if n > 0 {
+		return false, datasource.ErrRoleBindingExist
+	}
 	filter := mutil.NewFilter(mutil.RoleName(name))
-	result, err := client.GetMongoClient().Delete(ctx, model.CollectionRole, filter)
+	result, err := client.DeleteDoc(ctx, model.CollectionRole, filter)
 	if err != nil {
 		return false, err
 	}
diff --git a/examples/etcd_data_struct.yaml b/examples/etcd_data_struct.yaml
index 13c98d7..bb18424 100644
--- a/examples/etcd_data_struct.yaml
+++ b/examples/etcd_data_struct.yaml
@@ -130,7 +130,9 @@
     "currentPassword": "password",
     "status": "normal"
   }
-
+# record role binding to account
+/cse-sr/idx-role-account/{role}/{account}:
+  {no value}
 # domain
 # /cse-sr/domains/{domain}
 /cse-sr/domains/default:
diff --git a/server/resource/v4/role_resource.go b/server/resource/v4/role_resource.go
index b768207..17e363b 100644
--- a/server/resource/v4/role_resource.go
+++ b/server/resource/v4/role_resource.go
@@ -19,6 +19,8 @@ package v4
 
 import (
 	"encoding/json"
+	"errors"
+	"github.com/apache/servicecomb-service-center/datasource"
 	"io/ioutil"
 	"net/http"
 
@@ -138,6 +140,10 @@ func (rr *RoleResource) DeleteRole(w http.ResponseWriter, req *http.Request) {
 	n := req.URL.Query().Get(":roleName")
 
 	status, err := dao.DeleteRole(req.Context(), n)
+	if errors.Is(err, datasource.ErrRoleBindingExist) {
+		rest.WriteError(w, discovery.ErrInvalidParams, errorsEx.MsgJSON)
+		return
+	}
 	if err != nil {
 		log.Error(errorsEx.MsgJSON, err)
 		rest.WriteError(w, discovery.ErrInternal, errorsEx.MsgJSON)
diff --git a/server/resource/v4/role_resource_test.go b/server/resource/v4/role_resource_test.go
index 76ea993..4598496 100644
--- a/server/resource/v4/role_resource_test.go
+++ b/server/resource/v4/role_resource_test.go
@@ -18,11 +18,12 @@ import (
 )
 
 func TestRoleResource_CreateOrUpdateRole(t *testing.T) {
-	var to = &rbacmodel.Token{}
+	var superToken = &rbacmodel.Token{}
 	ctx := context.TODO()
 	dao.DeleteAccount(ctx, "dev_test")
+	dao.DeleteAccount(ctx, "dev_test2")
 	dao.DeleteRole(ctx, "tester")
-	t.Run("root login", func(t *testing.T) {
+	t.Run("root login,to get super token", func(t *testing.T) {
 		b, _ := json.Marshal(&rbacmodel.Account{Name: "root", Password: "Complicated_password1"})
 
 		r, err := http.NewRequest(http.MethodPost, "/v4/token", bytes.NewBuffer(b))
@@ -30,14 +31,14 @@ func TestRoleResource_CreateOrUpdateRole(t *testing.T) {
 		w := httptest.NewRecorder()
 		rest.GetRouter().ServeHTTP(w, r)
 		assert.Equal(t, http.StatusOK, w.Code)
-		json.Unmarshal(w.Body.Bytes(), to)
+		json.Unmarshal(w.Body.Bytes(), superToken)
 	})
 
 	t.Run("create account dev_test and add a role", func(t *testing.T) {
 		b, _ := json.Marshal(&rbacmodel.Account{Name: "dev_test", Password: "Complicated_password3", Roles: []string{"tester"}})
 
 		r, _ := http.NewRequest(http.MethodPost, "/v4/accounts", bytes.NewBuffer(b))
-		r.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
+		r.Header.Set(restful.HeaderAuth, "Bearer "+superToken.TokenStr)
 		w := httptest.NewRecorder()
 		rest.GetRouter().ServeHTTP(w, r)
 		assert.Equal(t, http.StatusOK, w.Code)
@@ -64,13 +65,13 @@ func TestRoleResource_CreateOrUpdateRole(t *testing.T) {
 		})
 
 		r2, _ := http.NewRequest(http.MethodPost, "/v4/roles", bytes.NewReader(b2))
-		r2.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
+		r2.Header.Set(restful.HeaderAuth, "Bearer "+superToken.TokenStr)
 		w2 := httptest.NewRecorder()
 		rest.GetRouter().ServeHTTP(w2, r2)
 		assert.Equal(t, http.StatusOK, w2.Code)
 
 		r3, _ := http.NewRequest(http.MethodGet, "/v4/roles", nil)
-		r3.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
+		r3.Header.Set(restful.HeaderAuth, "Bearer "+superToken.TokenStr)
 		w3 := httptest.NewRecorder()
 		rest.GetRouter().ServeHTTP(w3, r3)
 		assert.Equal(t, http.StatusOK, w3.Code)
@@ -85,13 +86,13 @@ func TestRoleResource_CreateOrUpdateRole(t *testing.T) {
 			},
 		})
 		r4, _ := http.NewRequest(http.MethodPut, "/v4/roles/tester", bytes.NewReader(b4))
-		r4.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
+		r4.Header.Set(restful.HeaderAuth, "Bearer "+superToken.TokenStr)
 		w4 := httptest.NewRecorder()
 		rest.GetRouter().ServeHTTP(w4, r4)
 		assert.Equal(t, http.StatusOK, w4.Code)
 	})
 
-	t.Run("Inquire role", func(t *testing.T) {
+	t.Run("get role", func(t *testing.T) {
 		b, _ := json.Marshal(&rbacmodel.Account{Name: "root", Password: "Complicated_password1"})
 
 		r, _ := http.NewRequest(http.MethodPost, "/v4/token", bytes.NewBuffer(b))
@@ -124,6 +125,27 @@ func TestRoleResource_CreateOrUpdateRole(t *testing.T) {
 		rest.GetRouter().ServeHTTP(w3, r3)
 		assert.Equal(t, http.StatusForbidden, w3.Code)
 	})
+
+	t.Run("delete tester role, it is bind to account, should fail", func(t *testing.T) {
+		r3, _ := http.NewRequest(http.MethodDelete, "/v4/roles/tester", nil)
+		r3.Header.Set(restful.HeaderAuth, "Bearer "+superToken.TokenStr)
+		w3 := httptest.NewRecorder()
+		rest.GetRouter().ServeHTTP(w3, r3)
+		assert.Equal(t, http.StatusBadRequest, w3.Code)
+	})
+	t.Run("delete account, then delete role should success", func(t *testing.T) {
+		r3, _ := http.NewRequest(http.MethodDelete, "/v4/accounts/dev_test", nil)
+		r3.Header.Set(restful.HeaderAuth, "Bearer "+superToken.TokenStr)
+		w3 := httptest.NewRecorder()
+		rest.GetRouter().ServeHTTP(w3, r3)
+		assert.Equal(t, http.StatusOK, w3.Code)
+
+		r4, _ := http.NewRequest(http.MethodDelete, "/v4/roles/tester", nil)
+		r4.Header.Set(restful.HeaderAuth, "Bearer "+superToken.TokenStr)
+		w4 := httptest.NewRecorder()
+		rest.GetRouter().ServeHTTP(w4, r4)
+		assert.Equal(t, http.StatusOK, w4.Code)
+	})
 }
 func TestRoleResource_MoreRoles(t *testing.T) {
 	var to = &rbacmodel.Token{}