diff --git a/configs/milvus.yaml b/configs/milvus.yaml index de101742d6d5a..ca2291bc2da46 100644 --- a/configs/milvus.yaml +++ b/configs/milvus.yaml @@ -797,6 +797,30 @@ common: # like the old password verification when updating the credential superUsers: defaultRootPassword: Milvus # default password for root user + rbac: + overrideBuiltInPrivilgeGroups: + enabled: false # Whether to override build-in privilege groups + cluster: + readonly: + privileges: ListDatabases,SelectOwnership,SelectUser,DescribeResourceGroup,ListResourceGroups # Cluster level readonly privileges + readwrite: + privileges: ListDatabases,SelectOwnership,SelectUser,DescribeResourceGroup,ListResourceGroups,FlushAll,TransferNode,TransferReplica,UpdateResourceGroups # Cluster level readwrite privileges + admin: + privileges: ListDatabases,SelectOwnership,SelectUser,DescribeResourceGroup,ListResourceGroups,FlushAll,TransferNode,TransferReplica,UpdateResourceGroups,BackupRBAC,RestoreRBAC,CreateDatabase,DropDatabase,CreateOwnership,DropOwnership,ManageOwnership,CreateResourceGroup,DropResourceGroup,UpdateUser # Cluster level admin privileges + database: + readonly: + privileges: ShowCollections,DescribeDatabase # Database level readonly privileges + readwrite: + privileges: ShowCollections,DescribeDatabase,AlterDatabase # Database level readwrite privileges + admin: + privileges: ShowCollections,DescribeDatabase,AlterDatabase,CreateCollection,DropCollection # Database level admin privileges + collection: + readonly: + privileges: Query,Search,IndexDetail,GetFlushState,GetLoadState,GetLoadingProgress,HasPartition,ShowPartitions,DescribeCollection,DescribeAlias,GetStatistics,ListAliases # Collection level readonly privileges + readwrite: + privileges: Query,Search,IndexDetail,GetFlushState,GetLoadState,GetLoadingProgress,HasPartition,ShowPartitions,DescribeCollection,DescribeAlias,GetStatistics,ListAliases,Load,Release,Insert,Delete,Upsert,Import,Flush,Compaction,LoadBalance,RenameCollection,CreateIndex,DropIndex,CreatePartition,DropPartition # Collection level readwrite privileges + admin: + privileges: Query,Search,IndexDetail,GetFlushState,GetLoadState,GetLoadingProgress,HasPartition,ShowPartitions,DescribeCollection,DescribeAlias,GetStatistics,ListAliases,Load,Release,Insert,Delete,Upsert,Import,Flush,Compaction,LoadBalance,RenameCollection,CreateIndex,DropIndex,CreatePartition,DropPartition,CreateAlias,DropAlias # Collection level admin privileges tlsMode: 0 session: ttl: 30 # ttl value when session granting a lease to register service diff --git a/go.mod b/go.mod index f136b2dfd3af4..a5b4e214a1c99 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/klauspost/compress v1.17.9 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d - github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17 + github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1 github.com/minio/minio-go/v7 v7.0.73 github.com/pingcap/log v1.1.1-0.20221015072633-39906604fb81 github.com/prometheus/client_golang v1.14.0 diff --git a/go.sum b/go.sum index 46fc79a0f13b4..cf279bd693c66 100644 --- a/go.sum +++ b/go.sum @@ -608,8 +608,8 @@ github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119 h1:9VXijWu github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119/go.mod h1:DvXTE/K/RtHehxU8/GtDs4vFtfw64jJ3PaCnFri8CRg= github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b h1:TfeY0NxYxZzUfIfYe5qYDBzt4ZYRqzUjTR6CvUzjat8= github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b/go.mod h1:iwW+9cWfIzzDseEBCCeDSN5SD16Tidvy8cwQ7ZY8Qj4= -github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17 h1:ANkXdUKKpIPPQkw9pkV9ku9AEtSaPyua9XzdMTUxjCs= -github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= +github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1 h1:Xp4zOR85XFFtM7Eif945BeSmDf30hbdijbeNSuy92Bg= +github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= github.com/milvus-io/milvus-storage/go v0.0.0-20231227072638-ebd0b8e56d70 h1:Z+sp64fmAOxAG7mU0dfVOXvAXlwRB0c8a96rIM5HevI= github.com/milvus-io/milvus-storage/go v0.0.0-20231227072638-ebd0b8e56d70/go.mod h1:GPETMcTZq1gLY1WA6Na5kiNAKnq8SEMMiVKUZrM3sho= github.com/milvus-io/pulsar-client-go v0.6.10 h1:eqpJjU+/QX0iIhEo3nhOqMNXL+TyInAs1IAHZCrCM/A= diff --git a/internal/distributed/proxy/httpserver/constant.go b/internal/distributed/proxy/httpserver/constant.go index 4d4a3f8fcb2cb..ef39d46ef7c1f 100644 --- a/internal/distributed/proxy/httpserver/constant.go +++ b/internal/distributed/proxy/httpserver/constant.go @@ -43,6 +43,8 @@ const ( RevokeRoleAction = "revoke_role" GrantPrivilegeAction = "grant_privilege" RevokePrivilegeAction = "revoke_privilege" + GrantPrivilegeActionV2 = "grant_privilege_v2" + RevokePrivilegeActionV2 = "revoke_privilege_v2" AlterAction = "alter" GetProgressAction = "get_progress" // deprecated, keep it for compatibility, use `/v2/vectordb/jobs/import/describe` instead AddPrivilegesToGroupAction = "add_privileges_to_group" diff --git a/internal/distributed/proxy/httpserver/handler_v2.go b/internal/distributed/proxy/httpserver/handler_v2.go index 13e5beb164572..6fe1c7d84608f 100644 --- a/internal/distributed/proxy/httpserver/handler_v2.go +++ b/internal/distributed/proxy/httpserver/handler_v2.go @@ -131,6 +131,8 @@ func (h *HandlersV2) RegisterRoutesToV2(router gin.IRouter) { router.POST(RoleCategory+DropAction, timeoutMiddleware(wrapperPost(func() any { return &RoleReq{} }, wrapperTraceLog(h.dropRole)))) router.POST(RoleCategory+GrantPrivilegeAction, timeoutMiddleware(wrapperPost(func() any { return &GrantReq{} }, wrapperTraceLog(h.addPrivilegeToRole)))) router.POST(RoleCategory+RevokePrivilegeAction, timeoutMiddleware(wrapperPost(func() any { return &GrantReq{} }, wrapperTraceLog(h.removePrivilegeFromRole)))) + router.POST(RoleCategory+GrantPrivilegeActionV2, timeoutMiddleware(wrapperPost(func() any { return &GrantV2Req{} }, wrapperTraceLog(h.grantV2)))) + router.POST(RoleCategory+RevokePrivilegeActionV2, timeoutMiddleware(wrapperPost(func() any { return &GrantV2Req{} }, wrapperTraceLog(h.revokeV2)))) // privilege group router.POST(PrivilegeGroupCategory+CreateAction, timeoutMiddleware(wrapperPost(func() any { return &PrivilegeGroupReq{} }, wrapperTraceLog(h.createPrivilegeGroup)))) @@ -1702,6 +1704,37 @@ func (h *HandlersV2) operatePrivilegeToRole(ctx context.Context, c *gin.Context, return resp, err } +func (h *HandlersV2) operatePrivilegeToRoleV2(ctx context.Context, c *gin.Context, httpReq *GrantV2Req, operateType milvuspb.OperatePrivilegeType) (interface{}, error) { + req := &milvuspb.OperatePrivilegeRequest{ + Entity: &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: httpReq.RoleName}, + Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()}, + ObjectName: httpReq.CollectionName, + DbName: httpReq.DbName, + Grantor: &milvuspb.GrantorEntity{ + Privilege: &milvuspb.PrivilegeEntity{Name: httpReq.Privilege}, + }, + }, + Type: operateType, + Version: "v2", + } + resp, err := wrapperProxy(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/OperatePrivilege", func(reqCtx context.Context, req any) (interface{}, error) { + return h.proxy.OperatePrivilege(reqCtx, req.(*milvuspb.OperatePrivilegeRequest)) + }) + if err == nil { + HTTPReturn(c, http.StatusOK, wrapperReturnDefault()) + } + return resp, err +} + +func (h *HandlersV2) grantV2(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) { + return h.operatePrivilegeToRoleV2(ctx, c, anyReq.(*GrantV2Req), milvuspb.OperatePrivilegeType_Grant) +} + +func (h *HandlersV2) revokeV2(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) { + return h.operatePrivilegeToRoleV2(ctx, c, anyReq.(*GrantV2Req), milvuspb.OperatePrivilegeType_Revoke) +} + func (h *HandlersV2) addPrivilegeToRole(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) { return h.operatePrivilegeToRole(ctx, c, anyReq.(*GrantReq), milvuspb.OperatePrivilegeType_Grant, dbName) } diff --git a/internal/distributed/proxy/httpserver/handler_v2_test.go b/internal/distributed/proxy/httpserver/handler_v2_test.go index 2eecd7f0d9110..e23e035e9e916 100644 --- a/internal/distributed/proxy/httpserver/handler_v2_test.go +++ b/internal/distributed/proxy/httpserver/handler_v2_test.go @@ -1106,7 +1106,7 @@ func TestMethodPost(t *testing.T) { mp.EXPECT().UpdateCredential(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once() mp.EXPECT().OperateUserRole(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Twice() mp.EXPECT().CreateRole(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once() - mp.EXPECT().OperatePrivilege(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Twice() + mp.EXPECT().OperatePrivilege(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Times(4) mp.EXPECT().CreateIndex(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Twice() mp.EXPECT().CreateIndex(mock.Anything, mock.Anything).Return(commonErrorStatus, nil).Once() mp.EXPECT().CreateAlias(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once() @@ -1179,6 +1179,12 @@ func TestMethodPost(t *testing.T) { queryTestCases = append(queryTestCases, rawTestCase{ path: versionalV2(RoleCategory, RevokePrivilegeAction), }) + queryTestCases = append(queryTestCases, rawTestCase{ + path: versionalV2(RoleCategory, GrantPrivilegeActionV2), + }) + queryTestCases = append(queryTestCases, rawTestCase{ + path: versionalV2(RoleCategory, RevokePrivilegeActionV2), + }) queryTestCases = append(queryTestCases, rawTestCase{ path: versionalV2(IndexCategory, CreateAction), }) diff --git a/internal/distributed/proxy/httpserver/request_v2.go b/internal/distributed/proxy/httpserver/request_v2.go index 0951593e78c60..0ef3c2045e0f6 100644 --- a/internal/distributed/proxy/httpserver/request_v2.go +++ b/internal/distributed/proxy/httpserver/request_v2.go @@ -273,6 +273,13 @@ type PrivilegeGroupReq struct { Privileges []string `json:"privileges"` } +type GrantV2Req struct { + RoleName string `json:"roleName" binding:"required"` + DbName string `json:"dbName"` + CollectionName string `json:"collectionName"` + Privilege string `json:"privilege" binding:"required"` +} + type GrantReq struct { RoleName string `json:"roleName" binding:"required"` ObjectType string `json:"objectType" binding:"required"` diff --git a/internal/mocks/mock_proxy.go b/internal/mocks/mock_proxy.go index 2fc1856e907be..492b0d864a0a7 100644 --- a/internal/mocks/mock_proxy.go +++ b/internal/mocks/mock_proxy.go @@ -4734,6 +4734,61 @@ func (_c *MockProxy_OperatePrivilegeGroup_Call) RunAndReturn(run func(context.Co return _c } +// OperatePrivilegeV2 provides a mock function with given fields: _a0, _a1 +func (_m *MockProxy) OperatePrivilegeV2(_a0 context.Context, _a1 *milvuspb.OperatePrivilegeV2Request) (*commonpb.Status, error) { + ret := _m.Called(_a0, _a1) + + var r0 *commonpb.Status + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.OperatePrivilegeV2Request) (*commonpb.Status, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.OperatePrivilegeV2Request) *commonpb.Status); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*commonpb.Status) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.OperatePrivilegeV2Request) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockProxy_OperatePrivilegeV2_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OperatePrivilegeV2' +type MockProxy_OperatePrivilegeV2_Call struct { + *mock.Call +} + +// OperatePrivilegeV2 is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.OperatePrivilegeV2Request +func (_e *MockProxy_Expecter) OperatePrivilegeV2(_a0 interface{}, _a1 interface{}) *MockProxy_OperatePrivilegeV2_Call { + return &MockProxy_OperatePrivilegeV2_Call{Call: _e.mock.On("OperatePrivilegeV2", _a0, _a1)} +} + +func (_c *MockProxy_OperatePrivilegeV2_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.OperatePrivilegeV2Request)) *MockProxy_OperatePrivilegeV2_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.OperatePrivilegeV2Request)) + }) + return _c +} + +func (_c *MockProxy_OperatePrivilegeV2_Call) Return(_a0 *commonpb.Status, _a1 error) *MockProxy_OperatePrivilegeV2_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockProxy_OperatePrivilegeV2_Call) RunAndReturn(run func(context.Context, *milvuspb.OperatePrivilegeV2Request) (*commonpb.Status, error)) *MockProxy_OperatePrivilegeV2_Call { + _c.Call.Return(run) + return _c +} + // OperateUserRole provides a mock function with given fields: _a0, _a1 func (_m *MockProxy) OperateUserRole(_a0 context.Context, _a1 *milvuspb.OperateUserRoleRequest) (*commonpb.Status, error) { ret := _m.Called(_a0, _a1) diff --git a/internal/proxy/impl.go b/internal/proxy/impl.go index 87c5f1fa09f75..f408c5dbbe73b 100644 --- a/internal/proxy/impl.go +++ b/internal/proxy/impl.go @@ -5210,6 +5210,87 @@ func (node *Proxy) validPrivilegeParams(req *milvuspb.OperatePrivilegeRequest) e return nil } +func (node *Proxy) validOperatePrivilegeV2Params(req *milvuspb.OperatePrivilegeV2Request) error { + if req.Role == nil { + return fmt.Errorf("the role in the request is nil") + } + if err := ValidateRoleName(req.Role.Name); err != nil { + return err + } + if err := ValidatePrivilege(req.Grantor.Privilege.Name); err != nil { + return err + } + if req.Type != milvuspb.OperatePrivilegeType_Grant && req.Type != milvuspb.OperatePrivilegeType_Revoke { + return fmt.Errorf("the type in the request not grant or revoke") + } + if err := ValidateObjectName(req.DbName); err != nil { + return err + } + if err := ValidateObjectName(req.CollectionName); err != nil { + return err + } + return nil +} + +func (node *Proxy) OperatePrivilegeV2(ctx context.Context, req *milvuspb.OperatePrivilegeV2Request) (*commonpb.Status, error) { + ctx, sp := otel.Tracer(typeutil.ProxyRole).Start(ctx, "Proxy-OperatePrivilegeV2") + defer sp.End() + + log := log.Ctx(ctx) + + log.Info("OperatePrivilegeV2", + zap.Any("req", req)) + if err := merr.CheckHealthy(node.GetStateCode()); err != nil { + return merr.Status(err), nil + } + if err := node.validOperatePrivilegeV2Params(req); err != nil { + return merr.Status(err), nil + } + curUser, err := GetCurUserFromContext(ctx) + if err != nil { + return merr.Status(err), nil + } + req.Grantor.User = &milvuspb.UserEntity{Name: curUser} + request := &milvuspb.OperatePrivilegeRequest{ + Base: &commonpb.MsgBase{MsgType: commonpb.MsgType_OperatePrivilege}, + Entity: &milvuspb.GrantEntity{ + Role: req.Role, + Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()}, + ObjectName: req.CollectionName, + DbName: req.DbName, + Grantor: req.Grantor, + }, + Type: req.Type, + Version: "v2", + } + req.Grantor.User = &milvuspb.UserEntity{Name: curUser} + result, err := node.rootCoord.OperatePrivilege(ctx, request) + if err != nil { + log.Warn("fail to operate privilege", zap.Error(err)) + return merr.Status(err), nil + } + relatedPrivileges := util.RelatedPrivileges[util.PrivilegeNameForMetastore(req.Grantor.Privilege.Name)] + if len(relatedPrivileges) != 0 { + for _, relatedPrivilege := range relatedPrivileges { + relatedReq := proto.Clone(request).(*milvuspb.OperatePrivilegeRequest) + relatedReq.Entity.Grantor.Privilege.Name = util.PrivilegeNameForAPI(relatedPrivilege) + result, err = node.rootCoord.OperatePrivilege(ctx, relatedReq) + if err != nil { + log.Warn("fail to operate related privilege", zap.String("related_privilege", relatedPrivilege), zap.Error(err)) + return merr.Status(err), nil + } + if !merr.Ok(result) { + log.Warn("fail to operate related privilege", zap.String("related_privilege", relatedPrivilege), zap.Any("result", result)) + return result, nil + } + } + } + if merr.Ok(result) { + SendReplicateMessagePack(ctx, node.replicateMsgStream, req) + } + return result, nil +} + func (node *Proxy) OperatePrivilege(ctx context.Context, req *milvuspb.OperatePrivilegeRequest) (*commonpb.Status, error) { ctx, sp := otel.Tracer(typeutil.ProxyRole).Start(ctx, "Proxy-OperatePrivilege") defer sp.End() @@ -6399,8 +6480,11 @@ func (node *Proxy) CreatePrivilegeGroup(ctx context.Context, req *milvuspb.Creat if err := merr.CheckHealthy(node.GetStateCode()); err != nil { return merr.Status(err), nil } - if req.GroupName == "" { - return merr.Status(fmt.Errorf("the group name in the drop privilege group request is nil")), nil + if err := ValidatePrivilegeGroupName(req.GroupName); err != nil { + log.Warn("CreatePrivilegeGroup failed", + zap.Error(err), + ) + return getErrResponse(err, "CreatePrivilegeGroup", "", ""), nil } if req.Base == nil { req.Base = &commonpb.MsgBase{} @@ -6428,8 +6512,11 @@ func (node *Proxy) DropPrivilegeGroup(ctx context.Context, req *milvuspb.DropPri if err := merr.CheckHealthy(node.GetStateCode()); err != nil { return merr.Status(err), nil } - if req.GroupName == "" { - return merr.Status(fmt.Errorf("the group name in the drop privilege group request is nil")), nil + if err := ValidatePrivilegeGroupName(req.GroupName); err != nil { + log.Warn("DropPrivilegeGroup failed", + zap.Error(err), + ) + return getErrResponse(err, "DropPrivilegeGroup", "", ""), nil } if req.Base == nil { req.Base = &commonpb.MsgBase{} @@ -6486,8 +6573,11 @@ func (node *Proxy) OperatePrivilegeGroup(ctx context.Context, req *milvuspb.Oper if err := merr.CheckHealthy(node.GetStateCode()); err != nil { return merr.Status(err), nil } - if req.GroupName == "" { - return merr.Status(fmt.Errorf("the group name in the drop privilege group request is nil")), nil + if err := ValidatePrivilegeGroupName(req.GroupName); err != nil { + log.Warn("OperatePrivilegeGroup failed", + zap.Error(err), + ) + return getErrResponse(err, "OperatePrivilegeGroup", "", ""), nil } for _, priv := range req.GetPrivileges() { if err := ValidatePrivilege(priv.Name); err != nil { diff --git a/internal/proxy/privilege_interceptor.go b/internal/proxy/privilege_interceptor.go index fc78fc35383dd..9656cf244c9b6 100644 --- a/internal/proxy/privilege_interceptor.go +++ b/internal/proxy/privilege_interceptor.go @@ -162,8 +162,8 @@ func PrivilegeInterceptor(ctx context.Context, req interface{}) (context.Context e := getEnforcer() for _, roleName := range roleNames { - permitFunc := func(resName string) (bool, error) { - object := funcutil.PolicyForResource(dbName, objectType, resName) + permitFunc := func(objectName string) (bool, error) { + object := funcutil.PolicyForResource(dbName, objectType, objectName) isPermit, cached, version := GetPrivilegeCache(roleName, object, objectPrivilege) if cached { return isPermit, nil diff --git a/internal/proxy/privilege_interceptor_test.go b/internal/proxy/privilege_interceptor_test.go index 92621d010eac7..e8093e0985d7b 100644 --- a/internal/proxy/privilege_interceptor_test.go +++ b/internal/proxy/privilege_interceptor_test.go @@ -563,3 +563,45 @@ func TestPrivilegeGroup(t *testing.T) { assert.NoError(t, err) }) } + +func TestBuiltinPrivilegeGroup(t *testing.T) { + t.Run("ClusterAdmin", func(t *testing.T) { + paramtable.Get().Save(Params.CommonCfg.AuthorizationEnabled.Key, "true") + initPrivilegeGroups() + + var err error + ctx := GetContext(context.Background(), "fooo:123456") + client := &MockRootCoordClientInterface{} + queryCoord := &mocks.MockQueryCoordClient{} + mgr := newShardClientMgr() + + policies := []string{} + for _, priv := range util.BuiltinPrivilegeGroups["ClusterReadOnly"] { + objectType := util.GetObjectType(priv) + policies = append(policies, funcutil.PolicyForPrivilege("role1", objectType, "*", util.PrivilegeNameForMetastore(priv), "default")) + } + client.listPolicy = func(ctx context.Context, in *internalpb.ListPolicyRequest) (*internalpb.ListPolicyResponse, error) { + return &internalpb.ListPolicyResponse{ + Status: merr.Success(), + PolicyInfos: policies, + UserRoles: []string{ + funcutil.EncodeUserRoleCache("fooo", "role1"), + }, + }, nil + } + InitMetaCache(ctx, client, queryCoord, mgr) + defer CleanPrivilegeCache() + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.SelectUserRequest{}) + assert.NoError(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.DescribeResourceGroupRequest{}) + assert.NoError(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.ListResourceGroupsRequest{}) + assert.NoError(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.CreateResourceGroupRequest{}) + assert.Error(t, err) + }) +} diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index c500bff6461f8..6c100f06982f5 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -2700,6 +2700,8 @@ func TestProxy(t *testing.T) { testProxyRole(ctx, t, proxy) testProxyPrivilege(ctx, t, proxy) + testProxyOperatePrivilegeV2(ctx, t, proxy) + assert.False(t, false, true) testProxyRefreshPolicyInfoCache(ctx, t, proxy) // proxy unhealthy @@ -4368,6 +4370,106 @@ func testProxyPrivilege(ctx context.Context, t *testing.T, proxy *Proxy) { wg.Wait() } +func testProxyOperatePrivilegeV2(ctx context.Context, t *testing.T, proxy *Proxy) { + var wg sync.WaitGroup + wg.Add(1) + t.Run("Operate Privilege V2, Select Grant", func(t *testing.T) { + defer wg.Done() + + // GrantPrivilege + req := &milvuspb.OperatePrivilegeV2Request{} + resp, _ := proxy.OperatePrivilegeV2(ctx, req) + assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + req.Role = &milvuspb.RoleEntity{} + resp, _ = proxy.OperatePrivilegeV2(ctx, req) + assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + req.Grantor = &milvuspb.GrantorEntity{} + resp, _ = proxy.OperatePrivilegeV2(ctx, req) + assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + req.Grantor.Privilege = &milvuspb.PrivilegeEntity{} + resp, _ = proxy.OperatePrivilegeV2(ctx, req) + assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + req.Grantor.Privilege.Name = util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeAll.String()) + + resp, _ = proxy.OperatePrivilegeV2(ctx, req) + assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + req.DbName = "" + resp, _ = proxy.OperatePrivilegeV2(ctx, req) + assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + req.CollectionName = "" + resp, _ = proxy.OperatePrivilegeV2(ctx, req) + assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + roleReq := &milvuspb.OperatePrivilegeV2Request{ + Role: &milvuspb.RoleEntity{Name: "public"}, + Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeLoad.String())}}, + DbName: "default", + CollectionName: "col1", + Type: milvuspb.OperatePrivilegeType_Grant, + } + resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq) + assert.Equal(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + roleReq = &milvuspb.OperatePrivilegeV2Request{ + Role: &milvuspb.RoleEntity{Name: "public"}, + Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadOnly.String())}}, + DbName: util.AnyWord, + CollectionName: util.AnyWord, + Type: milvuspb.OperatePrivilegeType_Grant, + } + resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq) + assert.Equal(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + roleReq = &milvuspb.OperatePrivilegeV2Request{ + Role: &milvuspb.RoleEntity{Name: "public"}, + Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadOnly.String())}}, + DbName: "db1", + CollectionName: util.AnyWord, + Type: milvuspb.OperatePrivilegeType_Grant, + } + resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq) + assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + roleReq = &milvuspb.OperatePrivilegeV2Request{ + Role: &milvuspb.RoleEntity{Name: "public"}, + Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionReadOnly.String())}}, + DbName: "db1", + CollectionName: util.AnyWord, + Type: milvuspb.OperatePrivilegeType_Grant, + } + resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq) + assert.Equal(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + roleReq = &milvuspb.OperatePrivilegeV2Request{ + Role: &milvuspb.RoleEntity{Name: "public"}, + Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadOnly.String())}}, + DbName: "db1", + CollectionName: "col1", + Type: milvuspb.OperatePrivilegeType_Grant, + } + resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq) + assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode) + + roleReq = &milvuspb.OperatePrivilegeV2Request{ + Role: &milvuspb.RoleEntity{Name: "public"}, + Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseReadOnly.String())}}, + DbName: "db1", + CollectionName: util.AnyWord, + Type: milvuspb.OperatePrivilegeType_Grant, + } + resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq) + assert.Equal(t, commonpb.ErrorCode_Success, resp.ErrorCode) + }) + + wg.Wait() +} + func testProxyPrivilegeUnhealthy(ctx context.Context, t *testing.T, proxy *Proxy) { testProxyPrivilegeFail(ctx, t, proxy, "unhealthy") } diff --git a/internal/proxy/util.go b/internal/proxy/util.go index dae3c88c56fe4..55c4befe7b517 100644 --- a/internal/proxy/util.go +++ b/internal/proxy/util.go @@ -155,6 +155,32 @@ func validateCollectionNameOrAlias(entity, entityType string) error { return nil } +func ValidatePrivilegeGroupName(groupName string) error { + if groupName == "" { + return merr.WrapErrDatabaseNameInvalid(groupName, "privilege group name couldn't be empty") + } + + if len(groupName) > Params.ProxyCfg.MaxNameLength.GetAsInt() { + return merr.WrapErrDatabaseNameInvalid(groupName, + fmt.Sprintf("the length of a privilege group name must be less than %d characters", Params.ProxyCfg.MaxNameLength.GetAsInt())) + } + + firstChar := groupName[0] + if firstChar != '_' && !isAlpha(firstChar) { + return merr.WrapErrDatabaseNameInvalid(groupName, + "the first character of a privilege group name must be an underscore or letter") + } + + for i := 1; i < len(groupName); i++ { + c := groupName[i] + if c != '_' && !isAlpha(c) && !isNumber(c) { + return merr.WrapErrDatabaseNameInvalid(groupName, + "privilege group name can only contain numbers, letters and underscores") + } + } + return nil +} + func ValidateResourceGroupName(entity string) error { if entity == "" { return errors.New("resource group name couldn't be empty") diff --git a/internal/rootcoord/mock_test.go b/internal/rootcoord/mock_test.go index a9cf579fe5e85..5cc0b97b72f70 100644 --- a/internal/rootcoord/mock_test.go +++ b/internal/rootcoord/mock_test.go @@ -98,6 +98,7 @@ type mockMetaTable struct { DescribeDatabaseFunc func(ctx context.Context, dbName string) (*model.Database, error) CreatePrivilegeGroupFunc func(groupName string) error DropPrivilegeGroupFunc func(groupName string) error + IsCustomPrivilegeGroupFunc func(groupName string) (bool, error) ListPrivilegeGroupsFunc func() ([]*milvuspb.PrivilegeGroupInfo, error) OperatePrivilegeGroupFunc func(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error GetPrivilegeGroupRolesFunc func(groupName string) ([]*milvuspb.RoleEntity, error) @@ -267,6 +268,10 @@ func (m mockMetaTable) ListPrivilegeGroups() ([]*milvuspb.PrivilegeGroupInfo, er return m.ListPrivilegeGroupsFunc() } +func (m mockMetaTable) IsCustomPrivilegeGroup(groupName string) (bool, error) { + return m.IsCustomPrivilegeGroupFunc(groupName) +} + func (m mockMetaTable) OperatePrivilegeGroup(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error { return m.OperatePrivilegeGroupFunc(groupName, privileges, operateType) } @@ -558,6 +563,9 @@ func withInvalidMeta() Opt { meta.ListPrivilegeGroupsFunc = func() ([]*milvuspb.PrivilegeGroupInfo, error) { return nil, errors.New("error mock ListPrivilegeGroups") } + meta.IsCustomPrivilegeGroupFunc = func(groupName string) (bool, error) { + return false, errors.New("error mock IsCustomPrivilegeGroup") + } meta.OperatePrivilegeGroupFunc = func(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error { return errors.New("error mock OperatePrivilegeGroup") } diff --git a/internal/rootcoord/root_coord.go b/internal/rootcoord/root_coord.go index 27f66f13f56d6..90ff6fd7007e1 100644 --- a/internal/rootcoord/root_coord.go +++ b/internal/rootcoord/root_coord.go @@ -21,6 +21,7 @@ import ( "fmt" "math/rand" "os" + "strings" "sync" "time" @@ -601,6 +602,50 @@ func (c *Core) initPublicRolePrivilege() error { return nil } +func (c *Core) initBuiltinPrivilegeGroups() []*milvuspb.PrivilegeGroupInfo { + // init built in privilege groups, override by config if rbac config enabled + builtinGroups := make([]*milvuspb.PrivilegeGroupInfo, 0) + for groupName, privileges := range util.BuiltinPrivilegeGroups { + if Params.RbacConfig.Enabled.GetAsBool() { + var confPrivs []string + switch groupName { + case "ClusterReadOnly": + confPrivs = Params.RbacConfig.ClusterReadOnlyPrivileges.GetAsStrings() + case "ClusterReadWrite": + confPrivs = Params.RbacConfig.ClusterReadWritePrivileges.GetAsStrings() + case "ClusterAdmin": + confPrivs = Params.RbacConfig.ClusterAdminPrivileges.GetAsStrings() + case "DatabaseReadOnly": + confPrivs = Params.RbacConfig.DBReadOnlyPrivileges.GetAsStrings() + case "DatabaseReadWrite": + confPrivs = Params.RbacConfig.DBReadWritePrivileges.GetAsStrings() + case "DatabaseAdmin": + confPrivs = Params.RbacConfig.DBAdminPrivileges.GetAsStrings() + case "CollectionReadOnly": + confPrivs = Params.RbacConfig.CollectionReadOnlyPrivileges.GetAsStrings() + case "CollectionReadWrite": + confPrivs = Params.RbacConfig.CollectionReadWritePrivileges.GetAsStrings() + case "CollectionAdmin": + confPrivs = Params.RbacConfig.CollectionAdminPrivileges.GetAsStrings() + default: + return nil + } + if len(confPrivs) > 0 { + privileges = confPrivs + } + } + + privs := lo.Map(privileges, func(name string, _ int) *milvuspb.PrivilegeEntity { + return &milvuspb.PrivilegeEntity{Name: name} + }) + builtinGroups = append(builtinGroups, &milvuspb.PrivilegeGroupInfo{ + GroupName: groupName, + Privileges: privs, + }) + } + return builtinGroups +} + func (c *Core) initBuiltinRoles() error { rolePrivilegesMap := Params.RoleCfg.Roles.GetAsRoleDetails() for role, privilegesJSON := range rolePrivilegesMap { @@ -612,7 +657,7 @@ func (c *Core) initBuiltinRoles() error { for _, privilege := range privilegesJSON[util.RoleConfigPrivileges] { privilegeName := privilege[util.RoleConfigPrivilege] if !util.IsAnyWord(privilege[util.RoleConfigPrivilege]) { - dbPrivName, err := c.getMetastorePrivilegeName(privilege[util.RoleConfigPrivilege]) + dbPrivName, err := c.getMetastorePrivilegeName(c.ctx, privilege[util.RoleConfigPrivilege]) if err != nil { return errors.Wrapf(err, "failed to get metastore privilege name for: %s", privilege[util.RoleConfigPrivilege]) } @@ -2491,44 +2536,72 @@ func (c *Core) isValidObject(entity *milvuspb.ObjectEntity) error { return nil } -func (c *Core) isValidGrantor(entity *milvuspb.GrantorEntity, object string) error { - if entity == nil { - return errors.New("the grantor entity is nil") - } - if entity.User == nil || entity.User.Name == "" { - return errors.New("the user entity in the grantor entity is nil or empty") +func (c *Core) isValidPrivilege(ctx context.Context, privilegeName string, object string) error { + if util.IsAnyWord(privilegeName) { + return nil } - if _, err := c.meta.SelectUser(util.DefaultTenant, &milvuspb.UserEntity{Name: entity.User.Name}, false); err != nil { - log.Warn("fail to select the user", zap.String("username", entity.User.Name), zap.Error(err)) - return errors.New("not found the user, maybe the user isn't existed or internal system error") + customPrivGroup, err := c.meta.IsCustomPrivilegeGroup(privilegeName) + if err != nil { + return err } - if entity.Privilege == nil { - return errors.New("the privilege entity in the grantor entity is nil") + if customPrivGroup { + return fmt.Errorf("can not operate the custom privilege group [%s]", privilegeName) } - if util.IsAnyWord(entity.Privilege.Name) { - return nil + if lo.Contains(lo.Keys(util.BuiltinPrivilegeGroups), privilegeName) { + return fmt.Errorf("can not operate the built-in privilege group [%s]", privilegeName) } // check object privileges for built-in privileges - if util.IsPrivilegeNameDefined(entity.Privilege.Name) { + if util.IsPrivilegeNameDefined(privilegeName) { privileges, ok := util.ObjectPrivileges[object] if !ok { return fmt.Errorf("not found the object type[name: %s], supported the object types: %v", object, lo.Keys(commonpb.ObjectType_value)) } for _, privilege := range privileges { - if privilege == entity.Privilege.Name { + if privilege == privilegeName { return nil } } } - // check if it is a custom privilege group - customPrivGroup, err := c.meta.IsCustomPrivilegeGroup(entity.Privilege.Name) - if err != nil { - return err + return fmt.Errorf("not found the privilege name[%s] in object[%s]", privilegeName, object) +} + +func (c *Core) isValidPrivilegeV2(ctx context.Context, privilegeName, dbName, collectionName string) error { + if util.IsAnyWord(privilegeName) { + return nil } - if customPrivGroup { + var privilegeLevel string + for group, privileges := range util.BuiltinPrivilegeGroups { + if privilegeName == group || lo.Contains(privileges, privilegeName) { + privilegeLevel = group + break + } + } + if privilegeLevel == "" { + customPrivGroup, err := c.meta.IsCustomPrivilegeGroup(privilegeName) + if err != nil { + return err + } + if customPrivGroup { + return nil + } + return fmt.Errorf("not found the privilege name[%s] in the custom privilege groups", privilegeName) + } + switch { + case strings.HasPrefix(privilegeLevel, milvuspb.PrivilegeLevel_Cluster.String()): + if !util.IsAnyWord(dbName) || !util.IsAnyWord(collectionName) { + return fmt.Errorf("dbName and collectionName should be * for the cluster level privilege: %s", privilegeName) + } + return nil + case strings.HasPrefix(privilegeLevel, milvuspb.PrivilegeLevel_Database.String()): + if collectionName != "" && collectionName != util.AnyWord { + return fmt.Errorf("collectionName should be empty or * for the database level privilege: %s", privilegeName) + } + return nil + case strings.HasPrefix(privilegeLevel, milvuspb.PrivilegeLevel_Collection.String()): + return nil + default: return nil } - return fmt.Errorf("not found the privilege name[%s] in object[%s]", entity.Privilege.Name, object) } // OperatePrivilege operate the privilege, including grant and revoke @@ -2545,50 +2618,42 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile ctxLog := log.Ctx(ctx).With(zap.String("role", typeutil.RootCoordRole), zap.Any("in", in)) ctxLog.Debug(method) - if err := merr.CheckHealthy(c.GetStateCode()); err != nil { - return merr.Status(err), nil - } - if in.Type != milvuspb.OperatePrivilegeType_Grant && in.Type != milvuspb.OperatePrivilegeType_Revoke { - errMsg := fmt.Sprintf("invalid operate privilege type, current type: %s, valid value: [%s, %s]", in.Type, milvuspb.OperatePrivilegeType_Grant, milvuspb.OperatePrivilegeType_Revoke) - ctxLog.Warn(errMsg) - return merr.StatusWithErrorCode(errors.New(errMsg), commonpb.ErrorCode_OperatePrivilegeFailure), nil - } - if in.Entity == nil { - errMsg := "the grant entity in the request is nil" - ctxLog.Error(errMsg) - return merr.StatusWithErrorCode(errors.New(errMsg), commonpb.ErrorCode_OperatePrivilegeFailure), nil - } - if err := c.isValidObject(in.Entity.Object); err != nil { - ctxLog.Warn("", zap.Error(err)) - return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil - } - if err := c.isValidRole(in.Entity.Role); err != nil { - ctxLog.Warn("", zap.Error(err)) - return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil - } - if err := c.isValidGrantor(in.Entity.Grantor, in.Entity.Object.Name); err != nil { - ctxLog.Error("", zap.Error(err)) + if err := c.operatePrivilegeCommonCheck(ctx, in); err != nil { return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil } - // set up privilege name for metastore - privName := in.Entity.Grantor.Privilege.Name - ctxLog.Debug("before PrivilegeNameForMetastore", zap.String("privilege", privName)) - if !util.IsAnyWord(privName) { - dbPrivName, err := c.getMetastorePrivilegeName(privName) - if err != nil { + switch in.Version { + case "v2": + if err := c.isValidPrivilegeV2(ctx, in.Entity.Grantor.Privilege.Name, + in.Entity.DbName, in.Entity.ObjectName); err != nil { + ctxLog.Error("", zap.Error(err)) + return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil + } + default: + if err := c.isValidPrivilege(ctx, in.Entity.Grantor.Privilege.Name, in.Entity.Object.Name); err != nil { + ctxLog.Error("", zap.Error(err)) return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil } - in.Entity.Grantor.Privilege.Name = dbPrivName + // set up object name if it is global object type and not built in privilege group + if in.Entity.Object.Name == commonpb.ObjectType_Global.String() && !lo.Contains(lo.Keys(util.BuiltinPrivilegeGroups), in.Entity.Grantor.Privilege.Name) { + in.Entity.ObjectName = util.AnyWord + } } - ctxLog.Debug("after PrivilegeNameForMetastore", zap.String("privilege", privName)) - if in.Entity.Object.Name == commonpb.ObjectType_Global.String() { - in.Entity.ObjectName = util.AnyWord - } + // set up privilege name for metastore + privName := in.Entity.Grantor.Privilege.Name redoTask := newBaseRedoTask(c.stepExecutor) redoTask.AddSyncStep(NewSimpleStep("operate privilege meta data", func(ctx context.Context) ([]nestedStep, error) { + if !util.IsAnyWord(privName) { + // set up privilege name for metastore + dbPrivName, err := c.getMetastorePrivilegeName(ctx, privName) + if err != nil { + return nil, err + } + in.Entity.Grantor.Privilege.Name = dbPrivName + } + err := c.meta.OperatePrivilege(util.DefaultTenant, in.Entity, in.Type) if err != nil && !common.IsIgnorableError(err) { log.Warn("fail to operate the privilege", zap.Any("in", in), zap.Error(err)) @@ -2597,6 +2662,8 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile return nil, nil })) redoTask.AddAsyncStep(NewSimpleStep("operate privilege cache", func(ctx context.Context) ([]nestedStep, error) { + // set back to expand privilege group + in.Entity.Grantor.Privilege.Name = privName var opType int32 switch in.Type { case milvuspb.OperatePrivilegeType_Grant: @@ -2607,9 +2674,23 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile log.Warn("invalid operate type for the OperatePrivilege api", zap.Any("in", in)) return nil, nil } + grants := []*milvuspb.GrantEntity{in.Entity} + + allGroups, err := c.meta.ListPrivilegeGroups() + allGroups = append(allGroups, c.initBuiltinPrivilegeGroups()...) + if err != nil { + return nil, err + } + groups := lo.SliceToMap(allGroups, func(group *milvuspb.PrivilegeGroupInfo) (string, []*milvuspb.PrivilegeEntity) { + return group.GroupName, group.Privileges + }) + expandGrants, err := c.expandPrivilegeGroups(grants, groups) + if err != nil { + return nil, err + } 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, in.Entity.DbName), + OpKey: funcutil.PolicyForPrivileges(expandGrants), }); err != nil { log.Warn("fail to refresh policy info cache", zap.Any("in", in), zap.Error(err)) return nil, err @@ -2630,7 +2711,42 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile return merr.Success(), nil } -func (c *Core) getMetastorePrivilegeName(privName string) (string, error) { +func (c *Core) operatePrivilegeCommonCheck(ctx context.Context, in *milvuspb.OperatePrivilegeRequest) error { + if err := merr.CheckHealthy(c.GetStateCode()); err != nil { + return err + } + if in.Type != milvuspb.OperatePrivilegeType_Grant && in.Type != milvuspb.OperatePrivilegeType_Revoke { + errMsg := fmt.Sprintf("invalid operate privilege type, current type: %s, valid value: [%s, %s]", in.Type, milvuspb.OperatePrivilegeType_Grant, milvuspb.OperatePrivilegeType_Revoke) + return errors.New(errMsg) + } + if in.Entity == nil { + errMsg := "the grant entity in the request is nil" + return errors.New(errMsg) + } + if err := c.isValidObject(in.Entity.Object); err != nil { + return errors.New("the object entity in the request is nil or invalid") + } + if err := c.isValidRole(in.Entity.Role); err != nil { + return err + } + entity := in.Entity.Grantor + if entity == nil { + return errors.New("the grantor entity is nil") + } + if entity.User == nil || entity.User.Name == "" { + return errors.New("the user entity in the grantor entity is nil or empty") + } + if _, err := c.meta.SelectUser(util.DefaultTenant, &milvuspb.UserEntity{Name: entity.User.Name}, false); err != nil { + log.Warn("fail to select the user", zap.String("username", entity.User.Name), zap.Error(err)) + return errors.New("not found the user, maybe the user isn't existed or internal system error") + } + if entity.Privilege == nil { + return errors.New("the privilege entity in the grantor entity is nil") + } + return nil +} + +func (c *Core) getMetastorePrivilegeName(ctx context.Context, privName string) (string, error) { // if it is built-in privilege, return the privilege name directly if util.IsPrivilegeNameDefined(privName) { return util.PrivilegeNameForMetastore(privName), nil @@ -3026,6 +3142,18 @@ func (c *Core) ListPrivilegeGroups(ctx context.Context, in *milvuspb.ListPrivile ctxLog.Debug(method + " success") metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.SuccessLabel).Inc() metrics.RootCoordDDLReqLatency.WithLabelValues(method).Observe(float64(tr.ElapseSpan().Milliseconds())) + + // append built in privilege groups + for groupName, privileges := range util.BuiltinPrivilegeGroups { + privGroups = append(privGroups, &milvuspb.PrivilegeGroupInfo{ + GroupName: groupName, + Privileges: lo.Map(privileges, func(p string, _ int) *milvuspb.PrivilegeEntity { + return &milvuspb.PrivilegeEntity{ + Name: p, + } + }), + }) + } return &milvuspb.ListPrivilegeGroupsResponse{ Status: merr.Success(), PrivilegeGroups: privGroups, @@ -3072,6 +3200,14 @@ func (c *Core) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePr newGroups[k] = lo.UniqBy(newPrivs, func(p *milvuspb.PrivilegeEntity) string { return p.Name }) + + // check if privileges are the same object type + objectTypes := lo.SliceToMap(newPrivs, func(p *milvuspb.PrivilegeEntity) (string, struct{}) { + return util.GetObjectType(p.Name), struct{}{} + }) + if len(objectTypes) > 1 { + return nil, errors.New("privileges are not the same object type") + } case milvuspb.OperatePrivilegeGroupType_RemovePrivilegesFromGroup: newPrivs, _ := lo.Difference(v, in.Privileges) newGroups[k] = newPrivs @@ -3083,11 +3219,11 @@ func (c *Core) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePr rolesToRevoke := []*milvuspb.GrantEntity{} rolesToGrant := []*milvuspb.GrantEntity{} compareGrants := func(a, b *milvuspb.GrantEntity) bool { - return a.Role.GetName() == b.Role.GetName() && - a.Object.GetName() == b.Object.GetName() && + return a.Role.Name == b.Role.Name && + a.Object.Name == b.Object.Name && a.ObjectName == b.ObjectName && - a.Grantor.GetUser().GetName() == b.Grantor.GetUser().GetName() && - a.Grantor.GetPrivilege().GetName() == b.Grantor.GetPrivilege().GetName() && + a.Grantor.User.Name == b.Grantor.User.Name && + a.Grantor.Privilege.Name == b.Grantor.Privilege.Name && a.DbName == b.DbName } for _, role := range roles { @@ -3098,8 +3234,14 @@ func (c *Core) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePr if err != nil { return nil, err } - currGrants := c.expandPrivilegeGroups(grants, currGroups) - newGrants := c.expandPrivilegeGroups(grants, newGroups) + currGrants, err := c.expandPrivilegeGroups(grants, currGroups) + if err != nil { + return nil, err + } + newGrants, err := c.expandPrivilegeGroups(grants, newGroups) + if err != nil { + return nil, err + } toRevoke := lo.Filter(currGrants, func(item *milvuspb.GrantEntity, _ int) bool { return !lo.ContainsBy(newGrants, func(newItem *milvuspb.GrantEntity) bool { @@ -3163,28 +3305,50 @@ func (c *Core) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePr return merr.Success(), nil } -func (c *Core) expandPrivilegeGroups(grants []*milvuspb.GrantEntity, groups map[string][]*milvuspb.PrivilegeEntity) []*milvuspb.GrantEntity { +func (c *Core) expandPrivilegeGroups(grants []*milvuspb.GrantEntity, groups map[string][]*milvuspb.PrivilegeEntity) ([]*milvuspb.GrantEntity, error) { newGrants := []*milvuspb.GrantEntity{} + createGrantEntity := func(grant *milvuspb.GrantEntity, privilegeName string) (*milvuspb.GrantEntity, error) { + metaName, err := c.getMetastorePrivilegeName(c.ctx, privilegeName) + if err != nil { + return nil, err + } + if objectType := util.GetObjectType(privilegeName); objectType != "" { + grant.Object.Name = objectType + } + return &milvuspb.GrantEntity{ + Role: grant.Role, + Object: grant.Object, + ObjectName: grant.ObjectName, + Grantor: &milvuspb.GrantorEntity{ + User: grant.Grantor.User, + Privilege: &milvuspb.PrivilegeEntity{ + Name: metaName, + }, + }, + DbName: grant.DbName, + }, nil + } + for _, grant := range grants { - if groups[grant.Grantor.Privilege.Name] == nil { - newGrants = append(newGrants, grant) + privName := grant.Grantor.Privilege.Name + if privGroup, exists := groups[privName]; !exists { + newGrant, err := createGrantEntity(grant, privName) + if err != nil { + return nil, err + } + newGrants = append(newGrants, newGrant) } else { - for _, priv := range groups[grant.Grantor.Privilege.Name] { - newGrants = append(newGrants, &milvuspb.GrantEntity{ - Role: grant.Role, - Object: grant.Object, - ObjectName: grant.ObjectName, - Grantor: &milvuspb.GrantorEntity{ - User: grant.Grantor.User, - Privilege: priv, - }, - DbName: grant.DbName, - }) + for _, priv := range privGroup { + newGrant, err := createGrantEntity(grant, priv.Name) + if err != nil { + return nil, err + } + newGrants = append(newGrants, newGrant) } } } // uniq by role + object + object name + grantor user + privilege name + db name return lo.UniqBy(newGrants, func(g *milvuspb.GrantEntity) string { return fmt.Sprintf("%s-%s-%s-%s-%s-%s", g.Role, g.Object, g.ObjectName, g.Grantor.User, g.Grantor.Privilege.Name, g.DbName) - }) + }), nil } diff --git a/internal/rootcoord/root_coord_test.go b/internal/rootcoord/root_coord_test.go index bdf93e15a9a41..1ea416a4ce4b6 100644 --- a/internal/rootcoord/root_coord_test.go +++ b/internal/rootcoord/root_coord_test.go @@ -1764,7 +1764,9 @@ func TestRootCoord_RBACError(t *testing.T) { assert.NoError(t, err) assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode) } - + mockMeta.IsCustomPrivilegeGroupFunc = func(groupName string) (bool, error) { + return false, nil + } mockMeta.SelectUserFunc = func(tenant string, entity *milvuspb.UserEntity, includeRoleInfo bool) ([]*milvuspb.UserResult, error) { return nil, nil } @@ -2009,6 +2011,29 @@ func TestCore_InitRBAC(t *testing.T) { err := c.initRbac() assert.NoError(t, err) }) + + t.Run("init default privilege groups", func(t *testing.T) { + clusterReadWrite := `SelectOwnership,SelectUser,DescribeResourceGroup` + meta := mockrootcoord.NewIMetaTable(t) + c := newTestCore(withHealthyCode(), withMeta(meta)) + + Params.Save(Params.RbacConfig.Enabled.Key, "true") + Params.Save(Params.RbacConfig.ClusterReadWritePrivileges.Key, clusterReadWrite) + + defer func() { + Params.Reset(Params.RbacConfig.Enabled.Key) + Params.Reset(Params.RbacConfig.ClusterReadWritePrivileges.Key) + }() + + builtinGroups := c.initBuiltinPrivilegeGroups() + fmt.Println(builtinGroups) + assert.Equal(t, len(util.BuiltinPrivilegeGroups), len(builtinGroups)) + for _, group := range builtinGroups { + if group.GroupName == "ClusterReadWrite" { + assert.Equal(t, len(group.Privileges), 3) + } + } + }) } func TestCore_BackupRBAC(t *testing.T) { diff --git a/pkg/go.mod b/pkg/go.mod index df0496e29ce39..a812a5ea8e139 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -12,7 +12,7 @@ require ( github.com/expr-lang/expr v1.15.7 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/klauspost/compress v1.17.7 - github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17 + github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1 github.com/nats-io/nats-server/v2 v2.10.12 github.com/nats-io/nats.go v1.34.1 github.com/panjf2000/ants/v2 v2.7.2 diff --git a/pkg/go.sum b/pkg/go.sum index 09011b0beff15..e83872f8688b5 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -503,8 +503,8 @@ github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119 h1:9VXijWu github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119/go.mod h1:DvXTE/K/RtHehxU8/GtDs4vFtfw64jJ3PaCnFri8CRg= github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b h1:TfeY0NxYxZzUfIfYe5qYDBzt4ZYRqzUjTR6CvUzjat8= github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b/go.mod h1:iwW+9cWfIzzDseEBCCeDSN5SD16Tidvy8cwQ7ZY8Qj4= -github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17 h1:ANkXdUKKpIPPQkw9pkV9ku9AEtSaPyua9XzdMTUxjCs= -github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= +github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1 h1:Xp4zOR85XFFtM7Eif945BeSmDf30hbdijbeNSuy92Bg= +github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= github.com/milvus-io/pulsar-client-go v0.6.10 h1:eqpJjU+/QX0iIhEo3nhOqMNXL+TyInAs1IAHZCrCM/A= github.com/milvus-io/pulsar-client-go v0.6.10/go.mod h1:lQqCkgwDF8YFYjKA+zOheTk1tev2B+bKj5j7+nm8M1w= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= diff --git a/pkg/util/constant.go b/pkg/util/constant.go index d68ac7fce276a..a1323f7ab1e77 100644 --- a/pkg/util/constant.go +++ b/pkg/util/constant.go @@ -19,6 +19,8 @@ package util import ( "strings" + "github.com/samber/lo" + "github.com/milvus-io/milvus-proto/go-api/v2/commonpb" "github.com/milvus-io/milvus/pkg/common" "github.com/milvus-io/milvus/pkg/util/typeutil" @@ -160,6 +162,15 @@ var ( MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropPrivilegeGroup.String()), MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListPrivilegeGroups.String()), MetaStore2API(commonpb.ObjectPrivilege_PrivilegeOperatePrivilegeGroup.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadOnly.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadWrite.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterAdmin.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseReadOnly.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseReadWrite.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseAdmin.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionReadOnly.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionReadWrite.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionAdmin.String()), }, commonpb.ObjectType_User.String(): { MetaStore2API(commonpb.ObjectPrivilege_PrivilegeUpdateUser.String()), @@ -283,6 +294,97 @@ var ( commonpb.ObjectPrivilege_PrivilegeAlterDatabase.String(), commonpb.ObjectPrivilege_PrivilegeFlush.String(), } + + BuiltinPrivilegeGroups = map[string][]string{ + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionReadOnly.String()): CollectionReadOnlyPrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionReadWrite.String()): CollectionReadWritePrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionAdmin.String()): CollectionAdminPrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseReadOnly.String()): DatabaseReadOnlyPrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseReadWrite.String()): DatabaseReadWritePrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseAdmin.String()): DatabaseAdminPrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadOnly.String()): ClusterReadOnlyPrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadWrite.String()): ClusterReadWritePrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterAdmin.String()): ClusterAdminPrivilegeGroup, + } + + CollectionReadOnlyPrivilegeGroup = []string{ + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeQuery.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeSearch.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeIndexDetail.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGetFlushState.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGetLoadState.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGetLoadingProgress.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeHasPartition.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeShowPartitions.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDescribeCollection.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDescribeAlias.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGetStatistics.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListAliases.String()), + } + + CollectionReadWritePrivilegeGroup = append(CollectionReadOnlyPrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeLoad.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeRelease.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeInsert.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDelete.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeUpsert.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeImport.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeFlush.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCompaction.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeLoadBalance.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeRenameCollection.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateIndex.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropIndex.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreatePartition.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropPartition.String()), + ) + + CollectionAdminPrivilegeGroup = append(CollectionReadWritePrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateAlias.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropAlias.String()), + ) + + DatabaseReadOnlyPrivilegeGroup = []string{ + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeShowCollections.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDescribeDatabase.String()), + } + + DatabaseReadWritePrivilegeGroup = append(DatabaseReadOnlyPrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeAlterDatabase.String()), + ) + + DatabaseAdminPrivilegeGroup = append(DatabaseReadWritePrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateCollection.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropCollection.String()), + ) + + ClusterReadOnlyPrivilegeGroup = []string{ + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListDatabases.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeSelectOwnership.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeSelectUser.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDescribeResourceGroup.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListResourceGroups.String()), + } + + ClusterReadWritePrivilegeGroup = append(ClusterReadOnlyPrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeFlushAll.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeTransferNode.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeTransferReplica.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeUpdateResourceGroups.String()), + ) + + ClusterAdminPrivilegeGroup = append(ClusterReadWritePrivilegeGroup, + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeBackupRBAC.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeRestoreRBAC.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateDatabase.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropDatabase.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateOwnership.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropOwnership.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeManageOwnership.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateResourceGroup.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropResourceGroup.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeUpdateUser.String()), + ) ) // StringSet convert array to map for conveniently check if the array contains an element @@ -344,6 +446,12 @@ func IsPrivilegeNameDefined(name string) bool { return PrivilegeNameForMetastore(name) != "" } +func IsBuiltinPrivilegeGroup(name string) bool { + dbPrivilege := PrivilegeGroupWord + name + _, ok := commonpb.ObjectPrivilege_value[dbPrivilege] + return ok +} + func PrivilegeGroupNameForMetastore(name string) string { return PrivilegeGroupWord + name } @@ -360,3 +468,12 @@ func IsBuiltinRole(roleName string) bool { } return false } + +func GetObjectType(privName string) string { + for objectType, privs := range ObjectPrivileges { + if lo.Contains(privs, privName) { + return objectType + } + } + return "" +} diff --git a/pkg/util/paramtable/component_param.go b/pkg/util/paramtable/component_param.go index 3040555bebb47..60a40b70b010d 100644 --- a/pkg/util/paramtable/component_param.go +++ b/pkg/util/paramtable/component_param.go @@ -74,6 +74,7 @@ type ComponentParam struct { HTTPCfg httpConfig LogCfg logConfig RoleCfg roleConfig + RbacConfig rbacConfig RootCoordGrpcServerCfg GrpcServerConfig ProxyGrpcServerCfg GrpcServerConfig @@ -124,6 +125,7 @@ func (p *ComponentParam) init(bt *BaseTable) { p.HTTPCfg.init(bt) p.LogCfg.init(bt) p.RoleCfg.init(bt) + p.RbacConfig.init(bt) p.GpuConfig.init(bt) p.RootCoordGrpcServerCfg.Init("rootCoord", bt) diff --git a/pkg/util/paramtable/rbac_config_test.go b/pkg/util/paramtable/rbac_config_test.go new file mode 100644 index 0000000000000..803fceabe2896 --- /dev/null +++ b/pkg/util/paramtable/rbac_config_test.go @@ -0,0 +1,41 @@ +// Licensed to the LF AI & Data foundation under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package paramtable + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/milvus-io/milvus/pkg/util" +) + +func TestRbacConfig_Init(t *testing.T) { + params := ComponentParam{} + params.Init(NewBaseTable(SkipRemote(true))) + cfg := ¶ms.RbacConfig + assert.Equal(t, cfg.Enabled.GetAsBool(), false) + assert.Equal(t, cfg.ClusterReadOnlyPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["ClusterReadOnly"]) + assert.Equal(t, cfg.ClusterReadWritePrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["ClusterReadWrite"]) + assert.Equal(t, cfg.ClusterAdminPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["ClusterAdmin"]) + assert.Equal(t, cfg.DBReadOnlyPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["DatabaseReadOnly"]) + assert.Equal(t, cfg.DBReadWritePrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["DatabaseReadWrite"]) + assert.Equal(t, cfg.DBAdminPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["DatabaseAdmin"]) + assert.Equal(t, cfg.CollectionReadOnlyPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["CollectionReadOnly"]) + assert.Equal(t, cfg.CollectionReadWritePrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["CollectionReadWrite"]) + assert.Equal(t, cfg.CollectionAdminPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["CollectionAdmin"]) +} diff --git a/pkg/util/paramtable/rbac_param.go b/pkg/util/paramtable/rbac_param.go new file mode 100644 index 0000000000000..34e0bfb7dbc29 --- /dev/null +++ b/pkg/util/paramtable/rbac_param.go @@ -0,0 +1,114 @@ +package paramtable + +import ( + "strings" + + "github.com/milvus-io/milvus/pkg/util" +) + +type rbacConfig struct { + Enabled ParamItem `refreshable:"false"` + ClusterReadOnlyPrivileges ParamItem `refreshable:"false"` + ClusterReadWritePrivileges ParamItem `refreshable:"false"` + ClusterAdminPrivileges ParamItem `refreshable:"false"` + + DBReadOnlyPrivileges ParamItem `refreshable:"false"` + DBReadWritePrivileges ParamItem `refreshable:"false"` + DBAdminPrivileges ParamItem `refreshable:"false"` + + CollectionReadOnlyPrivileges ParamItem `refreshable:"false"` + CollectionReadWritePrivileges ParamItem `refreshable:"false"` + CollectionAdminPrivileges ParamItem `refreshable:"false"` +} + +func (p *rbacConfig) init(base *BaseTable) { + p.Enabled = ParamItem{ + Key: "common.security.rbac.overrideBuiltInPrivilgeGroups.enabled", + DefaultValue: "false", + Version: "2.4.16", + Doc: "Whether to override build-in privilege groups", + Export: true, + } + p.Enabled.Init(base.mgr) + + p.ClusterReadOnlyPrivileges = ParamItem{ + Key: "common.security.rbac.cluster.readonly.privileges", + DefaultValue: strings.Join(util.ClusterReadOnlyPrivilegeGroup, ","), + Version: "2.4.16", + Doc: "Cluster level readonly privileges", + Export: true, + } + p.ClusterReadOnlyPrivileges.Init(base.mgr) + + p.ClusterReadWritePrivileges = ParamItem{ + Key: "common.security.rbac.cluster.readwrite.privileges", + DefaultValue: strings.Join(util.ClusterReadWritePrivilegeGroup, ","), + Version: "2.4.16", + Doc: "Cluster level readwrite privileges", + Export: true, + } + p.ClusterReadWritePrivileges.Init(base.mgr) + + p.ClusterAdminPrivileges = ParamItem{ + Key: "common.security.rbac.cluster.admin.privileges", + DefaultValue: strings.Join(util.ClusterAdminPrivilegeGroup, ","), + Version: "2.4.16", + Doc: "Cluster level admin privileges", + Export: true, + } + p.ClusterAdminPrivileges.Init(base.mgr) + + p.DBReadOnlyPrivileges = ParamItem{ + Key: "common.security.rbac.database.readonly.privileges", + DefaultValue: strings.Join(util.DatabaseReadOnlyPrivilegeGroup, ","), + Version: "2.4.16", + Doc: "Database level readonly privileges", + Export: true, + } + p.DBReadOnlyPrivileges.Init(base.mgr) + + p.DBReadWritePrivileges = ParamItem{ + Key: "common.security.rbac.database.readwrite.privileges", + DefaultValue: strings.Join(util.DatabaseReadWritePrivilegeGroup, ","), + Version: "2.4.16", + Doc: "Database level readwrite privileges", + Export: true, + } + p.DBReadWritePrivileges.Init(base.mgr) + + p.DBAdminPrivileges = ParamItem{ + Key: "common.security.rbac.database.admin.privileges", + DefaultValue: strings.Join(util.DatabaseAdminPrivilegeGroup, ","), + Version: "2.4.16", + Doc: "Database level admin privileges", + Export: true, + } + p.DBAdminPrivileges.Init(base.mgr) + + p.CollectionReadOnlyPrivileges = ParamItem{ + Key: "common.security.rbac.collection.readonly.privileges", + DefaultValue: strings.Join(util.CollectionReadOnlyPrivilegeGroup, ","), + Version: "2.4.16", + Doc: "Collection level readonly privileges", + Export: true, + } + p.CollectionReadOnlyPrivileges.Init(base.mgr) + + p.CollectionReadWritePrivileges = ParamItem{ + Key: "common.security.rbac.collection.readwrite.privileges", + DefaultValue: strings.Join(util.CollectionReadWritePrivilegeGroup, ","), + Version: "2.4.16", + Doc: "Collection level readwrite privileges", + Export: true, + } + p.CollectionReadWritePrivileges.Init(base.mgr) + + p.CollectionAdminPrivileges = ParamItem{ + Key: "common.security.rbac.collection.admin.privileges", + DefaultValue: strings.Join(util.CollectionAdminPrivilegeGroup, ","), + Version: "2.4.16", + Doc: "Collection level admin privileges", + Export: true, + } + p.CollectionAdminPrivileges.Init(base.mgr) +} diff --git a/tests/integration/rbac/privilege_group_test.go b/tests/integration/rbac/privilege_group_test.go index bea3af0514737..1f1b13d41b2f4 100644 --- a/tests/integration/rbac/privilege_group_test.go +++ b/tests/integration/rbac/privilege_group_test.go @@ -18,8 +18,10 @@ package rbac import ( "context" "fmt" + "strings" "testing" + "github.com/samber/lo" "github.com/stretchr/testify/suite" "github.com/milvus-io/milvus-proto/go-api/v2/commonpb" @@ -46,10 +48,10 @@ func (s *PrivilegeGroupTestSuite) TestBuiltinPrivilegeGroup() { ctx := GetContext(context.Background(), "root:123456") // Test empty RBAC content - resp, err := s.Cluster.Proxy.BackupRBAC(ctx, &milvuspb.BackupRBACMetaRequest{}) + backupResp, err := s.Cluster.Proxy.BackupRBAC(ctx, &milvuspb.BackupRBACMetaRequest{}) s.NoError(err) - s.True(merr.Ok(resp.GetStatus())) - s.Equal("", resp.GetRBACMeta().String()) + s.True(merr.Ok(backupResp.GetStatus())) + s.Equal("", backupResp.GetRBACMeta().String()) // Generate some RBAC content roleName := "test_role" @@ -59,24 +61,130 @@ func (s *PrivilegeGroupTestSuite) TestBuiltinPrivilegeGroup() { s.NoError(err) s.True(merr.Ok(createRoleResp)) - s.operatePrivilege(ctx, roleName, "ReadOnly", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant) - s.operatePrivilege(ctx, roleName, "ReadWrite", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant) - s.operatePrivilege(ctx, roleName, "Admin", commonpb.ObjectType_Global.String(), milvuspb.OperatePrivilegeType_Grant) + resp, _ := s.operatePrivilege(ctx, roleName, "ReadOnly", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilege(ctx, roleName, "ReadWrite", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilege(ctx, roleName, "Admin", commonpb.ObjectType_Global.String(), milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + + for _, builtinGroup := range lo.Keys(util.BuiltinPrivilegeGroups) { + fmt.Println("!!! builtinGroup: ", builtinGroup) + resp, _ = s.operatePrivilege(ctx, roleName, builtinGroup, commonpb.ObjectType_Global.String(), milvuspb.OperatePrivilegeType_Grant) + s.False(merr.Ok(resp)) + } s.validateGrants(ctx, roleName, commonpb.ObjectType_Global.String(), 1) s.validateGrants(ctx, roleName, commonpb.ObjectType_Collection.String(), 2) } -/* -create group1: query, search -grant insert to role -> role: insert -grant group1 to role -> role: insert, group1(query, search) -create group2: query, delete -grant group2 to role -> role: insert, group1(query, search), group2(query, delete) -add query, load to group1 -> group1: query, search, load -> role: insert, group1(query, search, load), group2(query, delete) -remove query from group1 -> group1: search, load -> role: insert, group1(search, load), group2(query, delete), role still have query privilege because of group2 granted. -*/ -func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() { +func (s *PrivilegeGroupTestSuite) TestInvalidPrivilegeGroup() { + ctx := GetContext(context.Background(), "root:123456") + + createResp, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{ + GroupName: "", + }) + s.NoError(err) + s.False(merr.Ok(createResp)) + + // create group %$ will fail + createResp, _ = s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{ + GroupName: "%$", + }) + s.False(merr.Ok(createResp)) + + createResp, _ = s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{ + GroupName: "a&", + }) + s.False(merr.Ok(createResp)) + + createResp, _ = s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{ + GroupName: strings.Repeat("a", 300), + }) + s.False(merr.Ok(createResp)) + + // drop group %$ will fail + dropResp, _ := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ + GroupName: "%$", + }) + s.False(merr.Ok(dropResp)) + + dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ + GroupName: "group1", + }) + s.NoError(err) + s.True(merr.Ok(dropResp)) + + dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ + GroupName: "", + }) + s.NoError(err) + s.False(merr.Ok(dropResp)) + + operateResp, err := s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{ + GroupName: "", + }) + s.NoError(err) + s.False(merr.Ok(operateResp)) + + operateResp, err = s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{ + GroupName: "group1", + Privileges: []*milvuspb.PrivilegeEntity{{Name: "123"}}, + }) + s.NoError(err) + s.False(merr.Ok(operateResp)) +} + +func (s *PrivilegeGroupTestSuite) TestGrantV2BuiltinPrivilegeGroup() { + ctx := GetContext(context.Background(), "root:123456") + + roleName := "test_role" + createRoleResp, err := s.Cluster.Proxy.CreateRole(ctx, &milvuspb.CreateRoleRequest{ + Entity: &milvuspb.RoleEntity{Name: roleName}, + }) + s.NoError(err) + s.True(merr.Ok(createRoleResp)) + + resp, _ := s.operatePrivilegeV2(ctx, roleName, "ClusterReadOnly", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "ClusterReadWrite", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "ClusterAdmin", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseReadOnly", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseReadWrite", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseAdmin", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionReadOnly", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionReadWrite", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionAdmin", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + + resp, _ = s.operatePrivilegeV2(ctx, roleName, "ClusterAdmin", "db1", util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.False(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "ClusterAdmin", "db1", "col1", milvuspb.OperatePrivilegeType_Grant) + s.False(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "ClusterAdmin", util.AnyWord, "col1", milvuspb.OperatePrivilegeType_Grant) + s.False(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseAdmin", "db1", util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseAdmin", "db1", "col1", milvuspb.OperatePrivilegeType_Grant) + s.False(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseAdmin", util.AnyWord, "col1", milvuspb.OperatePrivilegeType_Grant) + s.False(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionAdmin", "db1", util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionAdmin", "db1", "col1", milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionAdmin", util.AnyWord, "col1", milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) +} + +func (s *PrivilegeGroupTestSuite) TestGrantV2CustomPrivilegeGroup() { ctx := GetContext(context.Background(), "root:123456") // Helper function to operate on privilege groups @@ -123,12 +231,14 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() { }) s.NoError(err) s.True(merr.Ok(createRoleResp)) - s.operatePrivilege(ctx, role, "Insert", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant) - s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 1) + resp, _ := s.operatePrivilegeV2(ctx, role, "Insert", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + s.validateGrants(ctx, role, commonpb.ObjectType_Global.String(), 1) // grant group1 to role -> role: insert, group1(query, search) - s.operatePrivilege(ctx, role, "group1", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant) - s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 2) + resp, _ = s.operatePrivilegeV2(ctx, role, "group1", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + s.validateGrants(ctx, role, commonpb.ObjectType_Global.String(), 2) // create group2: query, delete createResp2, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{ @@ -144,8 +254,9 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() { validatePrivilegeGroup("group2", 2) // grant group2 to role -> role: insert, group1(query, search), group2(query, delete) - s.operatePrivilege(ctx, role, "group2", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant) - s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 3) + resp, _ = s.operatePrivilegeV2(ctx, role, "group2", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + s.validateGrants(ctx, role, commonpb.ObjectType_Global.String(), 3) // add query, load to group1 -> group1: query, search, load -> role: insert, group1(query, search, load), group2(query, delete) operatePrivilegeGroup("group1", milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, []*milvuspb.PrivilegeEntity{ @@ -153,14 +264,22 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() { {Name: "Load"}, }) validatePrivilegeGroup("group1", 3) - s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 3) + s.validateGrants(ctx, role, commonpb.ObjectType_Global.String(), 3) + + // add different object type privileges to group1 is not allowed + resp, _ = s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{ + GroupName: "group1", + Type: milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, + Privileges: []*milvuspb.PrivilegeEntity{{Name: "CreateCollection"}}, + }) + s.Error(merr.Error(resp)) // remove query from group1 -> group1: search, load -> role: insert, group1(search, load), group2(query, delete), role still have query privilege because of group2 granted. operatePrivilegeGroup("group1", milvuspb.OperatePrivilegeGroupType_RemovePrivilegesFromGroup, []*milvuspb.PrivilegeEntity{ {Name: "Query"}, }) validatePrivilegeGroup("group1", 2) - s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 3) + s.validateGrants(ctx, role, commonpb.ObjectType_Global.String(), 3) // Drop the group during any role usage will cause error dropResp, _ := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ @@ -169,9 +288,12 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() { s.Error(merr.Error(dropResp)) // Revoke privilege group and privileges - s.operatePrivilege(ctx, role, "group1", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Revoke) - s.operatePrivilege(ctx, role, "group2", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Revoke) - s.operatePrivilege(ctx, role, "Insert", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Revoke) + resp, _ = s.operatePrivilegeV2(ctx, role, "group1", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, role, "group2", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + s.True(merr.Ok(resp)) + resp, _ = s.operatePrivilegeV2(ctx, role, "Insert", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + s.True(merr.Ok(resp)) // Drop the privilege group after revoking the privilege will succeed dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ @@ -187,54 +309,32 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() { s.True(merr.Ok(dropResp)) // Validate the group was dropped - resp, err := s.Cluster.Proxy.ListPrivilegeGroups(ctx, &milvuspb.ListPrivilegeGroupsRequest{}) - s.NoError(err) - s.Equal(0, len(resp.PrivilegeGroups)) - - // Drop the role - dropRoleResp, err := s.Cluster.Proxy.DropRole(ctx, &milvuspb.DropRoleRequest{ - RoleName: role, - }) + listResp, err := s.Cluster.Proxy.ListPrivilegeGroups(ctx, &milvuspb.ListPrivilegeGroupsRequest{}) s.NoError(err) - s.True(merr.Ok(dropRoleResp)) -} - -func (s *PrivilegeGroupTestSuite) TestInvalidPrivilegeGroup() { - ctx := GetContext(context.Background(), "root:123456") + s.Equal(len(util.BuiltinPrivilegeGroups), len(listResp.PrivilegeGroups)) - createResp, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{ - GroupName: "", - }) - s.NoError(err) - s.False(merr.Ok(createResp)) + // validate edge cases + resp, _ = s.operatePrivilegeV2(ctx, role, util.AnyWord, util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) - dropResp, err := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ - GroupName: "group1", - }) - s.NoError(err) - s.True(merr.Ok(dropResp)) + resp, _ = s.operatePrivilegeV2(ctx, role, util.AnyWord, util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + s.True(merr.Ok(resp)) - dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ - GroupName: "", - }) - s.NoError(err) - s.False(merr.Ok(dropResp)) + resp, _ = s.operatePrivilegeV2(ctx, role, "group3", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + s.False(merr.Ok(resp)) - operateResp, err := s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{ - GroupName: "", - }) - s.NoError(err) - s.False(merr.Ok(operateResp)) + resp, _ = s.operatePrivilegeV2(ctx, role, "%$", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.False(merr.Ok(resp)) - operateResp, err = s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{ - GroupName: "group1", - Privileges: []*milvuspb.PrivilegeEntity{{Name: "123"}}, + // Drop the role + dropRoleResp, err := s.Cluster.Proxy.DropRole(ctx, &milvuspb.DropRoleRequest{ + RoleName: role, }) s.NoError(err) - s.False(merr.Ok(operateResp)) + s.True(merr.Ok(dropRoleResp)) } -func (s *PrivilegeGroupTestSuite) operatePrivilege(ctx context.Context, role, privilege, objectType string, operateType milvuspb.OperatePrivilegeType) { +func (s *PrivilegeGroupTestSuite) operatePrivilege(ctx context.Context, role, privilege, objectType string, operateType milvuspb.OperatePrivilegeType) (*commonpb.Status, error) { resp, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ Type: operateType, Entity: &milvuspb.GrantEntity{ @@ -248,8 +348,25 @@ func (s *PrivilegeGroupTestSuite) operatePrivilege(ctx context.Context, role, pr }, }, }) - s.NoError(err) - s.True(merr.Ok(resp)) + return resp, err +} + +func (s *PrivilegeGroupTestSuite) operatePrivilegeV2(ctx context.Context, role, privilege, dbName, collectionName string, operateType milvuspb.OperatePrivilegeType) (*commonpb.Status, error) { + resp, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ + Type: operateType, + Entity: &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: role}, + Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()}, + ObjectName: collectionName, + DbName: dbName, + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: util.UserRoot}, + Privilege: &milvuspb.PrivilegeEntity{Name: privilege}, + }, + }, + Version: "v2", + }) + return resp, err } func (s *PrivilegeGroupTestSuite) validateGrants(ctx context.Context, roleName, objectType string, expectedCount int) { @@ -261,7 +378,6 @@ func (s *PrivilegeGroupTestSuite) validateGrants(ctx context.Context, roleName, DbName: util.AnyWord, }, }) - fmt.Println("!!!validateGrants: ", resp) s.NoError(err) s.True(merr.Ok(resp.GetStatus())) s.Len(resp.GetEntities(), expectedCount) diff --git a/tests/integration/rbac/rbac_backup_test.go b/tests/integration/rbac/rbac_backup_test.go index 5bb1e2adb7260..49e61ee4dcd5d 100644 --- a/tests/integration/rbac/rbac_backup_test.go +++ b/tests/integration/rbac/rbac_backup_test.go @@ -78,11 +78,15 @@ func (s *RBACBackupTestSuite) TestBackup() { Type: operateType, Entity: &milvuspb.GrantEntity{ Role: &milvuspb.RoleEntity{Name: role}, - Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()}, - ObjectName: objectName, + Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()}, + ObjectName: collectionName, DbName: dbName, - Grantor: &milvuspb.GrantorEntity{User: &milvuspb.UserEntity{Name: util.UserRoot}, Privilege: &milvuspb.PrivilegeEntity{Name: privilege}}, + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: util.UserRoot}, + Privilege: &milvuspb.PrivilegeEntity{Name: privilege}, + }, }, + Version: "v2", }) s.NoError(err) s.True(merr.Ok(resp))