From ce434b496e054ba4b56e995949cf5b068dfbf594 Mon Sep 17 00:00:00 2001 From: SimFG Date: Tue, 23 Aug 2022 10:26:53 +0800 Subject: [PATCH] Implement the mysql metastore of the rbac (#18704) Signed-off-by: SimFG Signed-off-by: SimFG --- configs/milvus.yaml | 2 +- internal/metastore/catalog.go | 10 +- internal/metastore/db/dao/collection_test.go | 44 +- internal/metastore/db/dao/common.go | 12 + internal/metastore/db/dao/grant.go | 93 +++ internal/metastore/db/dao/grant_test.go | 419 ++++++++++ internal/metastore/db/dao/role.go | 41 + internal/metastore/db/dao/role_test.go | 132 +++ internal/metastore/db/dao/user.go | 14 +- internal/metastore/db/dao/user_role.go | 41 + internal/metastore/db/dao/user_role_test.go | 193 +++++ internal/metastore/db/dao/user_test.go | 33 +- internal/metastore/db/dbmodel/base_model.go | 11 + internal/metastore/db/dbmodel/common.go | 10 + internal/metastore/db/dbmodel/grant.go | 101 +++ .../metastore/db/dbmodel/mocks/IGrantDb.go | 79 ++ .../metastore/db/dbmodel/mocks/IMetaDomain.go | 48 ++ .../metastore/db/dbmodel/mocks/IRoleDb.go | 79 ++ .../metastore/db/dbmodel/mocks/IUserDb.go | 10 +- .../metastore/db/dbmodel/mocks/IUserRoleDb.go | 79 ++ internal/metastore/db/dbmodel/role.go | 23 + internal/metastore/db/dbmodel/user.go | 2 +- internal/metastore/db/dbmodel/user_role.go | 21 + .../metastore/db/rootcoord/table_catalog.go | 289 ++++++- .../db/rootcoord/table_catalog_test.go | 749 +++++++++++++++++- internal/metastore/kv/rootcoord/kv_catalog.go | 54 +- internal/proxy/authentication_interceptor.go | 4 +- internal/rootcoord/meta_table.go | 10 +- internal/rootcoord/meta_table_test.go | 33 +- internal/rootcoord/root_coord.go | 115 ++- scripts/run_go_unittest.sh | 1 + scripts/sql/meta.sql | 40 + 32 files changed, 2626 insertions(+), 166 deletions(-) create mode 100644 internal/metastore/db/dao/grant.go create mode 100644 internal/metastore/db/dao/grant_test.go create mode 100644 internal/metastore/db/dao/role.go create mode 100644 internal/metastore/db/dao/role_test.go create mode 100644 internal/metastore/db/dao/user_role.go create mode 100644 internal/metastore/db/dao/user_role_test.go create mode 100644 internal/metastore/db/dbmodel/base_model.go create mode 100644 internal/metastore/db/dbmodel/grant.go create mode 100644 internal/metastore/db/dbmodel/mocks/IGrantDb.go create mode 100644 internal/metastore/db/dbmodel/mocks/IRoleDb.go create mode 100644 internal/metastore/db/dbmodel/mocks/IUserRoleDb.go create mode 100644 internal/metastore/db/dbmodel/role.go create mode 100644 internal/metastore/db/dbmodel/user_role.go diff --git a/configs/milvus.yaml b/configs/milvus.yaml index 9d68279bc..512e6962c 100644 --- a/configs/milvus.yaml +++ b/configs/milvus.yaml @@ -55,7 +55,7 @@ metastore: # Related configuration of mysql, used to store Milvus metadata. mysql: username: root - password: 11111111 + password: 123456 address: localhost port: 3306 dbName: milvus_meta diff --git a/internal/metastore/catalog.go b/internal/metastore/catalog.go index 807499601..014b72863 100644 --- a/internal/metastore/catalog.go +++ b/internal/metastore/catalog.go @@ -38,11 +38,11 @@ type RootCoordCatalog interface { CreateRole(ctx context.Context, tenant string, entity *milvuspb.RoleEntity) error DropRole(ctx context.Context, tenant string, roleName string) error - OperateUserRole(ctx context.Context, tenant string, userEntity *milvuspb.UserEntity, roleEntity *milvuspb.RoleEntity, operateType milvuspb.OperateUserRoleType) error - SelectRole(ctx context.Context, tenant string, entity *milvuspb.RoleEntity, includeUserInfo bool) ([]*milvuspb.RoleResult, error) - SelectUser(ctx context.Context, tenant string, entity *milvuspb.UserEntity, includeRoleInfo bool) ([]*milvuspb.UserResult, error) - OperatePrivilege(ctx context.Context, tenant string, entity *milvuspb.GrantEntity, operateType milvuspb.OperatePrivilegeType) error - SelectGrant(ctx context.Context, tenant string, entity *milvuspb.GrantEntity) ([]*milvuspb.GrantEntity, error) + AlterUserRole(ctx context.Context, tenant string, userEntity *milvuspb.UserEntity, roleEntity *milvuspb.RoleEntity, operateType milvuspb.OperateUserRoleType) error + ListRole(ctx context.Context, tenant string, entity *milvuspb.RoleEntity, includeUserInfo bool) ([]*milvuspb.RoleResult, error) + ListUser(ctx context.Context, tenant string, entity *milvuspb.UserEntity, includeRoleInfo bool) ([]*milvuspb.UserResult, error) + AlterGrant(ctx context.Context, tenant string, entity *milvuspb.GrantEntity, operateType milvuspb.OperatePrivilegeType) error + ListGrant(ctx context.Context, tenant string, entity *milvuspb.GrantEntity) ([]*milvuspb.GrantEntity, error) ListPolicy(ctx context.Context, tenant string) ([]string, error) ListUserRole(ctx context.Context, tenant string) ([]string, error) diff --git a/internal/metastore/db/dao/collection_test.go b/internal/metastore/db/dao/collection_test.go index 8c20f2201..dbc8bfc0c 100644 --- a/internal/metastore/db/dao/collection_test.go +++ b/internal/metastore/db/dao/collection_test.go @@ -44,6 +44,9 @@ var ( indexTestDb dbmodel.IIndexDb segIndexTestDb dbmodel.ISegmentIndexDb userTestDb dbmodel.IUserDb + roleTestDb dbmodel.IRoleDb + userRoleTestDb dbmodel.IUserRoleDb + grantTestDb dbmodel.IGrantDb ) // TestMain is the first function executed in current package, we will do some initial here @@ -51,6 +54,7 @@ func TestMain(m *testing.M) { var ( db *sql.DB err error + ctx = context.TODO() ) // setting sql MUST exact match @@ -70,14 +74,17 @@ func TestMain(m *testing.M) { // set mocked database dbcore.SetGlobalDB(DB) - collTestDb = NewMetaDomain().CollectionDb(context.TODO()) - aliasTestDb = NewMetaDomain().CollAliasDb(context.TODO()) - channelTestDb = NewMetaDomain().CollChannelDb(context.TODO()) - fieldTestDb = NewMetaDomain().FieldDb(context.TODO()) - partitionTestDb = NewMetaDomain().PartitionDb(context.TODO()) - indexTestDb = NewMetaDomain().IndexDb(context.TODO()) - segIndexTestDb = NewMetaDomain().SegmentIndexDb(context.TODO()) - userTestDb = NewMetaDomain().UserDb(context.TODO()) + collTestDb = NewMetaDomain().CollectionDb(ctx) + aliasTestDb = NewMetaDomain().CollAliasDb(ctx) + channelTestDb = NewMetaDomain().CollChannelDb(ctx) + fieldTestDb = NewMetaDomain().FieldDb(ctx) + partitionTestDb = NewMetaDomain().PartitionDb(ctx) + indexTestDb = NewMetaDomain().IndexDb(ctx) + segIndexTestDb = NewMetaDomain().SegmentIndexDb(ctx) + userTestDb = NewMetaDomain().UserDb(ctx) + roleTestDb = NewMetaDomain().RoleDb(ctx) + userRoleTestDb = NewMetaDomain().UserRoleDb(ctx) + grantTestDb = NewMetaDomain().GrantDb(ctx) // m.Run entry for executing tests os.Exit(m.Run()) @@ -392,3 +399,24 @@ func (a AnyTime) Match(v driver.Value) bool { _, ok := v.(time.Time) return ok } + +func GetBase() dbmodel.Base { + return dbmodel.Base{ + TenantID: tenantID, + IsDeleted: false, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } +} + +func SuccessExec(f func()) { + mock.ExpectBegin() + f() + mock.ExpectCommit() +} + +func ErrorExec(f func()) { + mock.ExpectBegin() + f() + mock.ExpectRollback() +} diff --git a/internal/metastore/db/dao/common.go b/internal/metastore/db/dao/common.go index e84c94b0f..0970aa896 100644 --- a/internal/metastore/db/dao/common.go +++ b/internal/metastore/db/dao/common.go @@ -44,3 +44,15 @@ func (*metaDomain) SegmentIndexDb(ctx context.Context) dbmodel.ISegmentIndexDb { func (*metaDomain) UserDb(ctx context.Context) dbmodel.IUserDb { return &userDb{dbcore.GetDB(ctx)} } + +func (d *metaDomain) RoleDb(ctx context.Context) dbmodel.IRoleDb { + return &roleDb{dbcore.GetDB(ctx)} +} + +func (d *metaDomain) UserRoleDb(ctx context.Context) dbmodel.IUserRoleDb { + return &userRoleDb{dbcore.GetDB(ctx)} +} + +func (d *metaDomain) GrantDb(ctx context.Context) dbmodel.IGrantDb { + return &grantDb{dbcore.GetDB(ctx)} +} diff --git a/internal/metastore/db/dao/grant.go b/internal/metastore/db/dao/grant.go new file mode 100644 index 000000000..6b94614da --- /dev/null +++ b/internal/metastore/db/dao/grant.go @@ -0,0 +1,93 @@ +package dao + +import ( + "errors" + "fmt" + + "github.com/milvus-io/milvus/internal/common" + + "github.com/milvus-io/milvus/internal/log" + "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type grantDb struct { + db *gorm.DB +} + +func (g *grantDb) GetGrants(tenantID string, roleID int64, object string, objectName string) ([]*dbmodel.Grant, error) { + var ( + grants []*dbmodel.Grant + err error + ) + err = g.db.Model(&dbmodel.Grant{}).Where(&dbmodel.Grant{RoleID: roleID, Object: object, ObjectName: objectName}).Where(dbmodel.GetCommonCondition(tenantID, false)).Preload("Role").Find(&grants).Error + if err != nil { + log.Error("fail to get grants", zap.String("tenant_id", tenantID), zap.Int64("roleID", roleID), zap.String("object", object), zap.String("object_name", objectName), zap.Error(err)) + return nil, err + } + return grants, nil +} + +func (g *grantDb) Insert(in *dbmodel.Grant) error { + var ( + sqlWhere = &dbmodel.Grant{RoleID: in.RoleID, Object: in.Object, ObjectName: in.ObjectName} + dbGrant *dbmodel.Grant + newDbDetail string + err error + ) + err = g.db.Model(&dbmodel.Grant{}).Where(sqlWhere).Where(dbmodel.GetCommonCondition(in.TenantID, false)).Take(&dbGrant).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + err = g.db.Create(in).Error + if err != nil { + log.Error("fail to insert the grant", zap.Any("in", in), zap.Error(err)) + } + return err + } + if err != nil { + log.Error("fail to take the origin grant", zap.Any("in", in), zap.Error(err)) + return err + } + if newDbDetail, err = dbmodel.EncodeGrantDetailForString(dbGrant.Detail, in.Detail, true); err != nil { + log.Error("fail to encode the grant detail", zap.Any("in", in), zap.Error(err)) + return err + } + err = g.db.Model(dbmodel.Grant{}).Where(sqlWhere).Where(dbmodel.GetCommonCondition(in.TenantID, false)).Update("detail", newDbDetail).Error + if err != nil { + log.Error("fail to update the grant", zap.Any("in", in), zap.Error(err)) + } + return err +} + +func (g *grantDb) Delete(tenantID string, roleID int64, object string, objectName string, privilege string) error { + var ( + sqlWhere = &dbmodel.Grant{RoleID: roleID, Object: object, ObjectName: objectName} + dbGrant *dbmodel.Grant + newDbDetail string + db *gorm.DB + err error + ) + + err = g.db.Model(&dbmodel.Grant{}).Where(sqlWhere).Where(dbmodel.GetCommonCondition(tenantID, false)).Take(&dbGrant).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return common.NewIgnorableError(fmt.Errorf("the privilege[%s-%s-%s] isn't granted", object, objectName, privilege)) + } + if err != nil { + log.Error("fail to take the origin grant", zap.Any("where", sqlWhere), zap.Error(err)) + return err + } + if newDbDetail, err = dbmodel.EncodeGrantDetail(dbGrant.Detail, "", privilege, false); err != nil { + log.Error("fail to encode the grant detail", zap.Any("detail", dbGrant.Detail), zap.String("privilege", privilege), zap.Error(err)) + return err + } + db = g.db.Model(dbmodel.Grant{}).Where(sqlWhere).Where(dbmodel.GetCommonCondition(tenantID, false)) + if newDbDetail == "" { + err = db.Update("is_deleted", true).Error + } else { + err = db.Update("detail", newDbDetail).Error + } + if err != nil { + log.Error("fail to delete the grant", zap.Bool("is_delete", newDbDetail == ""), zap.Any("where", sqlWhere), zap.String("privilege", privilege), zap.Error(err)) + } + return err +} diff --git a/internal/metastore/db/dao/grant_test.go b/internal/metastore/db/dao/grant_test.go new file mode 100644 index 000000000..595da9815 --- /dev/null +++ b/internal/metastore/db/dao/grant_test.go @@ -0,0 +1,419 @@ +package dao + +import ( + "errors" + "testing" + + "github.com/milvus-io/milvus/internal/common" + + "gorm.io/gorm" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" + "github.com/stretchr/testify/assert" +) + +func TestGrant_GetGrants(t *testing.T) { + var ( + roleID1 = 10 + roleID2 = 20 + object = "Collection" + objectName = "col1" + grants []*dbmodel.Grant + getQuery func() *sqlmock.ExpectedQuery + err error + ) + + getQuery = func() *sqlmock.ExpectedQuery { + return mock.ExpectQuery("SELECT * FROM `grant` WHERE `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(false, tenantID) + } + getQuery().WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "role_id", "object", "object_name"}). + AddRow(tenantID, roleID1, object, objectName). + AddRow(tenantID, roleID2, object, objectName)) + + mock.ExpectQuery("SELECT * FROM `role` WHERE `role`.`id` IN (?,?)"). + WithArgs(roleID1, roleID2). + WillReturnRows( + sqlmock.NewRows([]string{"id", "tenant_id", "name"}). + AddRow(roleID1, tenantID, "foo1"). + AddRow(roleID2, tenantID, "foo2")) + + grants, err = grantTestDb.GetGrants(tenantID, 0, "", "") + assert.NoError(t, err) + assert.Equal(t, 2, len(grants)) + assert.Equal(t, "foo2", grants[1].Role.Name) + assert.Equal(t, object, grants[0].Object) + assert.Equal(t, objectName, grants[0].ObjectName) + + getQuery().WillReturnError(errors.New("test error")) + _, err = grantTestDb.GetGrants(tenantID, 0, "", "") + assert.Error(t, err) + +} + +func TestGrant_GetGrantsWithRoleID(t *testing.T) { + var ( + roleID1 = 10 + object1 = "Collection" + objectName1 = "col1" + object2 = "Global" + objectName2 = "*" + grants []*dbmodel.Grant + getQuery func() *sqlmock.ExpectedQuery + err error + ) + + getQuery = func() *sqlmock.ExpectedQuery { + return mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(roleID1, false, tenantID) + } + getQuery().WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "role_id", "object", "object_name"}). + AddRow(tenantID, roleID1, object1, objectName1). + AddRow(tenantID, roleID1, object2, objectName2)) + + mock.ExpectQuery("SELECT * FROM `role` WHERE `role`.`id` = ?"). + WithArgs(roleID1). + WillReturnRows( + sqlmock.NewRows([]string{"id", "tenant_id", "name"}). + AddRow(roleID1, tenantID, "foo1")) + + grants, err = grantTestDb.GetGrants(tenantID, int64(roleID1), "", "") + assert.NoError(t, err) + assert.Equal(t, 2, len(grants)) + assert.Equal(t, "foo1", grants[0].Role.Name) + assert.Equal(t, object1, grants[0].Object) + assert.Equal(t, objectName2, grants[1].ObjectName) +} + +func TestGrant_GetGrantsWithObject(t *testing.T) { + var ( + roleID = 10 + object = "Collection" + objectName = "col1" + detail1 = "privilege1..." + detail2 = "privilege2..." + grants []*dbmodel.Grant + getQuery func() *sqlmock.ExpectedQuery + err error + ) + + getQuery = func() *sqlmock.ExpectedQuery { + return mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(roleID, object, objectName, false, tenantID) + } + getQuery().WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "role_id", "object", "object_name", "detail"}). + AddRow(tenantID, roleID, object, objectName, detail1). + AddRow(tenantID, roleID, object, objectName, detail2)) + + mock.ExpectQuery("SELECT * FROM `role` WHERE `role`.`id` = ?"). + WithArgs(roleID). + WillReturnRows( + sqlmock.NewRows([]string{"id", "tenant_id", "name"}). + AddRow(roleID, tenantID, "foo1")) + + grants, err = grantTestDb.GetGrants(tenantID, int64(roleID), object, objectName) + assert.NoError(t, err) + assert.Equal(t, 2, len(grants)) + assert.Equal(t, "foo1", grants[0].Role.Name) + assert.Equal(t, object, grants[1].Object) + assert.Equal(t, objectName, grants[1].ObjectName) + assert.Equal(t, detail2, grants[1].Detail) +} + +func TestGrant_Insert(t *testing.T) { + var ( + grant *dbmodel.Grant + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail1\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `grant` (`tenant_id`,`is_deleted`,`created_at`,`updated_at`,`role_id`,`object`,`object_name`,`detail`) VALUES (?,?,?,?,?,?,?,?)"). + WithArgs(grant.TenantID, grant.IsDeleted, grant.CreatedAt, grant.UpdatedAt, grant.RoleID, grant.Object, grant.ObjectName, grant.Detail). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + err = grantTestDb.Insert(grant) + assert.NoError(t, err) + +} + +func TestGrant_Insert_Error(t *testing.T) { + var ( + grant *dbmodel.Grant + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail2\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `grant` (`tenant_id`,`is_deleted`,`created_at`,`updated_at`,`role_id`,`object`,`object_name`,`detail`) VALUES (?,?,?,?,?,?,?,?)"). + WithArgs(grant.TenantID, grant.IsDeleted, grant.CreatedAt, grant.UpdatedAt, grant.RoleID, grant.Object, grant.ObjectName, grant.Detail). + WillReturnError(errors.New("test error")) + mock.ExpectRollback() + err = grantTestDb.Insert(grant) + assert.Error(t, err) +} + +func TestGrant_Insert_SelectError(t *testing.T) { + var ( + grant *dbmodel.Grant + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail3\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnError(errors.New("test error")) + err = grantTestDb.Insert(grant) + assert.Error(t, err) +} + +func TestGrant_InsertDecode(t *testing.T) { + var ( + grant *dbmodel.Grant + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "role_id", "object", "object_name", "detail"}). + AddRow(tenantID, grant.RoleID, grant.Object, grant.ObjectName, "aaa")) + err = grantTestDb.Insert(grant) + assert.Error(t, err) +} + +func TestGrant_InsertUpdate(t *testing.T) { + var ( + originDetail = "[[\"admin\",\"PrivilegeLoad\"]]" + grant *dbmodel.Grant + expectDetail string + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail\"]]", + } + + expectDetail, _ = dbmodel.EncodeGrantDetailForString(originDetail, grant.Detail, true) + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "role_id", "object", "object_name", "detail"}). + AddRow(tenantID, grant.RoleID, grant.Object, grant.ObjectName, originDetail)) + mock.ExpectBegin() + mock.ExpectExec("UPDATE `grant` SET `detail`=?,`updated_at`=? WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(expectDetail, AnyTime{}, grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + err = grantTestDb.Insert(grant) + assert.NoError(t, err) +} + +func TestGrant_InsertUpdateError(t *testing.T) { + var ( + originDetail = "[[\"admin\",\"PrivilegeIndexDetail\"]]" + grant *dbmodel.Grant + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "role_id", "object", "object_name", "detail"}). + AddRow(tenantID, grant.RoleID, grant.Object, grant.ObjectName, originDetail)) + err = grantTestDb.Insert(grant) + assert.Error(t, err) + assert.True(t, common.IsIgnorableError(err)) +} + +func TestGrant_DeleteWithoutPrivilege(t *testing.T) { + var ( + grant *dbmodel.Grant + privilege = "PrivilegeIndexDetail" + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnError(gorm.ErrRecordNotFound) + err = grantTestDb.Delete(grant.TenantID, grant.RoleID, grant.Object, grant.ObjectName, privilege) + assert.Error(t, err) + assert.True(t, common.IsIgnorableError(err)) +} + +func TestGrant_Delete_GetError(t *testing.T) { + var ( + grant *dbmodel.Grant + privilege = "PrivilegeIndexDetail" + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnError(errors.New("test error")) + err = grantTestDb.Delete(grant.TenantID, grant.RoleID, grant.Object, grant.ObjectName, privilege) + assert.Error(t, err) +} + +func TestGrant_Delete_DecodeError(t *testing.T) { + var ( + grant *dbmodel.Grant + privilege = "PrivilegeIndexDetail" + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "role_id", "object", "object_name", "detail"}). + AddRow(tenantID, grant.RoleID, grant.Object, grant.ObjectName, "aaa")) + err = grantTestDb.Delete(grant.TenantID, grant.RoleID, grant.Object, grant.ObjectName, privilege) + assert.Error(t, err) +} + +func TestGrant_Delete_Mark(t *testing.T) { + var ( + grant *dbmodel.Grant + privilege = "PrivilegeIndexDetail" + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "role_id", "object", "object_name", "detail"}). + AddRow(tenantID, grant.RoleID, grant.Object, grant.ObjectName, grant.Detail)) + mock.ExpectBegin() + mock.ExpectExec("UPDATE `grant` SET `is_deleted`=?,`updated_at`=? WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(true, AnyTime{}, grant.RoleID, grant.Object, grant.ObjectName, false, grant.TenantID). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + err = grantTestDb.Delete(grant.TenantID, grant.RoleID, grant.Object, grant.ObjectName, privilege) + assert.NoError(t, err) +} + +func TestGrant_Delete_Update(t *testing.T) { + var ( + grant *dbmodel.Grant + privilege = "PrivilegeIndexDetail" + expectDetail = "[[\"admin\",\"PrivilegeLoad\"]]" + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexDetail\"],[\"admin\",\"PrivilegeLoad\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "role_id", "object", "object_name", "detail"}). + AddRow(tenantID, grant.RoleID, grant.Object, grant.ObjectName, grant.Detail)) + mock.ExpectBegin() + mock.ExpectExec("UPDATE `grant` SET `detail`=?,`updated_at`=? WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(expectDetail, AnyTime{}, grant.RoleID, grant.Object, grant.ObjectName, false, grant.TenantID). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + err = grantTestDb.Delete(grant.TenantID, grant.RoleID, grant.Object, grant.ObjectName, privilege) + assert.NoError(t, err) +} + +func TestGrant_Delete_UpdateError(t *testing.T) { + var ( + grant *dbmodel.Grant + privilege = "PrivilegeIndexDetail" + err error + ) + grant = &dbmodel.Grant{ + Base: GetBase(), + RoleID: 1, + Object: "Global", + ObjectName: "Col", + Detail: "[[\"admin\",\"PrivilegeIndexLoad\"],[\"admin\",\"PrivilegeQuery\"]]", + } + + mock.ExpectQuery("SELECT * FROM `grant` WHERE `grant`.`role_id` = ? AND `grant`.`object` = ? AND `grant`.`object_name` = ? AND `is_deleted` = ? AND `tenant_id` = ? LIMIT 1"). + WithArgs(grant.RoleID, grant.Object, grant.ObjectName, grant.IsDeleted, grant.TenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "role_id", "object", "object_name", "detail"}). + AddRow(tenantID, grant.RoleID, grant.Object, grant.ObjectName, grant.Detail)) + err = grantTestDb.Delete(grant.TenantID, grant.RoleID, grant.Object, grant.ObjectName, privilege) + assert.Error(t, err) + assert.True(t, common.IsIgnorableError(err)) +} diff --git a/internal/metastore/db/dao/role.go b/internal/metastore/db/dao/role.go new file mode 100644 index 000000000..4020a5d72 --- /dev/null +++ b/internal/metastore/db/dao/role.go @@ -0,0 +1,41 @@ +package dao + +import ( + "github.com/milvus-io/milvus/internal/log" + "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type roleDb struct { + db *gorm.DB +} + +func (r *roleDb) GetRoles(tenantID string, name string) ([]*dbmodel.Role, error) { + var ( + roles []*dbmodel.Role + err error + ) + err = r.db.Model(&dbmodel.Role{}).Where(&dbmodel.Role{Name: name}).Where(dbmodel.GetCommonCondition(tenantID, false)).Find(&roles).Error + if err != nil { + log.Error("fail to get roles", zap.String("tenant_id", tenantID), zap.String("name", name), zap.Error(err)) + return nil, err + } + return roles, nil +} + +func (r *roleDb) Insert(in *dbmodel.Role) error { + err := r.db.Create(in).Error + if err != nil { + log.Error("fail to insert the role", zap.Any("in", in), zap.Error(err)) + } + return err +} + +func (r *roleDb) Delete(tenantID string, name string) error { + err := r.db.Model(dbmodel.Role{}).Where(&dbmodel.Role{Name: name}).Where(dbmodel.GetCommonCondition(tenantID, false)).Update("is_deleted", true).Error + if err != nil { + log.Error("fail to delete the role", zap.String("tenant_id", tenantID), zap.String("name", name), zap.Error(err)) + } + return err +} diff --git a/internal/metastore/db/dao/role_test.go b/internal/metastore/db/dao/role_test.go new file mode 100644 index 000000000..f9b31b5e5 --- /dev/null +++ b/internal/metastore/db/dao/role_test.go @@ -0,0 +1,132 @@ +package dao + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestRole_GetRoles(t *testing.T) { + var ( + roles []*dbmodel.Role + err error + ) + + mock.ExpectQuery("SELECT * FROM `role` WHERE `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(false, tenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "name"}). + AddRow(tenantID, "foo1"). + AddRow(tenantID, "foo2")) + + roles, err = roleTestDb.GetRoles(tenantID, "") + assert.NoError(t, err) + assert.Equal(t, 2, len(roles)) +} + +func TestRole_GetRoles_Error(t *testing.T) { + mock.ExpectQuery("SELECT * FROM `role` WHERE `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(false, tenantID). + WillReturnError(errors.New("test error")) + _, err := roleTestDb.GetRoles(tenantID, "") + assert.Error(t, err) +} + +func TestRole_GetRoles_WithRoleName(t *testing.T) { + var ( + roleName = "foo1" + roles []*dbmodel.Role + err error + ) + mock.ExpectQuery("SELECT * FROM `role` WHERE `role`.`name` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(roleName, false, tenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "name"}). + AddRow(tenantID, roleName)) + roles, err = roleTestDb.GetRoles(tenantID, roleName) + assert.NoError(t, err) + assert.Equal(t, 1, len(roles)) + assert.Equal(t, roleName, roles[0].Name) +} + +func TestRole_Insert(t *testing.T) { + var ( + role *dbmodel.Role + err error + ) + role = &dbmodel.Role{ + Base: GetBase(), + Name: "foo", + } + + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `role` (`tenant_id`,`is_deleted`,`created_at`,`updated_at`,`name`) VALUES (?,?,?,?,?)"). + WithArgs(role.TenantID, role.IsDeleted, role.CreatedAt, role.UpdatedAt, role.Name). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + err = roleTestDb.Insert(role) + assert.NoError(t, err) + +} + +func TestRole_Insert_Error(t *testing.T) { + var ( + role *dbmodel.Role + err error + ) + role = &dbmodel.Role{ + Base: GetBase(), + Name: "foo", + } + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `role` (`tenant_id`,`is_deleted`,`created_at`,`updated_at`,`name`) VALUES (?,?,?,?,?)"). + WithArgs(role.TenantID, role.IsDeleted, role.CreatedAt, role.UpdatedAt, role.Name). + WillReturnError(errors.New("test error")) + mock.ExpectRollback() + err = roleTestDb.Insert(role) + assert.Error(t, err) +} + +func TestRole_Delete(t *testing.T) { + var ( + role *dbmodel.Role + err error + ) + role = &dbmodel.Role{ + Base: GetBase(), + Name: "foo", + } + mock.ExpectBegin() + mock.ExpectExec("UPDATE `role` SET `is_deleted`=?,`updated_at`=? WHERE `role`.`name` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(true, AnyTime{}, role.Name, role.IsDeleted, role.TenantID). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + err = roleTestDb.Delete(role.TenantID, role.Name) + assert.NoError(t, err) +} + +func TestRole_Delete_Error(t *testing.T) { + var ( + role *dbmodel.Role + err error + ) + role = &dbmodel.Role{ + Base: GetBase(), + Name: "foo", + } + mock.ExpectBegin() + mock.ExpectExec("UPDATE `role` SET `is_deleted`=?,`updated_at`=? WHERE `role`.`name` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(true, AnyTime{}, role.Name, role.IsDeleted, role.TenantID). + WillReturnError(errors.New("test error")) + mock.ExpectRollback() + + err = roleTestDb.Delete(role.TenantID, role.Name) + assert.Error(t, err) +} diff --git a/internal/metastore/db/dao/user.go b/internal/metastore/db/dao/user.go index 8a7571efb..c1c64e2fb 100644 --- a/internal/metastore/db/dao/user.go +++ b/internal/metastore/db/dao/user.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" + "github.com/milvus-io/milvus/internal/common" + "github.com/milvus-io/milvus/internal/log" "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" "go.uber.org/zap" @@ -20,7 +22,7 @@ func (s *userDb) GetByUsername(tenantID string, username string) (*dbmodel.User, err := s.db.Model(&dbmodel.User{}).Where("tenant_id = ? AND username = ? AND is_deleted = false", tenantID, username).Take(&r).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("user %s not found", username) + return nil, common.NewKeyNotExistError(fmt.Sprintf("%s/%s", tenantID, username)) } if err != nil { log.Error("get user by username failed", zap.String("tenant", tenantID), zap.String("username", username), zap.Error(err)) @@ -30,16 +32,16 @@ func (s *userDb) GetByUsername(tenantID string, username string) (*dbmodel.User, return r, nil } -func (s *userDb) ListUsername(tenantID string) ([]string, error) { - var usernames []string +func (s *userDb) ListUser(tenantID string) ([]*dbmodel.User, error) { + var users []*dbmodel.User - err := s.db.Model(&dbmodel.User{}).Select("username").Where("tenant_id = ? AND is_deleted = false", tenantID).Find(&usernames).Error + err := s.db.Model(&dbmodel.User{}).Where("tenant_id = ? AND is_deleted = false", tenantID).Find(&users).Error if err != nil { - log.Error("list usernames failed", zap.String("tenant", tenantID), zap.Error(err)) + log.Error("list user failed", zap.String("tenant", tenantID), zap.Error(err)) return nil, err } - return usernames, nil + return users, nil } func (s *userDb) Insert(in *dbmodel.User) error { diff --git a/internal/metastore/db/dao/user_role.go b/internal/metastore/db/dao/user_role.go new file mode 100644 index 000000000..20a6564d8 --- /dev/null +++ b/internal/metastore/db/dao/user_role.go @@ -0,0 +1,41 @@ +package dao + +import ( + "github.com/milvus-io/milvus/internal/log" + "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type userRoleDb struct { + db *gorm.DB +} + +func (u *userRoleDb) GetUserRoles(tenantID string, userID int64, roleID int64) ([]*dbmodel.UserRole, error) { + var ( + userRoles []*dbmodel.UserRole + err error + ) + err = u.db.Model(&dbmodel.UserRole{}).Where(&dbmodel.UserRole{UserID: userID, RoleID: roleID}).Where(dbmodel.GetCommonCondition(tenantID, false)).Preload("User").Preload("Role").Find(&userRoles).Error + if err != nil { + log.Error("fail to get user-roles", zap.String("tenant_id", tenantID), zap.Int64("userID", userID), zap.Int64("roleID", roleID), zap.Error(err)) + return nil, err + } + return userRoles, nil +} + +func (u *userRoleDb) Insert(in *dbmodel.UserRole) error { + err := u.db.Create(in).Error + if err != nil { + log.Error("fail to insert the user-role", zap.Any("in", in), zap.Error(err)) + } + return err +} + +func (u *userRoleDb) Delete(tenantID string, userID int64, roleID int64) error { + err := u.db.Model(dbmodel.UserRole{}).Where(&dbmodel.UserRole{UserID: userID, RoleID: roleID}).Where(dbmodel.GetCommonCondition(tenantID, false)).Update("is_deleted", true).Error + if err != nil { + log.Error("fail to delete the user-role", zap.String("tenant_id", tenantID), zap.Int64("userID", userID), zap.Int64("roleID", roleID), zap.Error(err)) + } + return err +} diff --git a/internal/metastore/db/dao/user_role_test.go b/internal/metastore/db/dao/user_role_test.go new file mode 100644 index 000000000..c99beeca4 --- /dev/null +++ b/internal/metastore/db/dao/user_role_test.go @@ -0,0 +1,193 @@ +package dao + +import ( + "errors" + "testing" + + "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestUserRole_GetUserRoles(t *testing.T) { + var ( + userID1 = 1 + userID2 = 2 + roleID1 = 10 + roleID2 = 20 + userRoles []*dbmodel.UserRole + getQuery func() *sqlmock.ExpectedQuery + err error + ) + + // mock user and role + getQuery = func() *sqlmock.ExpectedQuery { + return mock.ExpectQuery("SELECT * FROM `user_role` WHERE `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(false, tenantID) + } + getQuery().WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "user_id", "role_id"}). + AddRow(tenantID, userID1, roleID1). + AddRow(tenantID, userID2, roleID2)) + + mock.ExpectQuery("SELECT * FROM `role` WHERE `role`.`id` IN (?,?)"). + WithArgs(roleID1, roleID2). + WillReturnRows( + sqlmock.NewRows([]string{"id", "tenant_id", "name"}). + AddRow(roleID1, tenantID, "foo1"). + AddRow(roleID2, tenantID, "foo2")) + + mock.ExpectQuery("SELECT * FROM `credential_users` WHERE `credential_users`.`id` IN (?,?)"). + WithArgs(userID1, userID2). + WillReturnRows( + sqlmock.NewRows([]string{"id", "tenant_id", "username"}). + AddRow(userID1, tenantID, "fo1"). + AddRow(userID2, tenantID, "fo2")) + + userRoles, err = userRoleTestDb.GetUserRoles(tenantID, 0, 0) + assert.NoError(t, err) + assert.Equal(t, 2, len(userRoles)) + assert.Equal(t, "foo1", userRoles[0].Role.Name) + assert.Equal(t, "fo1", userRoles[0].User.Username) + + getQuery().WillReturnError(errors.New("test error")) + _, err = userRoleTestDb.GetUserRoles(tenantID, 0, 0) + assert.Error(t, err) +} + +func TestUserRole_GetUserRolesWithUserID(t *testing.T) { + var ( + userID1 = 1 + roleID1 = 10 + roleID2 = 20 + userRoles []*dbmodel.UserRole + err error + ) + + mock.ExpectQuery("SELECT * FROM `user_role` WHERE `user_role`.`user_id` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(userID1, false, tenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "user_id", "role_id"}). + AddRow(tenantID, userID1, roleID1). + AddRow(tenantID, userID1, roleID2)) + mock.ExpectQuery("SELECT * FROM `role` WHERE `role`.`id` IN (?,?)"). + WithArgs(roleID1, roleID2). + WillReturnRows( + sqlmock.NewRows([]string{"id", "tenant_id", "name"}). + AddRow(roleID1, tenantID, "foo1"). + AddRow(roleID2, tenantID, "foo2")) + mock.ExpectQuery("SELECT * FROM `credential_users` WHERE `credential_users`.`id` = ?"). + WithArgs(userID1). + WillReturnRows( + sqlmock.NewRows([]string{"id", "tenant_id", "username"}). + AddRow(userID1, tenantID, "fo1")) + + userRoles, err = userRoleTestDb.GetUserRoles(tenantID, int64(userID1), 0) + assert.NoError(t, err) + assert.Equal(t, 2, len(userRoles)) + assert.Equal(t, "foo2", userRoles[1].Role.Name) + assert.Equal(t, "fo1", userRoles[0].User.Username) +} + +func TestUserRole_GetUserRolesWithRoleID(t *testing.T) { + var ( + userID1 = 1 + userID2 = 2 + roleID1 = 10 + userRoles []*dbmodel.UserRole + err error + ) + + mock.ExpectQuery("SELECT * FROM `user_role` WHERE `user_role`.`role_id` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(roleID1, false, tenantID). + WillReturnRows( + sqlmock.NewRows([]string{"tenant_id", "user_id", "role_id"}). + AddRow(tenantID, userID1, roleID1). + AddRow(tenantID, userID2, roleID1)) + mock.ExpectQuery("SELECT * FROM `role` WHERE `role`.`id` = ?"). + WithArgs(roleID1). + WillReturnRows( + sqlmock.NewRows([]string{"id", "tenant_id", "name"}). + AddRow(roleID1, tenantID, "foo1")) + mock.ExpectQuery("SELECT * FROM `credential_users` WHERE `credential_users`.`id` IN (?,?)"). + WithArgs(userID1, userID2). + WillReturnRows( + sqlmock.NewRows([]string{"id", "tenant_id", "username"}). + AddRow(userID1, tenantID, "fo1"). + AddRow(userID2, tenantID, "fo2")) + + userRoles, err = userRoleTestDb.GetUserRoles(tenantID, 0, int64(roleID1)) + assert.NoError(t, err) + assert.Equal(t, 2, len(userRoles)) + assert.Equal(t, "foo1", userRoles[0].Role.Name) + assert.Equal(t, "fo2", userRoles[1].User.Username) +} + +func TestUserRole_Insert(t *testing.T) { + var ( + userRole *dbmodel.UserRole + err error + ) + userRole = &dbmodel.UserRole{ + Base: GetBase(), + UserID: 1, + RoleID: 1, + } + + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `user_role` (`tenant_id`,`is_deleted`,`created_at`,`updated_at`,`user_id`,`role_id`) VALUES (?,?,?,?,?,?)"). + WithArgs(userRole.TenantID, userRole.IsDeleted, userRole.CreatedAt, userRole.UpdatedAt, userRole.UserID, userRole.RoleID). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + err = userRoleTestDb.Insert(userRole) + assert.NoError(t, err) +} + +func TestUserRole_InsertError(t *testing.T) { + var ( + userRole *dbmodel.UserRole + err error + ) + userRole = &dbmodel.UserRole{ + Base: GetBase(), + UserID: 1, + RoleID: 1, + } + + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `user_role` (`tenant_id`,`is_deleted`,`created_at`,`updated_at`,`user_id`,`role_id`) VALUES (?,?,?,?,?,?)"). + WithArgs(userRole.TenantID, userRole.IsDeleted, userRole.CreatedAt, userRole.UpdatedAt, userRole.UserID, userRole.RoleID). + WillReturnError(errors.New("test error")) + mock.ExpectRollback() + err = userRoleTestDb.Insert(userRole) + assert.Error(t, err) +} + +func TestUserRole_Delete(t *testing.T) { + var ( + userRole *dbmodel.UserRole + getExec func() *sqlmock.ExpectedExec + err error + ) + userRole = &dbmodel.UserRole{ + Base: GetBase(), + UserID: 1, + RoleID: 1, + } + getExec = func() *sqlmock.ExpectedExec { + return mock.ExpectExec("UPDATE `user_role` SET `is_deleted`=?,`updated_at`=? WHERE `user_role`.`user_id` = ? AND `user_role`.`role_id` = ? AND `is_deleted` = ? AND `tenant_id` = ?"). + WithArgs(true, AnyTime{}, userRole.UserID, userRole.RoleID, userRole.IsDeleted, userRole.TenantID) + } + mock.ExpectBegin() + getExec().WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + err = userRoleTestDb.Delete(userRole.TenantID, userRole.UserID, userRole.RoleID) + assert.NoError(t, err) + + mock.ExpectBegin() + getExec().WillReturnError(errors.New("test error")) + mock.ExpectRollback() + err = userRoleTestDb.Delete(userRole.TenantID, userRole.UserID, userRole.RoleID) + assert.Error(t, err) +} diff --git a/internal/metastore/db/dao/user_test.go b/internal/metastore/db/dao/user_test.go index 5d11b05d9..134731f0c 100644 --- a/internal/metastore/db/dao/user_test.go +++ b/internal/metastore/db/dao/user_test.go @@ -62,33 +62,42 @@ func TestUser_GetByUsername_Error(t *testing.T) { } func TestUser_ListUsername(t *testing.T) { - var usernames = []string{ - "test_username_1", - "test_username_2", - } + var ( + usernames = []string{ + "test_username_1", + "test_username_2", + } + user = &dbmodel.User{ + TenantID: tenantID, + EncryptedPassword: "xxx", + IsSuper: false, + } + ) // expectation - mock.ExpectQuery("SELECT `username` FROM `credential_users` WHERE tenant_id = ? AND is_deleted = false"). + mock.ExpectQuery("SELECT * FROM `credential_users` WHERE tenant_id = ? AND is_deleted = false"). WithArgs(tenantID). WillReturnRows( - sqlmock.NewRows([]string{"username"}). - AddRow(usernames[0]). - AddRow(usernames[1])) + sqlmock.NewRows([]string{"tenant_id", "username", "encrypted_password", "is_super"}). + AddRow(user.TenantID, usernames[0], user.EncryptedPassword, user.IsSuper). + AddRow(user.TenantID, usernames[1], user.EncryptedPassword, user.IsSuper)) // actual - res, err := userTestDb.ListUsername(tenantID) + res, err := userTestDb.ListUser(tenantID) assert.Nil(t, err) - assert.Equal(t, usernames, res) + assert.Equal(t, 2, len(res)) + assert.Equal(t, usernames[0], res[0].Username) + assert.Equal(t, usernames[1], res[1].Username) } func TestUser_ListUsername_Error(t *testing.T) { // expectation - mock.ExpectQuery("SELECT `username` FROM `credential_users` WHERE tenant_id = ? AND is_deleted = false"). + mock.ExpectQuery("SELECT * FROM `credential_users` WHERE tenant_id = ? AND is_deleted = false"). WithArgs(tenantID). WillReturnError(errors.New("test error")) // actual - res, err := userTestDb.ListUsername(tenantID) + res, err := userTestDb.ListUser(tenantID) assert.Nil(t, res) assert.Error(t, err) } diff --git a/internal/metastore/db/dbmodel/base_model.go b/internal/metastore/db/dbmodel/base_model.go new file mode 100644 index 000000000..5ab3fa70b --- /dev/null +++ b/internal/metastore/db/dbmodel/base_model.go @@ -0,0 +1,11 @@ +package dbmodel + +import "time" + +type Base struct { + ID int64 `gorm:"id"` + TenantID string `gorm:"tenant_id"` + IsDeleted bool `gorm:"is_deleted"` + CreatedAt time.Time `gorm:"created_at"` + UpdatedAt time.Time `gorm:"updated_at"` +} diff --git a/internal/metastore/db/dbmodel/common.go b/internal/metastore/db/dbmodel/common.go index 14759db42..682abbd7c 100644 --- a/internal/metastore/db/dbmodel/common.go +++ b/internal/metastore/db/dbmodel/common.go @@ -12,8 +12,18 @@ type IMetaDomain interface { IndexDb(ctx context.Context) IIndexDb SegmentIndexDb(ctx context.Context) ISegmentIndexDb UserDb(ctx context.Context) IUserDb + RoleDb(ctx context.Context) IRoleDb + UserRoleDb(ctx context.Context) IUserRoleDb + GrantDb(ctx context.Context) IGrantDb } type ITransaction interface { Transaction(ctx context.Context, fn func(txCtx context.Context) error) error } + +func GetCommonCondition(tenant string, isDelete bool) map[string]interface{} { + return map[string]interface{}{ + "tenant_id": tenant, + "is_deleted": isDelete, + } +} diff --git a/internal/metastore/db/dbmodel/grant.go b/internal/metastore/db/dbmodel/grant.go new file mode 100644 index 000000000..db0b0f9af --- /dev/null +++ b/internal/metastore/db/dbmodel/grant.go @@ -0,0 +1,101 @@ +package dbmodel + +import ( + "encoding/json" + "fmt" + + "github.com/milvus-io/milvus/internal/common" +) + +type Grant struct { + Base + RoleID int64 `gorm:"role_id"` + Role Role `gorm:"foreignKey:RoleID"` + Object string `gorm:"object"` + ObjectName string `gorm:"object_name"` + Detail string `gorm:"detail"` +} + +func (g *Grant) TableName() string { + return "grant" +} + +//go:generate mockery --name=IGrantDb +type IGrantDb interface { + GetGrants(tenantID string, roleID int64, object string, objectName string) ([]*Grant, error) + Insert(in *Grant) error + Delete(tenantID string, roleID int64, object string, objectName string, privilege string) error +} + +func EncodeGrantDetail(detail string, grantor string, privilege string, isAdd bool) (string, error) { + var ( + grant = []string{grantor, privilege} + resBytes []byte + originGrants [][]string + index = -1 + err error + + handleGrant = func(grants [][]string) (string, error) { + if resBytes, err = json.Marshal(grants); err != nil { + return "", err + } + return string(resBytes), nil + } + ) + + if detail == "" { + if !isAdd { + return "", common.NewIgnorableError(fmt.Errorf("the empty detail can't be remove")) + } + return handleGrant(append(originGrants, grant)) + } + if originGrants, err = DecodeGrantDetail(detail); err != nil { + return "", err + } + + for i, origin := range originGrants { + if origin[1] == privilege { + index = i + break + } + } + if isAdd { + if index != -1 { + return detail, common.NewIgnorableError(fmt.Errorf("the grant[%s-%s] is existed", grantor, privilege)) + } + return handleGrant(append(originGrants, grant)) + } + if index == -1 { + return detail, common.NewIgnorableError(fmt.Errorf("the grant[%s-%s] isn't existed", grantor, privilege)) + } + if len(originGrants) == 1 { + return "", nil + } + return handleGrant(append(originGrants[:index], originGrants[index+1:]...)) +} + +func EncodeGrantDetailForString(originDetail string, operateDetail string, isAdd bool) (string, error) { + var ( + operateGrant [][]string + err error + ) + + if operateGrant, err = DecodeGrantDetail(operateDetail); err != nil { + return "", err + } + if len(operateGrant) != 1 || len(operateGrant[0]) != 2 { + return "", fmt.Errorf("invalid operateDetail: [%s], decode result: %+v", operateDetail, operateGrant) + } + return EncodeGrantDetail(originDetail, operateGrant[0][0], operateGrant[0][1], isAdd) +} + +func DecodeGrantDetail(detail string) ([][]string, error) { + var ( + grants [][]string + err error + ) + if err = json.Unmarshal([]byte(detail), &grants); err != nil { + return grants, err + } + return grants, nil +} diff --git a/internal/metastore/db/dbmodel/mocks/IGrantDb.go b/internal/metastore/db/dbmodel/mocks/IGrantDb.go new file mode 100644 index 000000000..861d923ba --- /dev/null +++ b/internal/metastore/db/dbmodel/mocks/IGrantDb.go @@ -0,0 +1,79 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + dbmodel "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" + mock "github.com/stretchr/testify/mock" +) + +// IGrantDb is an autogenerated mock type for the IGrantDb type +type IGrantDb struct { + mock.Mock +} + +// Delete provides a mock function with given fields: tenantID, roleID, object, objectName, privilege +func (_m *IGrantDb) Delete(tenantID string, roleID int64, object string, objectName string, privilege string) error { + ret := _m.Called(tenantID, roleID, object, objectName, privilege) + + var r0 error + if rf, ok := ret.Get(0).(func(string, int64, string, string, string) error); ok { + r0 = rf(tenantID, roleID, object, objectName, privilege) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetGrants provides a mock function with given fields: tenantID, roleID, object, objectName +func (_m *IGrantDb) GetGrants(tenantID string, roleID int64, object string, objectName string) ([]*dbmodel.Grant, error) { + ret := _m.Called(tenantID, roleID, object, objectName) + + var r0 []*dbmodel.Grant + if rf, ok := ret.Get(0).(func(string, int64, string, string) []*dbmodel.Grant); ok { + r0 = rf(tenantID, roleID, object, objectName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*dbmodel.Grant) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, int64, string, string) error); ok { + r1 = rf(tenantID, roleID, object, objectName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Insert provides a mock function with given fields: in +func (_m *IGrantDb) Insert(in *dbmodel.Grant) error { + ret := _m.Called(in) + + var r0 error + if rf, ok := ret.Get(0).(func(*dbmodel.Grant) error); ok { + r0 = rf(in) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewIGrantDb interface { + mock.TestingT + Cleanup(func()) +} + +// NewIGrantDb creates a new instance of IGrantDb. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewIGrantDb(t mockConstructorTestingTNewIGrantDb) *IGrantDb { + mock := &IGrantDb{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/metastore/db/dbmodel/mocks/IMetaDomain.go b/internal/metastore/db/dbmodel/mocks/IMetaDomain.go index c31b90863..3ca661a81 100644 --- a/internal/metastore/db/dbmodel/mocks/IMetaDomain.go +++ b/internal/metastore/db/dbmodel/mocks/IMetaDomain.go @@ -78,6 +78,22 @@ func (_m *IMetaDomain) FieldDb(ctx context.Context) dbmodel.IFieldDb { return r0 } +// GrantDb provides a mock function with given fields: ctx +func (_m *IMetaDomain) GrantDb(ctx context.Context) dbmodel.IGrantDb { + ret := _m.Called(ctx) + + var r0 dbmodel.IGrantDb + if rf, ok := ret.Get(0).(func(context.Context) dbmodel.IGrantDb); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(dbmodel.IGrantDb) + } + } + + return r0 +} + // IndexDb provides a mock function with given fields: ctx func (_m *IMetaDomain) IndexDb(ctx context.Context) dbmodel.IIndexDb { ret := _m.Called(ctx) @@ -110,6 +126,22 @@ func (_m *IMetaDomain) PartitionDb(ctx context.Context) dbmodel.IPartitionDb { return r0 } +// RoleDb provides a mock function with given fields: ctx +func (_m *IMetaDomain) RoleDb(ctx context.Context) dbmodel.IRoleDb { + ret := _m.Called(ctx) + + var r0 dbmodel.IRoleDb + if rf, ok := ret.Get(0).(func(context.Context) dbmodel.IRoleDb); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(dbmodel.IRoleDb) + } + } + + return r0 +} + // SegmentIndexDb provides a mock function with given fields: ctx func (_m *IMetaDomain) SegmentIndexDb(ctx context.Context) dbmodel.ISegmentIndexDb { ret := _m.Called(ctx) @@ -142,6 +174,22 @@ func (_m *IMetaDomain) UserDb(ctx context.Context) dbmodel.IUserDb { return r0 } +// UserRoleDb provides a mock function with given fields: ctx +func (_m *IMetaDomain) UserRoleDb(ctx context.Context) dbmodel.IUserRoleDb { + ret := _m.Called(ctx) + + var r0 dbmodel.IUserRoleDb + if rf, ok := ret.Get(0).(func(context.Context) dbmodel.IUserRoleDb); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(dbmodel.IUserRoleDb) + } + } + + return r0 +} + type mockConstructorTestingTNewIMetaDomain interface { mock.TestingT Cleanup(func()) diff --git a/internal/metastore/db/dbmodel/mocks/IRoleDb.go b/internal/metastore/db/dbmodel/mocks/IRoleDb.go new file mode 100644 index 000000000..8e441154a --- /dev/null +++ b/internal/metastore/db/dbmodel/mocks/IRoleDb.go @@ -0,0 +1,79 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + dbmodel "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" + mock "github.com/stretchr/testify/mock" +) + +// IRoleDb is an autogenerated mock type for the IRoleDb type +type IRoleDb struct { + mock.Mock +} + +// Delete provides a mock function with given fields: tenantID, name +func (_m *IRoleDb) Delete(tenantID string, name string) error { + ret := _m.Called(tenantID, name) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(tenantID, name) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetRoles provides a mock function with given fields: tenantID, name +func (_m *IRoleDb) GetRoles(tenantID string, name string) ([]*dbmodel.Role, error) { + ret := _m.Called(tenantID, name) + + var r0 []*dbmodel.Role + if rf, ok := ret.Get(0).(func(string, string) []*dbmodel.Role); ok { + r0 = rf(tenantID, name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*dbmodel.Role) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(tenantID, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Insert provides a mock function with given fields: in +func (_m *IRoleDb) Insert(in *dbmodel.Role) error { + ret := _m.Called(in) + + var r0 error + if rf, ok := ret.Get(0).(func(*dbmodel.Role) error); ok { + r0 = rf(in) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewIRoleDb interface { + mock.TestingT + Cleanup(func()) +} + +// NewIRoleDb creates a new instance of IRoleDb. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewIRoleDb(t mockConstructorTestingTNewIRoleDb) *IRoleDb { + mock := &IRoleDb{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/metastore/db/dbmodel/mocks/IUserDb.go b/internal/metastore/db/dbmodel/mocks/IUserDb.go index aa7a67b6a..56806a8d1 100644 --- a/internal/metastore/db/dbmodel/mocks/IUserDb.go +++ b/internal/metastore/db/dbmodel/mocks/IUserDb.go @@ -49,16 +49,16 @@ func (_m *IUserDb) Insert(in *dbmodel.User) error { return r0 } -// ListUsername provides a mock function with given fields: tenantID -func (_m *IUserDb) ListUsername(tenantID string) ([]string, error) { +// ListUser provides a mock function with given fields: tenantID +func (_m *IUserDb) ListUser(tenantID string) ([]*dbmodel.User, error) { ret := _m.Called(tenantID) - var r0 []string - if rf, ok := ret.Get(0).(func(string) []string); ok { + var r0 []*dbmodel.User + if rf, ok := ret.Get(0).(func(string) []*dbmodel.User); ok { r0 = rf(tenantID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) + r0 = ret.Get(0).([]*dbmodel.User) } } diff --git a/internal/metastore/db/dbmodel/mocks/IUserRoleDb.go b/internal/metastore/db/dbmodel/mocks/IUserRoleDb.go new file mode 100644 index 000000000..2612848ee --- /dev/null +++ b/internal/metastore/db/dbmodel/mocks/IUserRoleDb.go @@ -0,0 +1,79 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + dbmodel "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" + mock "github.com/stretchr/testify/mock" +) + +// IUserRoleDb is an autogenerated mock type for the IUserRoleDb type +type IUserRoleDb struct { + mock.Mock +} + +// Delete provides a mock function with given fields: tenantID, userID, roleID +func (_m *IUserRoleDb) Delete(tenantID string, userID int64, roleID int64) error { + ret := _m.Called(tenantID, userID, roleID) + + var r0 error + if rf, ok := ret.Get(0).(func(string, int64, int64) error); ok { + r0 = rf(tenantID, userID, roleID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetUserRoles provides a mock function with given fields: tenantID, userID, roleID +func (_m *IUserRoleDb) GetUserRoles(tenantID string, userID int64, roleID int64) ([]*dbmodel.UserRole, error) { + ret := _m.Called(tenantID, userID, roleID) + + var r0 []*dbmodel.UserRole + if rf, ok := ret.Get(0).(func(string, int64, int64) []*dbmodel.UserRole); ok { + r0 = rf(tenantID, userID, roleID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*dbmodel.UserRole) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, int64, int64) error); ok { + r1 = rf(tenantID, userID, roleID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Insert provides a mock function with given fields: in +func (_m *IUserRoleDb) Insert(in *dbmodel.UserRole) error { + ret := _m.Called(in) + + var r0 error + if rf, ok := ret.Get(0).(func(*dbmodel.UserRole) error); ok { + r0 = rf(in) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewIUserRoleDb interface { + mock.TestingT + Cleanup(func()) +} + +// NewIUserRoleDb creates a new instance of IUserRoleDb. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewIUserRoleDb(t mockConstructorTestingTNewIUserRoleDb) *IUserRoleDb { + mock := &IUserRoleDb{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/metastore/db/dbmodel/role.go b/internal/metastore/db/dbmodel/role.go new file mode 100644 index 000000000..38546c9ba --- /dev/null +++ b/internal/metastore/db/dbmodel/role.go @@ -0,0 +1,23 @@ +package dbmodel + +import "github.com/milvus-io/milvus/internal/proto/milvuspb" + +type Role struct { + Base + Name string `gorm:"name"` +} + +func (r *Role) TableName() string { + return "role" +} + +func (r *Role) Unmarshal() *milvuspb.RoleEntity { + return &milvuspb.RoleEntity{Name: r.Name} +} + +//go:generate mockery --name=IRoleDb +type IRoleDb interface { + GetRoles(tenantID string, name string) ([]*Role, error) + Insert(in *Role) error + Delete(tenantID string, name string) error +} diff --git a/internal/metastore/db/dbmodel/user.go b/internal/metastore/db/dbmodel/user.go index bdaec4c09..0ef0bb8ad 100644 --- a/internal/metastore/db/dbmodel/user.go +++ b/internal/metastore/db/dbmodel/user.go @@ -24,7 +24,7 @@ func (v User) TableName() string { //go:generate mockery --name=IUserDb type IUserDb interface { GetByUsername(tenantID string, username string) (*User, error) - ListUsername(tenantID string) ([]string, error) + ListUser(tenantID string) ([]*User, error) Insert(in *User) error MarkDeletedByUsername(tenantID string, username string) error } diff --git a/internal/metastore/db/dbmodel/user_role.go b/internal/metastore/db/dbmodel/user_role.go new file mode 100644 index 000000000..f8d536448 --- /dev/null +++ b/internal/metastore/db/dbmodel/user_role.go @@ -0,0 +1,21 @@ +package dbmodel + +type UserRole struct { + Base + UserID int64 `gorm:"user_id"` + RoleID int64 `gorm:"role_id"` + + User User `gorm:"foreignKey:UserID"` + Role Role `gorm:"foreignKey:RoleID"` +} + +func (u *UserRole) TableName() string { + return "user_role" +} + +//go:generate mockery --name=IUserRoleDb +type IUserRoleDb interface { + GetUserRoles(tenantID string, userID int64, roleID int64) ([]*UserRole, error) + Insert(in *UserRole) error + Delete(tenantID string, userID int64, roleID int64) error +} diff --git a/internal/metastore/db/rootcoord/table_catalog.go b/internal/metastore/db/rootcoord/table_catalog.go index bb40cfd7e..e55a51585 100644 --- a/internal/metastore/db/rootcoord/table_catalog.go +++ b/internal/metastore/db/rootcoord/table_catalog.go @@ -8,6 +8,10 @@ import ( "reflect" "runtime" + "github.com/milvus-io/milvus/internal/common" + + "github.com/milvus-io/milvus/internal/util" + "github.com/milvus-io/milvus/internal/log" "github.com/milvus-io/milvus/internal/metastore" "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" @@ -763,57 +767,292 @@ func (tc *Catalog) DropCredential(ctx context.Context, username string) error { func (tc *Catalog) ListCredentials(ctx context.Context) ([]string, error) { tenantID := contextutil.TenantID(ctx) - usernames, err := tc.metaDomain.UserDb(ctx).ListUsername(tenantID) + users, err := tc.metaDomain.UserDb(ctx).ListUser(tenantID) if err != nil { return nil, err } - + var usernames []string + for _, user := range users { + usernames = append(usernames, user.Username) + } return usernames, nil } func (tc *Catalog) CreateRole(ctx context.Context, tenant string, entity *milvuspb.RoleEntity) error { - //TODO implement me - return nil + var err error + if _, err = tc.GetRoleIDByName(ctx, tenant, entity.Name); err != nil && !common.IsKeyNotExistError(err) { + return err + } + if err == nil { + return common.NewIgnorableError(fmt.Errorf("the role[%s] has existed", entity.Name)) + } + return tc.metaDomain.RoleDb(ctx).Insert(&dbmodel.Role{ + Base: dbmodel.Base{TenantID: tenant}, + Name: entity.Name, + }) } func (tc *Catalog) DropRole(ctx context.Context, tenant string, roleName string) error { - //TODO implement me - return nil + return tc.metaDomain.RoleDb(ctx).Delete(tenant, roleName) } -func (tc *Catalog) OperateUserRole(ctx context.Context, tenant string, userEntity *milvuspb.UserEntity, roleEntity *milvuspb.RoleEntity, operateType milvuspb.OperateUserRoleType) error { - //TODO implement me - return nil +func (tc *Catalog) GetRoleIDByName(ctx context.Context, tenant string, name string) (int64, error) { + var ( + roles []*dbmodel.Role + err error + ) + + if roles, err = tc.metaDomain.RoleDb(ctx).GetRoles(tenant, name); err != nil { + return 0, err + } + if len(roles) < 1 { + return 0, common.NewKeyNotExistError(fmt.Sprintf("%s/%s", tenant, name)) + } + return roles[0].ID, nil } -func (tc *Catalog) SelectRole(ctx context.Context, tenant string, entity *milvuspb.RoleEntity, includeUserInfo bool) ([]*milvuspb.RoleResult, error) { - //TODO implement me - return nil, nil +func (tc *Catalog) AlterUserRole(ctx context.Context, tenant string, userEntity *milvuspb.UserEntity, roleEntity *milvuspb.RoleEntity, operateType milvuspb.OperateUserRoleType) error { + var ( + user *dbmodel.User + roleID int64 + userRole *dbmodel.UserRole + userRoles []*dbmodel.UserRole + err error + ) + if user, err = tc.metaDomain.UserDb(ctx).GetByUsername(tenant, userEntity.Name); err != nil { + log.Error("fail to get userID by the username", zap.String("username", userEntity.Name), zap.Error(err)) + return err + } + if roleID, err = tc.GetRoleIDByName(ctx, tenant, roleEntity.Name); err != nil { + log.Error("fail to get roleID by the role name", zap.String("role_name", roleEntity.Name), zap.Error(err)) + return err + } + userRole = &dbmodel.UserRole{Base: dbmodel.Base{TenantID: tenant}, UserID: user.ID, RoleID: roleID} + userRoles, err = tc.metaDomain.UserRoleDb(ctx).GetUserRoles(userRole.TenantID, userRole.UserID, userRole.RoleID) + if err != nil { + return err + } + switch operateType { + case milvuspb.OperateUserRoleType_AddUserToRole: + if len(userRoles) > 0 { + return common.NewIgnorableError(fmt.Errorf("the user-role[%s-%s] is existed", userEntity.Name, roleEntity.Name)) + } + return tc.metaDomain.UserRoleDb(ctx).Insert(userRole) + case milvuspb.OperateUserRoleType_RemoveUserFromRole: + if len(userRoles) < 1 { + return common.NewIgnorableError(fmt.Errorf("the user-role[%s-%s] isn't existed", userEntity.Name, roleEntity.Name)) + } + return tc.metaDomain.UserRoleDb(ctx).Delete(userRole.TenantID, userRole.UserID, userRole.RoleID) + default: + err = fmt.Errorf("invalid operate type: %d", operateType) + log.Error("error: ", zap.Error(err)) + return err + } } -func (tc *Catalog) SelectUser(ctx context.Context, tenant string, entity *milvuspb.UserEntity, includeRoleInfo bool) ([]*milvuspb.UserResult, error) { - //TODO implement me - return nil, nil +func (tc *Catalog) ListRole(ctx context.Context, tenant string, entity *milvuspb.RoleEntity, includeUserInfo bool) ([]*milvuspb.RoleResult, error) { + var ( + roleName string + roles []*dbmodel.Role + results []*milvuspb.RoleResult + err error + ) + if entity != nil { + roleName = entity.Name + } + roles, err = tc.metaDomain.RoleDb(ctx).GetRoles(tenant, roleName) + if err != nil { + return nil, err + } + for _, role := range roles { + var users []*milvuspb.UserEntity + var userRoles []*dbmodel.UserRole + if includeUserInfo { + if userRoles, err = tc.metaDomain.UserRoleDb(ctx).GetUserRoles(tenant, 0, role.ID); err != nil { + return nil, err + } + for _, userRole := range userRoles { + users = append(users, &milvuspb.UserEntity{Name: userRole.User.Username}) + } + } + results = append(results, &milvuspb.RoleResult{ + Role: role.Unmarshal(), + Users: users, + }) + } + if !funcutil.IsEmptyString(roleName) && len(results) == 0 { + return nil, common.NewKeyNotExistError(fmt.Sprintf("%s/%s", tenant, roleName)) + } + return results, nil } -func (tc *Catalog) OperatePrivilege(ctx context.Context, tenant string, entity *milvuspb.GrantEntity, operateType milvuspb.OperatePrivilegeType) error { - //TODO implement me - return nil +func (tc *Catalog) ListUser(ctx context.Context, tenant string, entity *milvuspb.UserEntity, includeRoleInfo bool) ([]*milvuspb.UserResult, error) { + var ( + users []*dbmodel.User + results []*milvuspb.UserResult + username string + err error + ) + if entity != nil { + var user *dbmodel.User + username = entity.Name + if user, err = tc.metaDomain.UserDb(ctx).GetByUsername(tenant, username); err != nil { + return nil, err + } + users = append(users, user) + } else { + if users, err = tc.metaDomain.UserDb(ctx).ListUser(tenant); err != nil { + return nil, err + } + } + for _, user := range users { + var roles []*milvuspb.RoleEntity + var userRoles []*dbmodel.UserRole + if includeRoleInfo { + if userRoles, err = tc.metaDomain.UserRoleDb(ctx).GetUserRoles(tenant, user.ID, 0); err != nil { + return nil, err + } + for _, userRole := range userRoles { + roles = append(roles, &milvuspb.RoleEntity{Name: userRole.Role.Name}) + } + } + results = append(results, &milvuspb.UserResult{ + User: &milvuspb.UserEntity{Name: user.Username}, + Roles: roles, + }) + } + return results, nil } -func (tc *Catalog) SelectGrant(ctx context.Context, tenant string, entity *milvuspb.GrantEntity) ([]*milvuspb.GrantEntity, error) { - //TODO implement me - return nil, nil +func (tc *Catalog) AlterGrant(ctx context.Context, tenant string, entity *milvuspb.GrantEntity, operateType milvuspb.OperatePrivilegeType) error { + var ( + roleID int64 + detail string + err error + ) + + if roleID, err = tc.GetRoleIDByName(ctx, tenant, entity.Role.Name); err != nil { + return err + } + switch operateType { + case milvuspb.OperatePrivilegeType_Revoke: + return tc.metaDomain.GrantDb(ctx). + Delete(tenant, roleID, entity.Object.Name, entity.ObjectName, entity.Grantor.Privilege.Name) + case milvuspb.OperatePrivilegeType_Grant: + if detail, err = dbmodel.EncodeGrantDetail("", entity.Grantor.User.Name, entity.Grantor.Privilege.Name, true); err != nil { + log.Error("fail to encode grant detail", zap.String("tenant", tenant), zap.Any("entity", entity), zap.Error(err)) + return err + } + return tc.metaDomain.GrantDb(ctx).Insert(&dbmodel.Grant{ + Base: dbmodel.Base{TenantID: tenant}, + RoleID: roleID, + Object: entity.Object.Name, + ObjectName: entity.ObjectName, + Detail: detail, + }) + default: + err = fmt.Errorf("invalid operate type: %d", operateType) + log.Error("error: ", zap.Error(err)) + return err + } +} + +func (tc *Catalog) ListGrant(ctx context.Context, tenant string, entity *milvuspb.GrantEntity) ([]*milvuspb.GrantEntity, error) { + var ( + roleID int64 + object string + objectName string + grants []*dbmodel.Grant + grantEntities []*milvuspb.GrantEntity + details [][]string + privilegeName string + err error + ) + if !funcutil.IsEmptyString(entity.ObjectName) && entity.Object != nil && !funcutil.IsEmptyString(entity.Object.Name) { + object = entity.Object.Name + objectName = entity.ObjectName + } + if roleID, err = tc.GetRoleIDByName(ctx, tenant, entity.Role.Name); err != nil { + log.Error("fail to get roleID by the role name", zap.String("role_name", entity.Role.Name), zap.Error(err)) + return nil, err + } + if grants, err = tc.metaDomain.GrantDb(ctx).GetGrants(tenant, roleID, object, objectName); err != nil { + return nil, err + } + for _, grant := range grants { + if details, err = dbmodel.DecodeGrantDetail(grant.Detail); err != nil { + log.Error("fail to decode grant detail", zap.Any("detail", grant.Detail), zap.Error(err)) + return nil, err + } + for _, detail := range details { + if len(detail) != 2 { + log.Error("invalid operateDetail", zap.Any("detail", detail)) + return nil, fmt.Errorf("invalid operateDetail: [%s], decode result: %+v", grant.Detail, details) + } + privilegeName = util.PrivilegeNameForAPI(detail[1]) + if detail[1] == util.AnyWord { + privilegeName = util.AnyWord + } + grantEntities = append(grantEntities, &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: grant.Role.Name}, + Object: &milvuspb.ObjectEntity{Name: grant.Object}, + ObjectName: grant.ObjectName, + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: detail[0]}, + Privilege: &milvuspb.PrivilegeEntity{Name: privilegeName}, + }, + }) + } + } + if !funcutil.IsEmptyString(object) && !funcutil.IsEmptyString(objectName) && len(grantEntities) == 0 { + return nil, common.NewKeyNotExistError(fmt.Sprintf("%s/%s/%s/%s", tenant, entity.Role.Name, object, objectName)) + } + return grantEntities, nil } func (tc *Catalog) ListPolicy(ctx context.Context, tenant string) ([]string, error) { - //TODO implement me - return nil, nil + var ( + grants []*dbmodel.Grant + details [][]string + policies []string + err error + ) + if grants, err = tc.metaDomain.GrantDb(ctx).GetGrants(tenant, 0, "", ""); err != nil { + return nil, err + } + for _, grant := range grants { + if details, err = dbmodel.DecodeGrantDetail(grant.Detail); err != nil { + log.Error("fail to decode grant detail", zap.Any("detail", grant.Detail), zap.Error(err)) + return nil, err + } + for _, detail := range details { + if len(detail) != 2 { + log.Error("invalid operateDetail", zap.String("tenant", tenant), zap.Strings("detail", detail)) + return nil, fmt.Errorf("invalid operateDetail: %+v", detail) + } + policies = append(policies, + funcutil.PolicyForPrivilege(grant.Role.Name, grant.Object, grant.ObjectName, detail[1])) + } + } + + return policies, nil } func (tc *Catalog) ListUserRole(ctx context.Context, tenant string) ([]string, error) { - //TODO implement me - return nil, nil + var ( + userRoleStrs []string + userRoles []*dbmodel.UserRole + err error + ) + + if userRoles, err = tc.metaDomain.UserRoleDb(ctx).GetUserRoles(tenant, 0, 0); err != nil { + return nil, err + } + for _, userRole := range userRoles { + userRoleStrs = append(userRoleStrs, funcutil.EncodeUserRoleCache(userRole.User.Username, userRole.Role.Name)) + } + + return userRoleStrs, nil } func (tc *Catalog) Close() { diff --git a/internal/metastore/db/rootcoord/table_catalog_test.go b/internal/metastore/db/rootcoord/table_catalog_test.go index ec92158c2..2b8b3e101 100644 --- a/internal/metastore/db/rootcoord/table_catalog_test.go +++ b/internal/metastore/db/rootcoord/table_catalog_test.go @@ -8,12 +8,15 @@ import ( "testing" "time" + "github.com/milvus-io/milvus/internal/common" + + "github.com/milvus-io/milvus/internal/proto/milvuspb" + "github.com/milvus-io/milvus/internal/metastore" "github.com/milvus-io/milvus/internal/metastore/db/dbmodel" "github.com/milvus-io/milvus/internal/metastore/db/dbmodel/mocks" "github.com/milvus-io/milvus/internal/metastore/model" "github.com/milvus-io/milvus/internal/proto/commonpb" - "github.com/milvus-io/milvus/internal/proto/milvuspb" "github.com/milvus-io/milvus/internal/proto/schemapb" "github.com/milvus-io/milvus/internal/util/contextutil" "github.com/milvus-io/milvus/internal/util/funcutil" @@ -52,6 +55,9 @@ var ( aliasDbMock *mocks.ICollAliasDb segIndexDbMock *mocks.ISegmentIndexDb userDbMock *mocks.IUserDb + roleDbMock *mocks.IRoleDb + userRoleDbMock *mocks.IUserRoleDb + grantDbMock *mocks.IGrantDb mockCatalog *Catalog ) @@ -68,6 +74,9 @@ func TestMain(m *testing.M) { aliasDbMock = &mocks.ICollAliasDb{} segIndexDbMock = &mocks.ISegmentIndexDb{} userDbMock = &mocks.IUserDb{} + roleDbMock = &mocks.IRoleDb{} + userRoleDbMock = &mocks.IUserRoleDb{} + grantDbMock = &mocks.IGrantDb{} metaDomainMock = &mocks.IMetaDomain{} metaDomainMock.On("CollectionDb", ctx).Return(collDbMock) @@ -78,6 +87,9 @@ func TestMain(m *testing.M) { metaDomainMock.On("CollAliasDb", ctx).Return(aliasDbMock) metaDomainMock.On("SegmentIndexDb", ctx).Return(segIndexDbMock) metaDomainMock.On("UserDb", ctx).Return(userDbMock) + metaDomainMock.On("RoleDb", ctx).Return(roleDbMock) + metaDomainMock.On("UserRoleDb", ctx).Return(userRoleDbMock) + metaDomainMock.On("GrantDb", ctx).Return(grantDbMock) mockCatalog = mockMetaCatalog(metaDomainMock) @@ -1348,21 +1360,24 @@ func TestTableCatalog_DropCredential_MarkUserDeletedError(t *testing.T) { } func TestTableCatalog_ListCredentials(t *testing.T) { - usernames := []string{username} + user := &dbmodel.User{ + Username: username, + EncryptedPassword: password, + } // expectation - userDbMock.On("ListUsername", tenantID).Return(usernames, nil).Once() + userDbMock.On("ListUser", tenantID).Return([]*dbmodel.User{user}, nil).Once() // actual res, gotErr := mockCatalog.ListCredentials(ctx) require.NoError(t, gotErr) - require.Equal(t, usernames, res) + require.Equal(t, []string{username}, res) } func TestTableCatalog_ListCredentials_SelectUsernamesError(t *testing.T) { // expectation errTest := errors.New("test error") - userDbMock.On("ListUsername", tenantID).Return(nil, errTest).Once() + userDbMock.On("ListUser", tenantID).Return(nil, errTest).Once() // actual res, gotErr := mockCatalog.ListCredentials(ctx) @@ -1371,55 +1386,715 @@ func TestTableCatalog_ListCredentials_SelectUsernamesError(t *testing.T) { } func TestTableCatalog_CreateRole(t *testing.T) { - //TODO implement me - gotErr := mockCatalog.CreateRole(ctx, tenantID, nil) - require.Nil(t, gotErr) + var ( + roleName = "foo" + err error + ) + + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{}, nil).Once() + roleDbMock.On("Insert", mock.Anything).Return(nil).Once() + err = mockCatalog.CreateRole(ctx, tenantID, &milvuspb.RoleEntity{Name: roleName}) + require.NoError(t, err) +} + +func TestTableCatalog_CreateRole_Error(t *testing.T) { + var ( + roleName = "foo" + err error + ) + roleDbMock.On("GetRoles", tenantID, roleName).Return(nil, errors.New("test error")).Once() + err = mockCatalog.CreateRole(ctx, tenantID, &milvuspb.RoleEntity{Name: roleName}) + require.Error(t, err) + + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}}}, nil).Once() + err = mockCatalog.CreateRole(ctx, tenantID, &milvuspb.RoleEntity{Name: roleName}) + require.Equal(t, true, common.IsIgnorableError(err)) + + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{}, nil).Once() + roleDbMock.On("Insert", mock.Anything).Return(errors.New("test error")).Once() + err = mockCatalog.CreateRole(ctx, tenantID, &milvuspb.RoleEntity{Name: roleName}) + require.Error(t, err) } func TestTableCatalog_DropRole(t *testing.T) { - //TODO implement me - gotErr := mockCatalog.DropRole(ctx, tenantID, "") - require.Nil(t, gotErr) + var ( + roleName = "foo" + err error + ) + + roleDbMock.On("Delete", tenantID, roleName).Return(nil).Once() + err = mockCatalog.DropRole(ctx, tenantID, roleName) + require.NoError(t, err) +} + +func TestTableCatalog_DropRole_Error(t *testing.T) { + var ( + roleName = "foo" + err error + ) + + roleDbMock.On("Delete", tenantID, roleName).Return(errors.New("test error")).Once() + err = mockCatalog.DropRole(ctx, tenantID, roleName) + require.Error(t, err) +} + +func TestTableCatalog_AlterUserRole(t *testing.T) { + var ( + username = "foo" + userID = 100 + roleName = "fo" + roleID = 10 + err error + ) + + userDbMock.On("GetByUsername", tenantID, username).Return(&dbmodel.User{ID: int64(userID)}, nil).Once() + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: int64(roleID)}}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(userID), int64(roleID)).Return([]*dbmodel.UserRole{}, nil).Once() + userRoleDbMock.On("Insert", mock.Anything).Return(nil).Once() + + err = mockCatalog.AlterUserRole(ctx, tenantID, &milvuspb.UserEntity{Name: username}, &milvuspb.RoleEntity{Name: roleName}, milvuspb.OperateUserRoleType_AddUserToRole) + require.NoError(t, err) +} + +func TestTableCatalog_AlterUserRole_GetUserIDError(t *testing.T) { + var ( + username = "foo" + roleName = "fo" + err error + ) + + userDbMock.On("GetByUsername", tenantID, username).Return(nil, errors.New("test error")).Once() + err = mockCatalog.AlterUserRole(ctx, tenantID, &milvuspb.UserEntity{Name: username}, &milvuspb.RoleEntity{Name: roleName}, milvuspb.OperateUserRoleType_AddUserToRole) + require.Error(t, err) +} + +func TestTableCatalog_AlterUserRole_GetRoleIDError(t *testing.T) { + var ( + username = "foo" + userID = 100 + roleName = "fo" + err error + ) + + userDbMock.On("GetByUsername", tenantID, username).Return(&dbmodel.User{ID: int64(userID)}, nil).Once() + roleDbMock.On("GetRoles", tenantID, roleName).Return(nil, errors.New("test error")).Once() + err = mockCatalog.AlterUserRole(ctx, tenantID, &milvuspb.UserEntity{Name: username}, &milvuspb.RoleEntity{Name: roleName}, milvuspb.OperateUserRoleType_AddUserToRole) + require.Error(t, err) +} + +func TestTableCatalog_AlterUserRole_GetUserRoleError(t *testing.T) { + var ( + username = "foo" + userID = 100 + roleName = "fo" + roleID = 10 + err error + ) + + userDbMock.On("GetByUsername", tenantID, username).Return(&dbmodel.User{ID: int64(userID)}, nil).Once() + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: int64(roleID)}}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(userID), int64(roleID)).Return([]*dbmodel.UserRole{}, errors.New("test error")).Once() + err = mockCatalog.AlterUserRole(ctx, tenantID, &milvuspb.UserEntity{Name: username}, &milvuspb.RoleEntity{Name: roleName}, milvuspb.OperateUserRoleType_AddUserToRole) + require.Error(t, err) +} + +func TestTableCatalog_AlterUserRole_RepeatUserRole(t *testing.T) { + var ( + username = "foo" + userID = 100 + roleName = "fo" + roleID = 10 + err error + ) + + userDbMock.On("GetByUsername", tenantID, username).Return(&dbmodel.User{ID: int64(userID)}, nil).Once() + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: int64(roleID)}}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(userID), int64(roleID)).Return([]*dbmodel.UserRole{{}}, nil).Once() + err = mockCatalog.AlterUserRole(ctx, tenantID, &milvuspb.UserEntity{Name: username}, &milvuspb.RoleEntity{Name: roleName}, milvuspb.OperateUserRoleType_AddUserToRole) + require.Error(t, err) + require.Equal(t, true, common.IsIgnorableError(err)) +} + +func TestTableCatalog_AlterUserRole_InsertError(t *testing.T) { + var ( + username = "foo" + userID = 100 + roleName = "fo" + roleID = 10 + err error + ) + + userDbMock.On("GetByUsername", tenantID, username).Return(&dbmodel.User{ID: int64(userID)}, nil).Once() + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: int64(roleID)}}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(userID), int64(roleID)).Return([]*dbmodel.UserRole{}, nil).Once() + userRoleDbMock.On("Insert", mock.Anything).Return(errors.New("test error")).Once() + + err = mockCatalog.AlterUserRole(ctx, tenantID, &milvuspb.UserEntity{Name: username}, &milvuspb.RoleEntity{Name: roleName}, milvuspb.OperateUserRoleType_AddUserToRole) + require.Error(t, err) +} + +func TestTableCatalog_AlterUserRole_Delete(t *testing.T) { + var ( + username = "foo" + userID = 100 + roleName = "fo" + roleID = 10 + err error + ) + + userDbMock.On("GetByUsername", tenantID, username).Return(&dbmodel.User{ID: int64(userID)}, nil).Once() + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: int64(roleID)}}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(userID), int64(roleID)).Return([]*dbmodel.UserRole{{}}, nil).Once() + userRoleDbMock.On("Delete", tenantID, int64(userID), int64(roleID)).Return(nil).Once() + + err = mockCatalog.AlterUserRole(ctx, tenantID, &milvuspb.UserEntity{Name: username}, &milvuspb.RoleEntity{Name: roleName}, milvuspb.OperateUserRoleType_RemoveUserFromRole) + require.NoError(t, err) +} + +func TestTableCatalog_AlterUserRole_DeleteError(t *testing.T) { + var ( + username = "foo" + userID = 100 + roleName = "fo" + roleID = 10 + err error + ) + + userDbMock.On("GetByUsername", tenantID, username).Return(&dbmodel.User{ID: int64(userID)}, nil).Once() + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: int64(roleID)}}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(userID), int64(roleID)).Return([]*dbmodel.UserRole{}, nil).Once() + + err = mockCatalog.AlterUserRole(ctx, tenantID, &milvuspb.UserEntity{Name: username}, &milvuspb.RoleEntity{Name: roleName}, milvuspb.OperateUserRoleType_RemoveUserFromRole) + require.Error(t, err) + require.True(t, common.IsIgnorableError(err)) + + userDbMock.On("GetByUsername", tenantID, username).Return(&dbmodel.User{ID: int64(userID)}, nil).Once() + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: int64(roleID)}}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(userID), int64(roleID)).Return([]*dbmodel.UserRole{{}}, nil).Once() + userRoleDbMock.On("Delete", tenantID, int64(userID), int64(roleID)).Return(errors.New("test error")).Once() + + err = mockCatalog.AlterUserRole(ctx, tenantID, &milvuspb.UserEntity{Name: username}, &milvuspb.RoleEntity{Name: roleName}, milvuspb.OperateUserRoleType_RemoveUserFromRole) + require.Error(t, err) +} + +func TestTableCatalog_AlterUserRole_InvalidType(t *testing.T) { + var ( + username = "foo" + userID = 100 + roleName = "fo" + roleID = 10 + err error + ) + + userDbMock.On("GetByUsername", tenantID, username).Return(&dbmodel.User{ID: int64(userID)}, nil).Once() + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: int64(roleID)}}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(userID), int64(roleID)).Return([]*dbmodel.UserRole{{}}, nil).Once() + + err = mockCatalog.AlterUserRole(ctx, tenantID, &milvuspb.UserEntity{Name: username}, &milvuspb.RoleEntity{Name: roleName}, 100) + require.Error(t, err) +} + +func TestTableCatalog_ListRole_AllRole(t *testing.T) { + var ( + roleName1 = "foo1" + roleName2 = "foo2" + result []*milvuspb.RoleResult + err error + ) + + roleDbMock.On("GetRoles", tenantID, "").Return([]*dbmodel.Role{{Name: roleName1}, {Name: roleName2}}, nil).Once() + + result, err = mockCatalog.ListRole(ctx, tenantID, nil, false) + require.NoError(t, err) + require.Equal(t, 2, len(result)) + require.Equal(t, roleName1, result[0].Role.Name) +} + +func TestTableCatalog_ListRole_AllRole_IncludeUserInfo(t *testing.T) { + var ( + roleName1 = "foo1" + username1 = "fo1" + username2 = "fo2" + roleName2 = "foo2" + result []*milvuspb.RoleResult + err error + ) + + roleDbMock.On("GetRoles", tenantID, "").Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}, Name: roleName1}, {Base: dbmodel.Base{ID: 10}, Name: roleName2}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(0), int64(1)).Return([]*dbmodel.UserRole{{User: dbmodel.User{Username: username1}}, {User: dbmodel.User{Username: username2}}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(0), int64(10)).Return([]*dbmodel.UserRole{}, nil).Once() + + result, err = mockCatalog.ListRole(ctx, tenantID, nil, true) + require.NoError(t, err) + require.Equal(t, 2, len(result)) + require.Equal(t, roleName1, result[0].Role.Name) + require.Equal(t, 2, len(result[0].Users)) + require.Equal(t, username1, result[0].Users[0].Name) + require.Equal(t, username2, result[0].Users[1].Name) + require.Equal(t, roleName2, result[1].Role.Name) +} + +func TestTableCatalog_ListRole_AllRole_Empty(t *testing.T) { + var ( + result []*milvuspb.RoleResult + err error + ) + + roleDbMock.On("GetRoles", tenantID, "").Return([]*dbmodel.Role{}, nil).Once() + + result, err = mockCatalog.ListRole(ctx, tenantID, nil, false) + require.NoError(t, err) + require.Equal(t, 0, len(result)) +} + +func TestTableCatalog_ListRole_OneRole(t *testing.T) { + var ( + roleName1 = "foo1" + result []*milvuspb.RoleResult + err error + ) + + roleDbMock.On("GetRoles", tenantID, roleName1).Return([]*dbmodel.Role{{Name: roleName1}}, nil).Once() + + result, err = mockCatalog.ListRole(ctx, tenantID, &milvuspb.RoleEntity{Name: roleName1}, false) + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.Equal(t, roleName1, result[0].Role.Name) +} + +func TestTableCatalog_ListRole_OneRole_IncludeUserInfo(t *testing.T) { + var ( + roleName1 = "foo1" + username1 = "fo1" + username2 = "fo2" + result []*milvuspb.RoleResult + err error + ) + + roleDbMock.On("GetRoles", tenantID, roleName1).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}, Name: roleName1}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(0), int64(1)).Return([]*dbmodel.UserRole{{User: dbmodel.User{Username: username1}}, {User: dbmodel.User{Username: username2}}}, nil).Once() + + result, err = mockCatalog.ListRole(ctx, tenantID, &milvuspb.RoleEntity{Name: roleName1}, true) + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.Equal(t, roleName1, result[0].Role.Name) + require.Equal(t, 2, len(result[0].Users)) + require.Equal(t, username1, result[0].Users[0].Name) + require.Equal(t, username2, result[0].Users[1].Name) +} + +func TestTableCatalog_ListRole_OneRole_Empty(t *testing.T) { + var ( + roleName = "foo" + err error + ) + + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{}, nil).Once() + + _, err = mockCatalog.ListRole(ctx, tenantID, &milvuspb.RoleEntity{Name: roleName}, false) + require.Error(t, err) +} + +func TestTableCatalog_ListRole_GetRolesError(t *testing.T) { + roleDbMock.On("GetRoles", tenantID, "").Return(nil, errors.New("test error")).Once() + + _, err := mockCatalog.ListRole(ctx, tenantID, nil, false) + require.Error(t, err) +} + +func TestTableCatalog_ListRole_GetUserRolesError(t *testing.T) { + roleDbMock.On("GetRoles", tenantID, "").Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}, Name: "foo"}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(0), int64(1)).Return(nil, errors.New("test error")).Once() + _, err := mockCatalog.ListRole(ctx, tenantID, nil, true) + require.Error(t, err) +} + +func TestTableCatalog_ListUser_AllUser(t *testing.T) { + var ( + username1 = "foo1" + username2 = "foo2" + result []*milvuspb.UserResult + err error + ) + + userDbMock.On("ListUser", tenantID).Return([]*dbmodel.User{{Username: username1}, {Username: username2}}, nil).Once() + + result, err = mockCatalog.ListUser(ctx, tenantID, nil, false) + require.NoError(t, err) + require.Equal(t, 2, len(result)) + require.Equal(t, username1, result[0].User.Name) +} + +func TestTableCatalog_ListUser_AllUser_IncludeRoleInfo(t *testing.T) { + var ( + roleName1 = "foo1" + username1 = "fo1" + username2 = "fo2" + roleName2 = "foo2" + result []*milvuspb.UserResult + err error + ) + + userDbMock.On("ListUser", tenantID).Return([]*dbmodel.User{{ID: 1, Username: username1}, {ID: 10, Username: username2}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(1), int64(0)).Return([]*dbmodel.UserRole{{Role: dbmodel.Role{Name: roleName1}}, {Role: dbmodel.Role{Name: roleName2}}}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(10), int64(0)).Return([]*dbmodel.UserRole{}, nil).Once() + + result, err = mockCatalog.ListUser(ctx, tenantID, nil, true) + require.NoError(t, err) + require.Equal(t, 2, len(result)) + require.Equal(t, username1, result[0].User.Name) + require.Equal(t, 2, len(result[0].Roles)) + require.Equal(t, roleName1, result[0].Roles[0].Name) + require.Equal(t, roleName2, result[0].Roles[1].Name) + require.Equal(t, username2, result[1].User.Name) } -func TestTableCatalog_OperateUserRole(t *testing.T) { - //TODO implement me - gotErr := mockCatalog.OperateUserRole(ctx, tenantID, nil, nil, milvuspb.OperateUserRoleType_AddUserToRole) - require.Nil(t, gotErr) +func TestTableCatalog_ListUser_AllUser_Empty(t *testing.T) { + var ( + result []*milvuspb.UserResult + err error + ) + + userDbMock.On("ListUser", tenantID).Return([]*dbmodel.User{}, nil).Once() + + result, err = mockCatalog.ListUser(ctx, tenantID, nil, false) + require.NoError(t, err) + require.Equal(t, 0, len(result)) } -func TestTableCatalog_SelectRole(t *testing.T) { - //TODO implement me - _, gotErr := mockCatalog.SelectRole(ctx, tenantID, nil, false) - require.Nil(t, gotErr) +func TestTableCatalog_ListUser_OneUser(t *testing.T) { + var ( + username1 = "foo1" + result []*milvuspb.UserResult + err error + ) + + userDbMock.On("GetByUsername", tenantID, username1).Return(&dbmodel.User{Username: username1}, nil).Once() + + result, err = mockCatalog.ListUser(ctx, tenantID, &milvuspb.UserEntity{Name: username1}, false) + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.Equal(t, username1, result[0].User.Name) } -func TestTableCatalog_SelectUser(t *testing.T) { - //TODO implement me - _, gotErr := mockCatalog.SelectUser(ctx, tenantID, nil, false) - require.Nil(t, gotErr) +func TestTableCatalog_ListUser_OneUser_IncludeRoleInfo(t *testing.T) { + var ( + roleName1 = "foo1" + roleName2 = "foo1" + username1 = "fo1" + result []*milvuspb.UserResult + err error + ) + + userDbMock.On("GetByUsername", tenantID, username1).Return(&dbmodel.User{ID: 1, Username: username1}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(1), int64(0)). + Return([]*dbmodel.UserRole{{Role: dbmodel.Role{Name: roleName1}}, {Role: dbmodel.Role{Name: roleName2}}}, nil).Once() + + result, err = mockCatalog.ListUser(ctx, tenantID, &milvuspb.UserEntity{Name: username1}, true) + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.Equal(t, username1, result[0].User.Name) + require.Equal(t, 2, len(result[0].Roles)) + require.Equal(t, roleName1, result[0].Roles[0].Name) + require.Equal(t, roleName2, result[0].Roles[1].Name) +} + +func TestTableCatalog_ListUser_ListUserError(t *testing.T) { + userDbMock.On("ListUser", tenantID).Return(nil, errors.New("test error")).Once() + + _, err := mockCatalog.ListUser(ctx, tenantID, nil, false) + require.Error(t, err) +} + +func TestTableCatalog_ListUser_GetByUsernameError(t *testing.T) { + var ( + username1 = "foo" + err error + ) + + userDbMock.On("GetByUsername", tenantID, username1).Return(nil, errors.New("test error")).Once() + + _, err = mockCatalog.ListUser(ctx, tenantID, &milvuspb.UserEntity{Name: username1}, false) + require.Error(t, err) } -func TestTableCatalog_OperatePrivilege(t *testing.T) { - //TODO implement me - gotErr := mockCatalog.OperatePrivilege(ctx, tenantID, nil, milvuspb.OperatePrivilegeType_Revoke) - require.Nil(t, gotErr) +func TestTableCatalog_ListUser_GetUserRolesError(t *testing.T) { + var ( + username1 = "foo" + err error + ) + + userDbMock.On("GetByUsername", tenantID, username1).Return(&dbmodel.User{ID: 1, Username: username1}, nil).Once() + userRoleDbMock.On("GetUserRoles", tenantID, int64(1), int64(0)).Return(nil, errors.New("test error")).Once() + + _, err = mockCatalog.ListUser(ctx, tenantID, &milvuspb.UserEntity{Name: username1}, true) + require.Error(t, err) +} + +func TestTableCatalog_AlterPrivilege_Revoke(t *testing.T) { + var ( + grant *milvuspb.GrantEntity + err error + ) + grant = &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: "foo"}, + Object: &milvuspb.ObjectEntity{Name: "Collection"}, + ObjectName: "col1", + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: "foo"}, + Privilege: &milvuspb.PrivilegeEntity{Name: "PrivilegeLoad"}, + }, + } + roleDbMock.On("GetRoles", tenantID, grant.Role.Name).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}}}, nil).Once() + grantDbMock.On("Delete", tenantID, int64(1), grant.Object.Name, grant.ObjectName, grant.Grantor.Privilege.Name).Return(nil).Once() + + err = mockCatalog.AlterGrant(ctx, tenantID, grant, milvuspb.OperatePrivilegeType_Revoke) + require.NoError(t, err) +} + +func TestTableCatalog_AlterPrivilege_Grant(t *testing.T) { + var ( + grant *milvuspb.GrantEntity + err error + ) + grant = &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: "foo"}, + Object: &milvuspb.ObjectEntity{Name: "Collection"}, + ObjectName: "col1", + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: "foo"}, + Privilege: &milvuspb.PrivilegeEntity{Name: "PrivilegeLoad"}, + }, + } + roleDbMock.On("GetRoles", tenantID, grant.Role.Name).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}}}, nil).Once() + grantDbMock.On("Insert", mock.Anything).Return(nil).Once() + + err = mockCatalog.AlterGrant(ctx, tenantID, grant, milvuspb.OperatePrivilegeType_Grant) + require.NoError(t, err) +} + +func TestTableCatalog_AlterPrivilege_InvalidType(t *testing.T) { + var ( + roleName = "foo" + err error + ) + + roleDbMock.On("GetRoles", tenantID, roleName).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}}}, nil).Once() + err = mockCatalog.AlterGrant(ctx, tenantID, &milvuspb.GrantEntity{Role: &milvuspb.RoleEntity{Name: roleName}}, 100) + require.Error(t, err) } -func TestTableCatalog_SelectGrant(t *testing.T) { - //TODO implement me - _, gotErr := mockCatalog.SelectGrant(ctx, tenantID, nil) - require.Nil(t, gotErr) +func TestTableCatalog_ListGrant(t *testing.T) { + var ( + grant *milvuspb.GrantEntity + grants []*dbmodel.Grant + entites []*milvuspb.GrantEntity + err error + ) + + grant = &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: "foo"}, + Object: &milvuspb.ObjectEntity{Name: "Collection"}, + ObjectName: "col1", + } + grants = []*dbmodel.Grant{ + { + Role: dbmodel.Role{Name: "foo"}, + Object: "Collection", + ObjectName: "col1", + Detail: "[[\"admin\",\"PrivilegeIndexDetail\"],[\"admin\",\"PrivilegeLoad\"],[\"admin\",\"*\"]]", + }, + } + roleDbMock.On("GetRoles", tenantID, grant.Role.Name).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}}}, nil).Once() + grantDbMock.On("GetGrants", tenantID, int64(1), grant.Object.Name, grant.ObjectName).Return(grants, nil).Once() + + entites, err = mockCatalog.ListGrant(ctx, tenantID, grant) + require.NoError(t, err) + require.Equal(t, 3, len(entites)) +} + +func TestTableCatalog_ListGrant_GetRolesError(t *testing.T) { + var ( + grant *milvuspb.GrantEntity + err error + ) + grant = &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: "foo"}, + Object: &milvuspb.ObjectEntity{Name: "Collection"}, + ObjectName: "col1", + } + roleDbMock.On("GetRoles", tenantID, grant.Role.Name).Return(nil, errors.New("test error")).Once() + _, err = mockCatalog.ListGrant(ctx, tenantID, grant) + require.Error(t, err) +} + +func TestTableCatalog_ListGrant_GetGrantError(t *testing.T) { + var ( + grant *milvuspb.GrantEntity + err error + ) + grant = &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: "foo"}, + Object: &milvuspb.ObjectEntity{Name: "Collection"}, + ObjectName: "col1", + } + roleDbMock.On("GetRoles", tenantID, grant.Role.Name).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}}}, nil).Once() + grantDbMock.On("GetGrants", tenantID, int64(1), grant.Object.Name, grant.ObjectName).Return(nil, errors.New("test error")).Once() + _, err = mockCatalog.ListGrant(ctx, tenantID, grant) + require.Error(t, err) +} + +func TestTableCatalog_ListGrant_DecodeError(t *testing.T) { + var ( + grant *milvuspb.GrantEntity + grants []*dbmodel.Grant + err error + ) + grant = &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: "foo"}, + Object: &milvuspb.ObjectEntity{Name: "Collection"}, + ObjectName: "col1", + } + grants = []*dbmodel.Grant{ + { + Role: dbmodel.Role{Name: "foo"}, + Object: "Collection", + ObjectName: "col1", + Detail: "decode error", + }, + } + roleDbMock.On("GetRoles", tenantID, grant.Role.Name).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}}}, nil).Once() + grantDbMock.On("GetGrants", tenantID, int64(1), grant.Object.Name, grant.ObjectName).Return(grants, nil).Once() + + _, err = mockCatalog.ListGrant(ctx, tenantID, grant) + require.Error(t, err) +} + +func TestTableCatalog_ListGrant_DetailLenError(t *testing.T) { + var ( + grant *milvuspb.GrantEntity + grants []*dbmodel.Grant + err error + ) + grant = &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: "foo"}, + Object: &milvuspb.ObjectEntity{Name: "Collection"}, + ObjectName: "col1", + } + grants = []*dbmodel.Grant{ + { + Role: dbmodel.Role{Name: "foo"}, + Object: "Collection", + ObjectName: "col1", + Detail: "[[\"admin\"]]", + }, + } + roleDbMock.On("GetRoles", tenantID, grant.Role.Name).Return([]*dbmodel.Role{{Base: dbmodel.Base{ID: 1}}}, nil).Once() + grantDbMock.On("GetGrants", tenantID, int64(1), grant.Object.Name, grant.ObjectName).Return(grants, nil).Once() + + _, err = mockCatalog.ListGrant(ctx, tenantID, grant) + require.Error(t, err) } func TestTableCatalog_ListPolicy(t *testing.T) { - //TODO implement me - _, gotErr := mockCatalog.ListPolicy(ctx, tenantID) - require.Nil(t, gotErr) + var ( + grant *milvuspb.GrantEntity + grants []*dbmodel.Grant + policies []string + err error + ) + + grant = &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: "foo"}, + Object: &milvuspb.ObjectEntity{Name: "Collection"}, + ObjectName: "col1", + } + grants = []*dbmodel.Grant{ + { + Role: dbmodel.Role{Name: "foo"}, + Object: "Collection", + ObjectName: "col1", + Detail: "[[\"admin\",\"PrivilegeIndexDetail\"]]", + }, + } + grantDbMock.On("GetGrants", tenantID, int64(0), "", "").Return(grants, nil).Once() + + policies, err = mockCatalog.ListPolicy(ctx, tenantID) + require.NoError(t, err) + require.Equal(t, 1, len(policies)) + require.Equal(t, funcutil.PolicyForPrivilege(grant.Role.Name, grant.Object.Name, grant.ObjectName, "PrivilegeIndexDetail"), policies[0]) +} + +func TestTableCatalog_ListPolicy_GetGrantsError(t *testing.T) { + grantDbMock.On("GetGrants", tenantID, int64(0), "", "").Return(nil, errors.New("test error")).Once() + + _, err := mockCatalog.ListPolicy(ctx, tenantID) + require.Error(t, err) +} + +func TestTableCatalog_ListPolicy_DetailLenError(t *testing.T) { + var ( + grants []*dbmodel.Grant + err error + ) + + grants = []*dbmodel.Grant{ + { + Role: dbmodel.Role{Name: "foo"}, + Object: "Collection", + ObjectName: "col1", + Detail: "decode error", + }, + } + grantDbMock.On("GetGrants", tenantID, int64(0), "", "").Return(grants, nil).Once() + + _, err = mockCatalog.ListPolicy(ctx, tenantID) + require.Error(t, err) +} + +func TestTableCatalog_ListPolicy_DecodeError(t *testing.T) { + var ( + grants []*dbmodel.Grant + err error + ) + + grants = []*dbmodel.Grant{ + { + Role: dbmodel.Role{Name: "foo"}, + Object: "Collection", + ObjectName: "col1", + Detail: "[[\"admin\"]]", + }, + } + grantDbMock.On("GetGrants", tenantID, int64(0), "", "").Return(grants, nil).Once() + + _, err = mockCatalog.ListPolicy(ctx, tenantID) + require.Error(t, err) } func TestTableCatalog_ListUserRole(t *testing.T) { - //TODO implement me - _, gotErr := mockCatalog.ListUserRole(ctx, tenantID) - require.Nil(t, gotErr) + var ( + username1 = "foo1" + username2 = "foo2" + roleName1 = "fo1" + roleName2 = "fo2" + userRoles []string + err error + ) + + userRoleDbMock.On("GetUserRoles", tenantID, int64(0), int64(0)).Return([]*dbmodel.UserRole{ + {User: dbmodel.User{Username: username1}, Role: dbmodel.Role{Name: roleName1}}, + {User: dbmodel.User{Username: username2}, Role: dbmodel.Role{Name: roleName2}}, + }, nil).Once() + + userRoles, err = mockCatalog.ListUserRole(ctx, tenantID) + require.NoError(t, err) + require.Equal(t, 2, len(userRoles)) + require.Equal(t, funcutil.EncodeUserRoleCache(username1, roleName1), userRoles[0]) +} + +func TestTableCatalog_ListUserRole_Error(t *testing.T) { + userRoleDbMock.On("GetUserRoles", tenantID, int64(0), int64(0)).Return(nil, errors.New("test error")).Once() + _, err := mockCatalog.ListUserRole(ctx, tenantID) + require.Error(t, err) } diff --git a/internal/metastore/kv/rootcoord/kv_catalog.go b/internal/metastore/kv/rootcoord/kv_catalog.go index 05822cee2..6f8804f9f 100644 --- a/internal/metastore/kv/rootcoord/kv_catalog.go +++ b/internal/metastore/kv/rootcoord/kv_catalog.go @@ -784,14 +784,35 @@ func (kc *Catalog) ListCredentials(ctx context.Context) ([]string, error) { return usernames, nil } +func (kc *Catalog) save(k string) error { + var err error + if _, err = kc.Txn.Load(k); err != nil && !common.IsKeyNotExistError(err) { + return err + } + if err == nil { + return common.NewIgnorableError(fmt.Errorf("the key[%s] is existed", k)) + } + return kc.Txn.Save(k, "") +} + +func (kc *Catalog) remove(k string) error { + var err error + if _, err = kc.Txn.Load(k); err != nil { + return err + } + if common.IsKeyNotExistError(err) { + return common.NewIgnorableError(fmt.Errorf("the key[%s] isn't existed", k)) + } + return kc.Txn.Remove(k) +} + func (kc *Catalog) CreateRole(ctx context.Context, tenant string, entity *milvuspb.RoleEntity) error { k := funcutil.HandleTenantForEtcdKey(RolePrefix, tenant, entity.Name) - err := kc.Txn.Save(k, "") + err := kc.save(k) if err != nil { - log.Error("fail to create role", zap.String("key", k), zap.Error(err)) - return err + log.Error("fail to save the role", zap.String("key", k), zap.Error(err)) } - return nil + return err } func (kc *Catalog) DropRole(ctx context.Context, tenant string, roleName string) error { @@ -804,18 +825,18 @@ func (kc *Catalog) DropRole(ctx context.Context, tenant string, roleName string) return nil } -func (kc *Catalog) OperateUserRole(ctx context.Context, tenant string, userEntity *milvuspb.UserEntity, roleEntity *milvuspb.RoleEntity, operateType milvuspb.OperateUserRoleType) error { +func (kc *Catalog) AlterUserRole(ctx context.Context, tenant string, userEntity *milvuspb.UserEntity, roleEntity *milvuspb.RoleEntity, operateType milvuspb.OperateUserRoleType) error { k := funcutil.HandleTenantForEtcdKey(RoleMappingPrefix, tenant, fmt.Sprintf("%s/%s", userEntity.Name, roleEntity.Name)) var err error if operateType == milvuspb.OperateUserRoleType_AddUserToRole { - err = kc.Txn.Save(k, "") + err = kc.save(k) if err != nil { - log.Error("fail to add user to role", zap.String("key", k), zap.Error(err)) + log.Error("fail to save the user-role", zap.String("key", k), zap.Error(err)) } } else if operateType == milvuspb.OperateUserRoleType_RemoveUserFromRole { - err = kc.Txn.Remove(k) + err = kc.remove(k) if err != nil { - log.Error("fail to remove user from role", zap.String("key", k), zap.Error(err)) + log.Error("fail to remove the user-role", zap.String("key", k), zap.Error(err)) } } else { err = fmt.Errorf("invalid operate user role type, operate type: %d", operateType) @@ -823,7 +844,7 @@ func (kc *Catalog) OperateUserRole(ctx context.Context, tenant string, userEntit return err } -func (kc *Catalog) SelectRole(ctx context.Context, tenant string, entity *milvuspb.RoleEntity, includeUserInfo bool) ([]*milvuspb.RoleResult, error) { +func (kc *Catalog) ListRole(ctx context.Context, tenant string, entity *milvuspb.RoleEntity, includeUserInfo bool) ([]*milvuspb.RoleResult, error) { var results []*milvuspb.RoleResult roleToUsers := make(map[string][]string) @@ -927,7 +948,7 @@ func (kc *Catalog) getUserResult(tenant string, username string, includeRoleInfo return result, nil } -func (kc *Catalog) SelectUser(ctx context.Context, tenant string, entity *milvuspb.UserEntity, includeRoleInfo bool) ([]*milvuspb.UserResult, error) { +func (kc *Catalog) ListUser(ctx context.Context, tenant string, entity *milvuspb.UserEntity, includeRoleInfo bool) ([]*milvuspb.UserResult, error) { var ( usernames []string err error @@ -967,7 +988,7 @@ func (kc *Catalog) SelectUser(ctx context.Context, tenant string, entity *milvus return results, nil } -func (kc *Catalog) OperatePrivilege(ctx context.Context, tenant string, entity *milvuspb.GrantEntity, operateType milvuspb.OperatePrivilegeType) error { +func (kc *Catalog) AlterGrant(ctx context.Context, tenant string, entity *milvuspb.GrantEntity, operateType milvuspb.OperatePrivilegeType) error { privilegeName := entity.Grantor.Privilege.Name k := funcutil.HandleTenantForEtcdKey(GranteePrefix, tenant, fmt.Sprintf("%s/%s/%s", entity.Role.Name, entity.Object.Name, entity.ObjectName)) @@ -976,6 +997,9 @@ func (kc *Catalog) OperatePrivilege(ctx context.Context, tenant string, entity * if err != nil { log.Warn("fail to load grant privilege entity", zap.String("key", k), zap.Any("type", operateType), zap.Error(err)) if funcutil.IsRevoke(operateType) { + if common.IsKeyNotExistError(err) { + return common.NewIgnorableError(fmt.Errorf("the grant[%s] isn't existed", k)) + } return err } if !common.IsKeyNotExistError(err) { @@ -1007,9 +1031,9 @@ func (kc *Catalog) OperatePrivilege(ctx context.Context, tenant string, entity * User: &milvuspb.UserEntity{Name: entity.Grantor.User.Name}, }) } else if isExisted && funcutil.IsGrant(operateType) { - return nil + return common.NewIgnorableError(fmt.Errorf("the privilege[%s] is granted", privilegeName)) } else if !isExisted && funcutil.IsRevoke(operateType) { - return fmt.Errorf("fail to revoke the privilege because the privilege isn't granted for the role, key: /%s", k) + return common.NewIgnorableError(fmt.Errorf("the privilege[%s] isn't granted", privilegeName)) } else if isExisted && funcutil.IsRevoke(operateType) { curGrantPrivilegeEntity.Entities = append(curGrantPrivilegeEntity.Entities[:dropIndex], curGrantPrivilegeEntity.Entities[dropIndex+1:]...) } @@ -1037,7 +1061,7 @@ func (kc *Catalog) OperatePrivilege(ctx context.Context, tenant string, entity * return nil } -func (kc *Catalog) SelectGrant(ctx context.Context, tenant string, entity *milvuspb.GrantEntity) ([]*milvuspb.GrantEntity, error) { +func (kc *Catalog) ListGrant(ctx context.Context, tenant string, entity *milvuspb.GrantEntity) ([]*milvuspb.GrantEntity, error) { var entities []*milvuspb.GrantEntity var k string diff --git a/internal/proxy/authentication_interceptor.go b/internal/proxy/authentication_interceptor.go index 262a1183b..7d394fbaf 100644 --- a/internal/proxy/authentication_interceptor.go +++ b/internal/proxy/authentication_interceptor.go @@ -4,11 +4,11 @@ import ( "context" "strings" + "google.golang.org/grpc/metadata" + "github.com/milvus-io/milvus/internal/util" "github.com/milvus-io/milvus/internal/util/crypto" - - "google.golang.org/grpc/metadata" ) // validAuth validates the authentication diff --git a/internal/rootcoord/meta_table.go b/internal/rootcoord/meta_table.go index dee1cc539..0b8da050a 100644 --- a/internal/rootcoord/meta_table.go +++ b/internal/rootcoord/meta_table.go @@ -1370,21 +1370,21 @@ func (mt *MetaTable) OperateUserRole(tenant string, userEntity *milvuspb.UserEnt return fmt.Errorf("role name in the role entity is empty") } - return mt.catalog.OperateUserRole(mt.ctx, tenant, userEntity, roleEntity, operateType) + return mt.catalog.AlterUserRole(mt.ctx, tenant, userEntity, roleEntity, operateType) } // SelectRole select role. // Enter the role condition by the entity param. And this param is nil, which means selecting all roles. // Get all users that are added to the role by setting the includeUserInfo param to true. func (mt *MetaTable) SelectRole(tenant string, entity *milvuspb.RoleEntity, includeUserInfo bool) ([]*milvuspb.RoleResult, error) { - return mt.catalog.SelectRole(mt.ctx, tenant, entity, includeUserInfo) + return mt.catalog.ListRole(mt.ctx, tenant, entity, includeUserInfo) } // SelectUser select user. // Enter the user condition by the entity param. And this param is nil, which means selecting all users. // Get all roles that are added the user to by setting the includeRoleInfo param to true. func (mt *MetaTable) SelectUser(tenant string, entity *milvuspb.UserEntity, includeRoleInfo bool) ([]*milvuspb.UserResult, error) { - return mt.catalog.SelectUser(mt.ctx, tenant, entity, includeRoleInfo) + return mt.catalog.ListUser(mt.ctx, tenant, entity, includeRoleInfo) } // OperatePrivilege grant or revoke privilege by setting the operateType param @@ -1411,7 +1411,7 @@ func (mt *MetaTable) OperatePrivilege(tenant string, entity *milvuspb.GrantEntit return fmt.Errorf("the operate type in the grant entity is invalid") } - return mt.catalog.OperatePrivilege(mt.ctx, tenant, entity, operateType) + return mt.catalog.AlterGrant(mt.ctx, tenant, entity, operateType) } // SelectGrant select grant @@ -1422,7 +1422,7 @@ func (mt *MetaTable) SelectGrant(tenant string, entity *milvuspb.GrantEntity) ([ if entity.Role == nil || funcutil.IsEmptyString(entity.Role.Name) { return entities, fmt.Errorf("the role entity in the grant entity is invalid") } - return mt.catalog.SelectGrant(mt.ctx, tenant, entity) + return mt.catalog.ListGrant(mt.ctx, tenant, entity) } func (mt *MetaTable) ListPolicy(tenant string) ([]string, error) { diff --git a/internal/rootcoord/meta_table_test.go b/internal/rootcoord/meta_table_test.go index 80375f0f2..df3fc966e 100644 --- a/internal/rootcoord/meta_table_test.go +++ b/internal/rootcoord/meta_table_test.go @@ -1087,9 +1087,24 @@ func TestRbacCreateRole(t *testing.T) { mockTxnKV.save = func(key, value string) error { return nil } + mockTxnKV.load = func(key string) (string, error) { + return "", common.NewKeyNotExistError(key) + } err = mt.CreateRole(util.DefaultTenant, &milvuspb.RoleEntity{Name: "role1"}) assert.Nil(t, err) + mockTxnKV.load = func(key string) (string, error) { + return "", fmt.Errorf("load error") + } + err = mt.CreateRole(util.DefaultTenant, &milvuspb.RoleEntity{Name: "role1"}) + assert.NotNil(t, err) + + mockTxnKV.load = func(key string) (string, error) { + return "", nil + } + err = mt.CreateRole(util.DefaultTenant, &milvuspb.RoleEntity{Name: "role1"}) + assert.Equal(t, true, common.IsIgnorableError(err)) + mockTxnKV.save = func(key, value string) error { return fmt.Errorf("save error") } @@ -1134,6 +1149,9 @@ func TestRbacOperateRole(t *testing.T) { err = mt.OperateUserRole(util.DefaultTenant, &milvuspb.UserEntity{Name: "user"}, &milvuspb.RoleEntity{Name: "role"}, milvuspb.OperateUserRoleType(100)) assert.NotNil(t, err) + mockTxnKV.load = func(key string) (string, error) { + return "", common.NewKeyNotExistError(key) + } err = mt.OperateUserRole(util.DefaultTenant, &milvuspb.UserEntity{Name: "user"}, &milvuspb.RoleEntity{Name: "role"}, milvuspb.OperateUserRoleType_AddUserToRole) assert.Nil(t, err) @@ -1146,6 +1164,9 @@ func TestRbacOperateRole(t *testing.T) { mockTxnKV.remove = func(key string) error { return nil } + mockTxnKV.load = func(key string) (string, error) { + return "", nil + } err = mt.OperateUserRole(util.DefaultTenant, &milvuspb.UserEntity{Name: "user"}, &milvuspb.RoleEntity{Name: "role"}, milvuspb.OperateUserRoleType_RemoveUserFromRole) assert.Nil(t, err) @@ -1369,11 +1390,10 @@ func TestRbacOperatePrivilege(t *testing.T) { return string(grantPrivilegeEntityByte), nil } err = mt.OperatePrivilege(util.DefaultTenant, entity, milvuspb.OperatePrivilegeType_Grant) - assert.Nil(t, err) + assert.Equal(t, true, common.IsIgnorableError(err)) entity.Grantor.Privilege = &milvuspb.PrivilegeEntity{Name: commonpb.ObjectPrivilege_PrivilegeRelease.String()} err = mt.OperatePrivilege(util.DefaultTenant, entity, milvuspb.OperatePrivilegeType_Revoke) - assert.NotNil(t, err) - entity.Grantor.Privilege = &milvuspb.PrivilegeEntity{Name: commonpb.ObjectPrivilege_PrivilegeLoad.String()} + assert.Equal(t, true, common.IsIgnorableError(err)) grantPrivilegeEntity = &milvuspb.GrantPrivilegeEntity{Entities: []*milvuspb.GrantorEntity{ {User: &milvuspb.UserEntity{Name: "user2"}, Privilege: &milvuspb.PrivilegeEntity{Name: commonpb.ObjectPrivilege_PrivilegeLoad.String()}}, }} @@ -1383,6 +1403,13 @@ func TestRbacOperatePrivilege(t *testing.T) { } err = mt.OperatePrivilege(util.DefaultTenant, entity, milvuspb.OperatePrivilegeType_Grant) assert.Nil(t, err) + grantPrivilegeEntity = &milvuspb.GrantPrivilegeEntity{Entities: []*milvuspb.GrantorEntity{ + {User: &milvuspb.UserEntity{Name: "user2"}, Privilege: &milvuspb.PrivilegeEntity{Name: commonpb.ObjectPrivilege_PrivilegeRelease.String()}}, + }} + mockTxnKV.load = func(key string) (string, error) { + grantPrivilegeEntityByte, _ := proto.Marshal(grantPrivilegeEntity) + return string(grantPrivilegeEntityByte), nil + } mockTxnKV.remove = func(key string) error { return fmt.Errorf("remove error") } diff --git a/internal/rootcoord/root_coord.go b/internal/rootcoord/root_coord.go index 17b9d32e6..0d8dbf7f0 100644 --- a/internal/rootcoord/root_coord.go +++ b/internal/rootcoord/root_coord.go @@ -1292,11 +1292,14 @@ func (c *Core) initData() error { func (c *Core) initRbac() (initError error) { // create default roles, including admin, public - if initError = c.MetaTable.CreateRole(util.DefaultTenant, &milvuspb.RoleEntity{Name: util.RoleAdmin}); initError != nil { - return - } - if initError = c.MetaTable.CreateRole(util.DefaultTenant, &milvuspb.RoleEntity{Name: util.RolePublic}); initError != nil { - return + for _, role := range util.DefaultRoles { + if initError = c.MetaTable.CreateRole(util.DefaultTenant, &milvuspb.RoleEntity{Name: role}); initError != nil { + if common.IsIgnorableError(initError) { + initError = nil + continue + } + return + } } // grant privileges for the public role @@ -1318,6 +1321,10 @@ func (c *Core) initRbac() (initError error) { Privilege: &milvuspb.PrivilegeEntity{Name: globalPrivilege}, }, }, milvuspb.OperatePrivilegeType_Grant); initError != nil { + if common.IsIgnorableError(initError) { + initError = nil + continue + } return } } @@ -1331,6 +1338,10 @@ func (c *Core) initRbac() (initError error) { Privilege: &milvuspb.PrivilegeEntity{Name: collectionPrivilege}, }, }, milvuspb.OperatePrivilegeType_Grant); initError != nil { + if common.IsIgnorableError(initError) { + initError = nil + continue + } return } } @@ -2973,14 +2984,6 @@ func (c *Core) CreateRole(ctx context.Context, in *milvuspb.CreateRoleRequest) ( return errorutil.UnhealthyStatus(code), errorutil.UnhealthyError() } entity := in.Entity - _, err := c.MetaTable.SelectRole(util.DefaultTenant, &milvuspb.RoleEntity{Name: entity.Name}, false) - if err == nil { - errMsg := "role already exists:" + entity.Name - return failStatus(commonpb.ErrorCode_CreateRoleFailure, errMsg), errors.New(errMsg) - } - if !common.IsKeyNotExistError(err) { - return failStatus(commonpb.ErrorCode_CreateRoleFailure, err.Error()), err - } results, err := c.MetaTable.SelectRole(util.DefaultTenant, nil, false) if err != nil { @@ -3045,6 +3048,9 @@ func (c *Core) DropRole(ctx context.Context, in *milvuspb.DropRoleRequest) (*com for _, roleResult := range roleResults { for index, userEntity := range roleResult.Users { if err = c.MetaTable.OperateUserRole(util.DefaultTenant, &milvuspb.UserEntity{Name: userEntity.Name}, &milvuspb.RoleEntity{Name: roleResult.Role.Name}, milvuspb.OperateUserRoleType_RemoveUserFromRole); err != nil { + if common.IsIgnorableError(err) { + continue + } errMsg := "fail to remove user from role" logger.Error(errMsg, zap.String("role_name", roleResult.Role.Name), zap.String("username", userEntity.Name), zap.Int("current_index", index), zap.Error(err)) return failStatus(commonpb.ErrorCode_OperateUserRoleFailure, errMsg), err @@ -3090,23 +3096,34 @@ func (c *Core) OperateUserRole(ctx context.Context, in *milvuspb.OperateUserRole logger.Error(errMsg, zap.String("username", in.Username), zap.Error(err)) return failStatus(commonpb.ErrorCode_OperateUserRoleFailure, errMsg), err } + updateCache := true if err := c.MetaTable.OperateUserRole(util.DefaultTenant, &milvuspb.UserEntity{Name: in.Username}, &milvuspb.RoleEntity{Name: in.RoleName}, in.Type); err != nil { - errMsg := "fail to operate user to role" - logger.Error(errMsg, zap.String("role_name", in.RoleName), zap.String("username", in.Username), zap.Error(err)) - return failStatus(commonpb.ErrorCode_OperateUserRoleFailure, errMsg), err + if !common.IsIgnorableError(err) { + errMsg := "fail to operate user to role" + logger.Error(errMsg, zap.String("role_name", in.RoleName), zap.String("username", in.Username), zap.Error(err)) + return failStatus(commonpb.ErrorCode_OperateUserRoleFailure, errMsg), err + } + updateCache = false } - var opType int32 - if in.Type == milvuspb.OperateUserRoleType_AddUserToRole { - opType = int32(typeutil.CacheAddUserToRole) - } else if in.Type == milvuspb.OperateUserRoleType_RemoveUserFromRole { - opType = int32(typeutil.CacheRemoveUserFromRole) - } - if err := c.proxyClientManager.RefreshPolicyInfoCache(ctx, &proxypb.RefreshPolicyInfoCacheRequest{ - OpType: opType, - OpKey: funcutil.EncodeUserRoleCache(in.Username, in.RoleName), - }); err != nil { - return failStatus(commonpb.ErrorCode_OperateUserRoleFailure, err.Error()), err + if updateCache { + var opType int32 + switch in.Type { + case milvuspb.OperateUserRoleType_AddUserToRole: + opType = int32(typeutil.CacheAddUserToRole) + case milvuspb.OperateUserRoleType_RemoveUserFromRole: + opType = int32(typeutil.CacheRemoveUserFromRole) + default: + errMsg := "invalid operate type for the OperateUserRole api" + logger.Error(errMsg, zap.Any("op_type", in.Type)) + return failStatus(commonpb.ErrorCode_OperateUserRoleFailure, errMsg), errors.New(errMsg) + } + if err := c.proxyClientManager.RefreshPolicyInfoCache(ctx, &proxypb.RefreshPolicyInfoCacheRequest{ + OpType: opType, + OpKey: funcutil.EncodeUserRoleCache(in.Username, in.RoleName), + }); err != nil { + return failStatus(commonpb.ErrorCode_OperateUserRoleFailure, err.Error()), err + } } logger.Debug(method + " success") @@ -3306,23 +3323,34 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile if in.Entity.Object.Name == commonpb.ObjectType_Global.String() { in.Entity.ObjectName = util.AnyWord } + updateCache := true if err := c.MetaTable.OperatePrivilege(util.DefaultTenant, in.Entity, in.Type); err != nil { - errMsg := "fail to operate the privilege" - logger.Error(errMsg, zap.Error(err)) - return failStatus(commonpb.ErrorCode_OperatePrivilegeFailure, errMsg), err + if !common.IsIgnorableError(err) { + errMsg := "fail to operate the privilege" + logger.Error(errMsg, zap.Error(err)) + return failStatus(commonpb.ErrorCode_OperatePrivilegeFailure, errMsg), err + } + updateCache = false } - var opType int32 - if in.Type == milvuspb.OperatePrivilegeType_Grant { - opType = int32(typeutil.CacheGrantPrivilege) - } else if in.Type == milvuspb.OperatePrivilegeType_Revoke { - opType = int32(typeutil.CacheRevokePrivilege) - } - if err := c.proxyClientManager.RefreshPolicyInfoCache(ctx, &proxypb.RefreshPolicyInfoCacheRequest{ - OpType: opType, - OpKey: funcutil.PolicyForPrivilege(in.Entity.Role.Name, in.Entity.Object.Name, in.Entity.ObjectName, in.Entity.Grantor.Privilege.Name), - }); err != nil { - return failStatus(commonpb.ErrorCode_OperatePrivilegeFailure, err.Error()), err + if updateCache { + var opType int32 + switch in.Type { + case milvuspb.OperatePrivilegeType_Grant: + opType = int32(typeutil.CacheGrantPrivilege) + case milvuspb.OperatePrivilegeType_Revoke: + opType = int32(typeutil.CacheRevokePrivilege) + default: + errMsg := "invalid operate type for the OperatePrivilege api" + logger.Error(errMsg, zap.Any("op_type", in.Type)) + return failStatus(commonpb.ErrorCode_OperatePrivilegeFailure, errMsg), errors.New(errMsg) + } + if err := c.proxyClientManager.RefreshPolicyInfoCache(ctx, &proxypb.RefreshPolicyInfoCacheRequest{ + OpType: opType, + OpKey: funcutil.PolicyForPrivilege(in.Entity.Role.Name, in.Entity.Object.Name, in.Entity.ObjectName, in.Entity.Grantor.Privilege.Name), + }); err != nil { + return failStatus(commonpb.ErrorCode_OperatePrivilegeFailure, err.Error()), err + } } logger.Debug(method + " success") @@ -3367,6 +3395,11 @@ func (c *Core) SelectGrant(ctx context.Context, in *milvuspb.SelectGrantRequest) } grantEntities, err := c.MetaTable.SelectGrant(util.DefaultTenant, in.Entity) + if common.IsKeyNotExistError(err) { + return &milvuspb.SelectGrantResponse{ + Status: succStatus(), + }, nil + } if err != nil { errMsg := "fail to select the grant" logger.Error(errMsg, zap.Error(err)) diff --git a/scripts/run_go_unittest.sh b/scripts/run_go_unittest.sh index bb026612c..597a771c4 100755 --- a/scripts/run_go_unittest.sh +++ b/scripts/run_go_unittest.sh @@ -60,5 +60,6 @@ go test -race -cover ${APPLE_SILICON_FLAG} "${MILVUS_DIR}/distributed/querynode/ go test -race -cover ${APPLE_SILICON_FLAG} "${MILVUS_DIR}/rootcoord" -failfast go test -race -cover ${APPLE_SILICON_FLAG} "${MILVUS_DIR}/datacoord/..." -failfast go test -race -cover ${APPLE_SILICON_FLAG} "${MILVUS_DIR}/indexcoord/..." -failfast +go test -race -cover ${APPLE_SILICON_FLAG} "${MILVUS_DIR}/metastore/..." -failfast echo " Go unittest finished" diff --git a/scripts/sql/meta.sql b/scripts/sql/meta.sql index e126c6818..d90afdb71 100644 --- a/scripts/sql/meta.sql +++ b/scripts/sql/meta.sql @@ -210,3 +210,43 @@ CREATE TABLE if not exists milvus_meta.credential_users ( PRIMARY KEY (id), INDEX idx_tenant_id_username (tenant_id, username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- role +CREATE TABLE if not exists milvus_meta.role ( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id VARCHAR(128) DEFAULT NULL, + name VARCHAR(128) NOT NULL, + is_deleted BOOL NOT NULL DEFAULT false, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP on update current_timestamp, + INDEX idx_role_tenant_name (tenant_id, name), + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- user-role +CREATE TABLE if not exists milvus_meta.user_role ( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id VARCHAR(128) DEFAULT NULL, + user_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + is_deleted BOOL NOT NULL DEFAULT false, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP on update current_timestamp, + INDEX idx_role_mapping_tenant_user_role (tenant_id, user_id, role_id), + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- grant +CREATE TABLE if not exists milvus_meta.grant ( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id VARCHAR(128) DEFAULT NULL, + role_id BIGINT NOT NULL, + object VARCHAR(128) NOT NULL, + object_name VARCHAR(128) NOT NULL, + detail TEXT NOT NULL, + is_deleted BOOL NOT NULL DEFAULT false, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP on update current_timestamp, + INDEX idx_grant_principal_resource_tenant (tenant_id, role_id, object, object_name), + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- GitLab