From f99530a325af803efcce6cdb9440d564ef275558 Mon Sep 17 00:00:00 2001 From: shaoting-huang Date: Thu, 14 Nov 2024 16:44:38 +0800 Subject: [PATCH] RBAC grant/revoke privilege v2 Signed-off-by: shaoting-huang --- go.mod | 2 +- go.sum | 4 +- .../distributed/proxy/httpserver/constant.go | 2 + .../proxy/httpserver/handler_v2.go | 33 +++ .../proxy/httpserver/handler_v2_test.go | 8 +- .../proxy/httpserver/request_v2.go | 7 + internal/mocks/mock_proxy.go | 59 ++++ internal/proxy/impl.go | 102 ++++++- internal/proxy/privilege_interceptor.go | 4 +- internal/proxy/privilege_interceptor_test.go | 42 +++ internal/proxy/proxy_test.go | 102 +++++++ internal/proxy/util.go | 26 ++ internal/rootcoord/root_coord.go | 212 +++++++++----- pkg/go.mod | 2 +- pkg/go.sum | 4 +- pkg/util/constant.go | 11 + .../integration/rbac/privilege_group_test.go | 260 +++++++++++++++++- 17 files changed, 787 insertions(+), 93 deletions(-) diff --git a/go.mod b/go.mod index 0b6c7c89aae3a..63da99beea9a6 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,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.3.4-0.20241114133823-d3506c6f465c + github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20241120015424-93892e628c69 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 c91589f7f5f43..8542171ec5aaa 100644 --- a/go.sum +++ b/go.sum @@ -628,8 +628,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.3.4-0.20241114133823-d3506c6f465c h1:Ay5w6sTE1QxCydCqqW5N44EcJrMqaqbL5zcp2vclkOw= -github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20241114133823-d3506c6f465c/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= +github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20241120015424-93892e628c69 h1:Qt0Bv2Fum3EX3OlkuQYHJINBzeU4oEuHy2lXSfB/gZw= +github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20241120015424-93892e628c69/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= github.com/milvus-io/pulsar-client-go v0.12.1 h1:O2JZp1tsYiO7C0MQ4hrUY/aJXnn2Gry6hpm7UodghmE= github.com/milvus-io/pulsar-client-go v0.12.1/go.mod h1:dkutuH4oS2pXiGm+Ti7fQZ4MRjrMPZ8IJeEGAWMeckk= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= diff --git a/internal/distributed/proxy/httpserver/constant.go b/internal/distributed/proxy/httpserver/constant.go index 73cc91025acca..2dc24a634b6d8 100644 --- a/internal/distributed/proxy/httpserver/constant.go +++ b/internal/distributed/proxy/httpserver/constant.go @@ -59,6 +59,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 f8e7fb3bb8174..9cdb44c5ba900 100644 --- a/internal/distributed/proxy/httpserver/handler_v2.go +++ b/internal/distributed/proxy/httpserver/handler_v2.go @@ -147,6 +147,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)))) @@ -1810,6 +1812,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 74584b8a70a08..6658f86b185ae 100644 --- a/internal/distributed/proxy/httpserver/handler_v2_test.go +++ b/internal/distributed/proxy/httpserver/handler_v2_test.go @@ -1454,7 +1454,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() @@ -1527,6 +1527,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 f90ef1b69c731..54c77bd75cdac 100644 --- a/internal/distributed/proxy/httpserver/request_v2.go +++ b/internal/distributed/proxy/httpserver/request_v2.go @@ -294,6 +294,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 d02c9957188f2..28b1f9815489f 100644 --- a/internal/mocks/mock_proxy.go +++ b/internal/mocks/mock_proxy.go @@ -5137,6 +5137,65 @@ 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) + + if len(ret) == 0 { + panic("no return value specified for OperatePrivilegeV2") + } + + 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 9ece14dac79c1..f889b517af6a7 100644 --- a/internal/proxy/impl.go +++ b/internal/proxy/impl.go @@ -5332,6 +5332,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() @@ -6576,8 +6657,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{} @@ -6605,8 +6689,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{} @@ -6663,8 +6750,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 a3e066ba00f31..b8eed64bdf20c 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -2709,6 +2709,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 @@ -4377,6 +4379,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 326823fc32ec4..1468a283fbf7f 100644 --- a/internal/proxy/util.go +++ b/internal/proxy/util.go @@ -156,6 +156,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/root_coord.go b/internal/rootcoord/root_coord.go index 310d7b5e627bd..37e2a241bb0ba 100644 --- a/internal/rootcoord/root_coord.go +++ b/internal/rootcoord/root_coord.go @@ -22,6 +22,7 @@ import ( "math/rand" "os" "strconv" + "strings" "sync" "time" @@ -2554,44 +2555,70 @@ 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") - } - 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") - } - if util.IsAnyWord(entity.Privilege.Name) { +func (c *Core) isValidPrivilege(privilegeName string, object string) error { + if util.IsAnyWord(privilegeName) { return nil } // 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) + 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 object[%s]", entity.Privilege.Name, object) + return fmt.Errorf("not found the privilege name[%s] in object[%s]", privilegeName, object) +} + +func (c *Core) isValidPrivilegeV2(privilegeName, dbName, collectionName string) error { + if util.IsAnyWord(privilegeName) { + return nil + } + 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 + } } // OperatePrivilege operate the privilege, including grant and revoke @@ -2608,37 +2635,29 @@ 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 object name if it is global object type - if in.Entity.Object.Name == commonpb.ObjectType_Global.String() { - in.Entity.ObjectName = util.AnyWord + switch in.Version { + case "v2": + if err := c.isValidPrivilegeV2(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(in.Entity.Grantor.Privilege.Name, in.Entity.Object.Name); err != nil { + ctxLog.Error("", zap.Error(err)) + return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil + } + // set up object name if it is global object type + 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) @@ -2709,6 +2728,41 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile return merr.Success(), nil } +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(privName string) (string, error) { // if it is built-in privilege, return the privilege name directly if util.IsPrivilegeNameDefined(privName) { @@ -3151,6 +3205,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 @@ -3162,11 +3224,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 { @@ -3250,43 +3312,43 @@ func (c *Core) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePr 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(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 { privName := grant.Grantor.Privilege.Name if privGroup, exists := groups[privName]; !exists { - metaName, err := c.getMetastorePrivilegeName(privName) + newGrant, err := createGrantEntity(grant, privName) if err != nil { return nil, err } - newGrants = append(newGrants, &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, - }) + newGrants = append(newGrants, newGrant) } else { for _, priv := range privGroup { - metaName, err := c.getMetastorePrivilegeName(priv.Name) + newGrant, err := createGrantEntity(grant, priv.Name) if err != nil { return nil, err } - newGrants = append(newGrants, &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, - }) + newGrants = append(newGrants, newGrant) } } } diff --git a/pkg/go.mod b/pkg/go.mod index 60b97e39a1a53..c97f64092743c 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -14,7 +14,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/json-iterator/go v1.1.12 github.com/klauspost/compress v1.17.7 - github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20241114133823-d3506c6f465c + github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20241120015424-93892e628c69 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 c1f60d82a59e7..e5876f5b27064 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -488,8 +488,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.3.4-0.20241114133823-d3506c6f465c h1:Ay5w6sTE1QxCydCqqW5N44EcJrMqaqbL5zcp2vclkOw= -github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20241114133823-d3506c6f465c/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= +github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20241120015424-93892e628c69 h1:Qt0Bv2Fum3EX3OlkuQYHJINBzeU4oEuHy2lXSfB/gZw= +github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20241120015424-93892e628c69/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= github.com/milvus-io/pulsar-client-go v0.12.1 h1:O2JZp1tsYiO7C0MQ4hrUY/aJXnn2Gry6hpm7UodghmE= github.com/milvus-io/pulsar-client-go v0.12.1/go.mod h1:dkutuH4oS2pXiGm+Ti7fQZ4MRjrMPZ8IJeEGAWMeckk= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= diff --git a/pkg/util/constant.go b/pkg/util/constant.go index 920a50171f70c..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" @@ -466,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/tests/integration/rbac/privilege_group_test.go b/tests/integration/rbac/privilege_group_test.go index 2a3f814df9e49..f533273291e4e 100644 --- a/tests/integration/rbac/privilege_group_test.go +++ b/tests/integration/rbac/privilege_group_test.go @@ -17,7 +17,7 @@ package rbac import ( "context" - "fmt" + "strings" "testing" "github.com/stretchr/testify/suite" @@ -157,6 +157,22 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() { s.operatePrivilege(ctx, role, "group2", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant) s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 3) + // invalid privilege + errResp, _ := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ + Type: milvuspb.OperatePrivilegeType_Grant, + Entity: &milvuspb.GrantEntity{ + Role: &milvuspb.RoleEntity{Name: role}, + Object: &milvuspb.ObjectEntity{Name: "invalid"}, + ObjectName: util.AnyWord, + DbName: util.AnyWord, + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: util.UserRoot}, + Privilege: &milvuspb.PrivilegeEntity{Name: "Insert"}, + }, + }, + }) + s.False(merr.Ok(errResp)) + // 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{ {Name: "Query"}, @@ -218,7 +234,29 @@ func (s *PrivilegeGroupTestSuite) TestInvalidPrivilegeGroup() { s.NoError(err) s.False(merr.Ok(createResp)) - dropResp, err := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ + // 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) @@ -244,6 +282,205 @@ func (s *PrivilegeGroupTestSuite) TestInvalidPrivilegeGroup() { 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 + operatePrivilegeGroup := func(groupName string, operateType milvuspb.OperatePrivilegeGroupType, privileges []*milvuspb.PrivilegeEntity) { + resp, err := s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{ + GroupName: groupName, + Type: operateType, + Privileges: privileges, + }) + s.NoError(err) + s.True(merr.Ok(resp)) + } + + // Helper function to list privilege groups and return the target group and its privileges + validatePrivilegeGroup := func(groupName string, privileges int) []*milvuspb.PrivilegeEntity { + resp, err := s.Cluster.Proxy.ListPrivilegeGroups(ctx, &milvuspb.ListPrivilegeGroupsRequest{}) + s.NoError(err) + for _, privGroup := range resp.PrivilegeGroups { + if privGroup.GroupName == groupName { + s.Equal(privileges, len(privGroup.Privileges)) + return privGroup.Privileges + } + } + return nil + } + + // create group1: query, search + createResp, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{ + GroupName: "group1", + }) + s.NoError(err) + s.True(merr.Ok(createResp)) + validatePrivilegeGroup("group1", 0) + operatePrivilegeGroup("group1", milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, []*milvuspb.PrivilegeEntity{ + {Name: "Query"}, + {Name: "Search"}, + }) + validatePrivilegeGroup("group1", 2) + + // grant insert to role -> role: insert + role := "role1" + createRoleResp, err := s.Cluster.Proxy.CreateRole(ctx, &milvuspb.CreateRoleRequest{ + Entity: &milvuspb.RoleEntity{Name: role}, + }) + s.NoError(err) + s.True(merr.Ok(createRoleResp)) + 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) + 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{ + GroupName: "group2", + }) + s.NoError(err) + s.True(merr.Ok(createResp2)) + validatePrivilegeGroup("group2", 0) + operatePrivilegeGroup("group2", milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, []*milvuspb.PrivilegeEntity{ + {Name: "Query"}, + {Name: "Delete"}, + }) + validatePrivilegeGroup("group2", 2) + + // grant group2 to role -> role: insert, group1(query, search), group2(query, delete) + 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{ + {Name: "Query"}, + {Name: "Load"}, + }) + validatePrivilegeGroup("group1", 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_Global.String(), 3) + + // Drop the group during any role usage will cause error + dropResp, _ := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ + GroupName: "group1", + }) + s.Error(merr.Error(dropResp)) + + // Revoke privilege group and privileges + 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{ + GroupName: "group1", + }) + s.NoError(err) + s.True(merr.Ok(dropResp)) + + dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ + GroupName: "group2", + }) + s.NoError(err) + s.True(merr.Ok(dropResp)) + + // Validate the group was dropped + listResp, err := s.Cluster.Proxy.ListPrivilegeGroups(ctx, &milvuspb.ListPrivilegeGroupsRequest{}) + s.NoError(err) + s.Equal(0, len(listResp.PrivilegeGroups)) + + // validate edge cases + resp, _ = s.operatePrivilegeV2(ctx, role, util.AnyWord, util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.True(merr.Ok(resp)) + + resp, _ = s.operatePrivilegeV2(ctx, role, util.AnyWord, util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + s.True(merr.Ok(resp)) + + resp, _ = s.operatePrivilegeV2(ctx, role, "group3", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + s.False(merr.Ok(resp)) + + resp, _ = s.operatePrivilegeV2(ctx, role, "%$", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + s.False(merr.Ok(resp)) + + // Drop the role + dropRoleResp, err := s.Cluster.Proxy.DropRole(ctx, &milvuspb.DropRoleRequest{ + RoleName: role, + }) + s.NoError(err) + s.True(merr.Ok(dropRoleResp)) +} + func (s *PrivilegeGroupTestSuite) operatePrivilege(ctx context.Context, role, privilege, objectType string, operateType milvuspb.OperatePrivilegeType) { resp, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ Type: operateType, @@ -262,6 +499,24 @@ func (s *PrivilegeGroupTestSuite) operatePrivilege(ctx context.Context, role, pr s.True(merr.Ok(resp)) } +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) { resp, err := s.Cluster.Proxy.SelectGrant(ctx, &milvuspb.SelectGrantRequest{ Entity: &milvuspb.GrantEntity{ @@ -271,7 +526,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)