From 652b6172bcaea254bd07b4b95e9589bbc545e5fc Mon Sep 17 00:00:00 2001 From: shaoting-huang Date: Wed, 6 Nov 2024 14:57:29 +0800 Subject: [PATCH] RBAC custom privilege group Signed-off-by: shaoting-huang --- client/go.mod | 2 +- go.mod | 3 + go.sum | 8 +- internal/datacoord/mock_test.go | 16 + internal/datanode/importv2/pool_test.go | 3 +- .../distributed/proxy/httpserver/constant.go | 46 +-- .../proxy/httpserver/handler_v2.go | 84 +++++ .../proxy/httpserver/request_v2.go | 6 + internal/distributed/proxy/service.go | 16 + .../distributed/rootcoord/client/client.go | 48 +++ .../rootcoord/client/client_test.go | 28 ++ internal/distributed/rootcoord/service.go | 16 + internal/metastore/catalog.go | 5 + internal/metastore/kv/rootcoord/kv_catalog.go | 141 ++++++++- .../metastore/kv/rootcoord/kv_catalog_test.go | 175 ++++++++++- .../kv/rootcoord/rootcoord_constant.go | 7 + .../metastore/mocks/mock_rootcoord_catalog.go | 195 ++++++++++++ internal/mocks/mock_proxy.go | 220 +++++++++++++ internal/mocks/mock_rootcoord.go | 220 +++++++++++++ internal/mocks/mock_rootcoord_client.go | 296 ++++++++++++++++++ internal/proto/internal.proto | 2 + internal/proto/root_coord.proto | 4 + internal/proxy/impl.go | 121 +++++++ internal/proxy/meta_cache.go | 111 +++++-- internal/proxy/meta_cache_test.go | 50 ++- internal/proxy/mock_cache.go | 82 ++++- internal/proxy/privilege_interceptor.go | 7 +- internal/proxy/privilege_interceptor_test.go | 149 ++++++++- internal/proxy/rootcoord_mock_test.go | 16 + internal/proxy/util.go | 15 +- internal/proxy/util_test.go | 23 +- internal/rootcoord/meta_table.go | 148 +++++++++ internal/rootcoord/meta_table_test.go | 29 ++ internal/rootcoord/mock_test.go | 32 ++ internal/rootcoord/mocks/meta_table.go | 257 +++++++++++++++ internal/rootcoord/root_coord.go | 244 +++++++++++++-- internal/rootcoord/root_coord_test.go | 6 + internal/util/mock/grpc_rootcoord_client.go | 16 + pkg/go.mod | 17 +- pkg/go.sum | 30 +- pkg/metrics/rootcoord_metrics.go | 9 + pkg/util/constant.go | 21 ++ pkg/util/typeutil/cache.go | 3 + scripts/download_milvus_proto.sh | 6 +- .../integration/rbac/privilege_group_test.go | 180 ++++++++--- tests/integration/rbac/rbac_backup_test.go | 179 ++++++----- 46 files changed, 3008 insertions(+), 284 deletions(-) diff --git a/client/go.mod b/client/go.mod index 6645153fa324b..ec82b9088887e 100644 --- a/client/go.mod +++ b/client/go.mod @@ -13,7 +13,6 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.1 go.uber.org/atomic v1.10.0 - golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 ) @@ -109,6 +108,7 @@ require ( go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.20.0 // indirect golang.org/x/crypto v0.25.0 // indirect + golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.22.0 // indirect diff --git a/go.mod b/go.mod index 766a359265af5..1a88f548276fa 100644 --- a/go.mod +++ b/go.mod @@ -138,6 +138,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/mock v1.6.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/flatbuffers v2.0.8+incompatible // indirect github.com/google/s2a-go v0.1.7 // indirect @@ -274,3 +275,5 @@ replace ( ) exclude github.com/apache/pulsar-client-go/oauth2 v0.0.0-20211108044248-fe3b7c4e445b + +replace github.com/milvus-io/milvus-proto/go-api/v2 => github.com/shaoting-huang/milvus-proto/go-api/v2 v2.0.0-20241106022119-cd9f3e43c08c diff --git a/go.sum b/go.sum index 98ffb403f2456..6da578a2a9f29 100644 --- a/go.sum +++ b/go.sum @@ -363,8 +363,9 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -627,8 +628,6 @@ 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.20241025031121-4d5c88b00cf7 h1:HwAitQk+V59QdYUwwVVYHTujd4QZrebg2Cc2hmcjhAg= -github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20241025031121-4d5c88b00cf7/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= @@ -803,6 +802,8 @@ github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtm github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shaoting-huang/milvus-proto/go-api/v2 v2.0.0-20241106022119-cd9f3e43c08c h1:DhD0iqNocvSYgE6BO1XNj1KrpAoSVLm0wWTM5hUleVU= +github.com/shaoting-huang/milvus-proto/go-api/v2 v2.0.0-20241106022119-cd9f3e43c08c/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= github.com/shirou/gopsutil/v3 v3.22.9 h1:yibtJhIVEMcdw+tCTbOPiF1VcsuDeTE4utJ8Dm4c5eA= github.com/shirou/gopsutil/v3 v3.22.9/go.mod h1:bBYl1kjgEJpWpxeHmLI+dVHWtyAwfcmSBLDsp2TNT8A= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -1323,6 +1324,7 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/internal/datacoord/mock_test.go b/internal/datacoord/mock_test.go index 4108cc2e56682..d5d34ec323148 100644 --- a/internal/datacoord/mock_test.go +++ b/internal/datacoord/mock_test.go @@ -686,6 +686,22 @@ func (m *mockRootCoordClient) ListPolicy(ctx context.Context, in *internalpb.Lis return &internalpb.ListPolicyResponse{Status: &commonpb.Status{ErrorCode: commonpb.ErrorCode_Success}}, nil } +func (m *mockRootCoordClient) CreatePrivilegeGroup(ctx context.Context, req *milvuspb.CreatePrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + panic("implement me") +} + +func (m *mockRootCoordClient) DropPrivilegeGroup(ctx context.Context, req *milvuspb.DropPrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + panic("implement me") +} + +func (m *mockRootCoordClient) ListPrivilegeGroups(ctx context.Context, req *milvuspb.ListPrivilegeGroupsRequest, opts ...grpc.CallOption) (*milvuspb.ListPrivilegeGroupsResponse, error) { + panic("implement me") +} + +func (m *mockRootCoordClient) OperatePrivilegeGroup(ctx context.Context, req *milvuspb.OperatePrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + panic("implement me") +} + type mockHandler struct { meta *meta } diff --git a/internal/datanode/importv2/pool_test.go b/internal/datanode/importv2/pool_test.go index 4449a5031c812..06873c6d31ae5 100644 --- a/internal/datanode/importv2/pool_test.go +++ b/internal/datanode/importv2/pool_test.go @@ -20,10 +20,9 @@ import ( "fmt" "testing" - "github.com/stretchr/testify/assert" - "github.com/milvus-io/milvus/pkg/config" "github.com/milvus-io/milvus/pkg/util/paramtable" + "github.com/stretchr/testify/assert" ) func TestResizePools(t *testing.T) { diff --git a/internal/distributed/proxy/httpserver/constant.go b/internal/distributed/proxy/httpserver/constant.go index b064c69e6ac39..13c5d4f0ee612 100644 --- a/internal/distributed/proxy/httpserver/constant.go +++ b/internal/distributed/proxy/httpserver/constant.go @@ -9,14 +9,15 @@ import ( // v2 const ( // --- category --- - CollectionCategory = "/collections/" - EntityCategory = "/entities/" - PartitionCategory = "/partitions/" - UserCategory = "/users/" - RoleCategory = "/roles/" - IndexCategory = "/indexes/" - AliasCategory = "/aliases/" - ImportJobCategory = "/jobs/import/" + CollectionCategory = "/collections/" + EntityCategory = "/entities/" + PartitionCategory = "/partitions/" + UserCategory = "/users/" + RoleCategory = "/roles/" + IndexCategory = "/indexes/" + AliasCategory = "/aliases/" + ImportJobCategory = "/jobs/import/" + PrivilegeGroupCategory = "/privilege_groups/" ListAction = "list" HasAction = "has" @@ -37,13 +38,15 @@ const ( AdvancedSearchAction = "advanced_search" HybridSearchAction = "hybrid_search" - UpdatePasswordAction = "update_password" - GrantRoleAction = "grant_role" - RevokeRoleAction = "revoke_role" - GrantPrivilegeAction = "grant_privilege" - RevokePrivilegeAction = "revoke_privilege" - AlterAction = "alter" - GetProgressAction = "get_progress" // deprecated, keep it for compatibility, use `/v2/vectordb/jobs/import/describe` instead + UpdatePasswordAction = "update_password" + GrantRoleAction = "grant_role" + RevokeRoleAction = "revoke_role" + GrantPrivilegeAction = "grant_privilege" + RevokePrivilegeAction = "revoke_privilege" + AlterAction = "alter" + GetProgressAction = "get_progress" // deprecated, keep it for compatibility, use `/v2/vectordb/jobs/import/describe` instead + AddPrivilegesToGroupAction = "add_privileges_to_group" + RemovePrivilegesFromGroupAction = "remove_privileges_from_group" ) const ( @@ -125,11 +128,14 @@ const ( HTTPReturnRowCount = "rowCount" - HTTPReturnObjectType = "objectType" - HTTPReturnObjectName = "objectName" - HTTPReturnPrivilege = "privilege" - HTTPReturnGrantor = "grantor" - HTTPReturnDbName = "dbName" + HTTPReturnObjectType = "objectType" + HTTPReturnObjectName = "objectName" + HTTPReturnPrivilege = "privilege" + HTTPReturnGrantor = "grantor" + HTTPReturnDbName = "dbName" + HTTPReturnPrivilegeGroupName = "privilegeGroupName" + HTTPReturnPrivileges = "privileges" + HTTPReturnPrivilegeGroups = "privilegeGroups" DefaultMetricType = metric.COSINE DefaultPrimaryFieldName = "id" diff --git a/internal/distributed/proxy/httpserver/handler_v2.go b/internal/distributed/proxy/httpserver/handler_v2.go index 88a533637e984..e63767e0caa85 100644 --- a/internal/distributed/proxy/httpserver/handler_v2.go +++ b/internal/distributed/proxy/httpserver/handler_v2.go @@ -132,6 +132,13 @@ func (h *HandlersV2) RegisterRoutesToV2(router gin.IRouter) { 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)))) + // privilege group + router.POST(PrivilegeGroupCategory+CreateAction, timeoutMiddleware(wrapperPost(func() any { return &GrantReq{} }, wrapperTraceLog(h.createPrivilegeGroup)))) + router.POST(PrivilegeGroupCategory+DropAction, timeoutMiddleware(wrapperPost(func() any { return &GrantReq{} }, wrapperTraceLog(h.dropPrivilegeGroup)))) + router.POST(PrivilegeGroupCategory+ListAction, timeoutMiddleware(wrapperPost(func() any { return &GrantReq{} }, wrapperTraceLog(h.listPrivilegeGroups)))) + router.POST(PrivilegeGroupCategory+AddPrivilegesToGroupAction, timeoutMiddleware(wrapperPost(func() any { return &GrantReq{} }, wrapperTraceLog(h.addPrivilegesToGroup)))) + router.POST(PrivilegeGroupCategory+RemovePrivilegesFromGroupAction, timeoutMiddleware(wrapperPost(func() any { return &GrantReq{} }, wrapperTraceLog(h.removePrivilegesFromGroup)))) + router.POST(IndexCategory+ListAction, timeoutMiddleware(wrapperPost(func() any { return &CollectionNameReq{} }, wrapperTraceLog(h.wrapperCheckDatabase(h.listIndexes))))) router.POST(IndexCategory+DescribeAction, timeoutMiddleware(wrapperPost(func() any { return &IndexReq{} }, wrapperTraceLog(h.wrapperCheckDatabase(h.describeIndex))))) @@ -1786,6 +1793,83 @@ func (h *HandlersV2) removePrivilegeFromRole(ctx context.Context, c *gin.Context return h.operatePrivilegeToRole(ctx, c, anyReq.(*GrantReq), milvuspb.OperatePrivilegeType_Revoke, dbName) } +func (h *HandlersV2) createPrivilegeGroup(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) { + httpReq := anyReq.(*PrivilegeGroupReq) + req := &milvuspb.CreatePrivilegeGroupRequest{ + GroupName: httpReq.PrivilegeGroupName, + } + resp, err := wrapperProxy(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/CreatePrivilegeGroup", func(reqCtx context.Context, req any) (interface{}, error) { + return h.proxy.CreatePrivilegeGroup(reqCtx, req.(*milvuspb.CreatePrivilegeGroupRequest)) + }) + if err == nil { + HTTPReturn(c, http.StatusOK, wrapperReturnDefault()) + } + return resp, err +} + +func (h *HandlersV2) dropPrivilegeGroup(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) { + httpReq := anyReq.(*PrivilegeGroupReq) + req := &milvuspb.DropPrivilegeGroupRequest{ + GroupName: httpReq.PrivilegeGroupName, + } + resp, err := wrapperProxy(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/DropPrivilegeGroup", func(reqCtx context.Context, req any) (interface{}, error) { + return h.proxy.DropPrivilegeGroup(reqCtx, req.(*milvuspb.DropPrivilegeGroupRequest)) + }) + if err == nil { + HTTPReturn(c, http.StatusOK, wrapperReturnDefault()) + } + return resp, err +} + +func (h *HandlersV2) listPrivilegeGroups(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) { + req := &milvuspb.ListPrivilegeGroupsRequest{} + resp, err := wrapperProxy(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/ListPrivilegeGroups", func(reqCtx context.Context, req any) (interface{}, error) { + return h.proxy.ListPrivilegeGroups(reqCtx, req.(*milvuspb.ListPrivilegeGroupsRequest)) + }) + if err == nil { + privGroups := make([]map[string]interface{}, 0) + for _, group := range resp.(*milvuspb.ListPrivilegeGroupsResponse).PrivilegeGroups { + privileges := make([]string, len(group.Privileges)) + for i, privilege := range group.Privileges { + privileges[i] = privilege.Name + } + groupInfo := map[string]interface{}{ + HTTPReturnPrivilegeGroupName: group.GroupName, + HTTPReturnPrivileges: strings.Join(privileges, ","), + } + privGroups = append(privGroups, groupInfo) + } + HTTPReturn(c, http.StatusOK, gin.H{HTTPReturnCode: merr.Code(nil), HTTPReturnData: gin.H{ + HTTPReturnPrivilegeGroups: privGroups, + }}) + } + return resp, err +} + +func (h *HandlersV2) addPrivilegesToGroup(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) { + return h.operatePrivilegeGroup(ctx, c, anyReq, dbName, milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup) +} + +func (h *HandlersV2) removePrivilegesFromGroup(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) { + return h.operatePrivilegeGroup(ctx, c, anyReq, dbName, milvuspb.OperatePrivilegeGroupType_RemovePrivilegesFromGroup) +} + +func (h *HandlersV2) operatePrivilegeGroup(ctx context.Context, c *gin.Context, anyReq any, dbName string, operateType milvuspb.OperatePrivilegeGroupType) (interface{}, error) { + httpReq := anyReq.(*PrivilegeGroupReq) + req := &milvuspb.OperatePrivilegeGroupRequest{ + GroupName: httpReq.PrivilegeGroupName, + Privileges: httpReq.Privileges, + Type: operateType, + } + resp, err := wrapperProxy(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/OperatePrivilegeGroup", func(reqCtx context.Context, req any) (interface{}, error) { + return h.proxy.OperatePrivilegeGroup(reqCtx, req.(*milvuspb.OperatePrivilegeGroupRequest)) + }) + if err == nil { + HTTPReturn(c, http.StatusOK, wrapperReturnDefault()) + } + return resp, err +} + func (h *HandlersV2) listIndexes(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) { collectionGetter, _ := anyReq.(requestutil.CollectionNameGetter) indexNames := []string{} diff --git a/internal/distributed/proxy/httpserver/request_v2.go b/internal/distributed/proxy/httpserver/request_v2.go index 24165027c1e49..b4a9909bfc15a 100644 --- a/internal/distributed/proxy/httpserver/request_v2.go +++ b/internal/distributed/proxy/httpserver/request_v2.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/milvus-io/milvus-proto/go-api/v2/commonpb" + "github.com/milvus-io/milvus-proto/go-api/v2/milvuspb" "github.com/milvus-io/milvus/pkg/util/merr" ) @@ -276,6 +277,11 @@ func (req *RoleReq) GetRoleName() string { return req.RoleName } +type PrivilegeGroupReq struct { + PrivilegeGroupName string `json:"privilegeGroupName" binding:"required"` + Privileges []*milvuspb.PrivilegeEntity `json:"privileges"` +} + type GrantReq struct { RoleName string `json:"roleName" binding:"required"` ObjectType string `json:"objectType" binding:"required"` diff --git a/internal/distributed/proxy/service.go b/internal/distributed/proxy/service.go index 8fb4a5ab0f87d..6c4bd02a3e10f 100644 --- a/internal/distributed/proxy/service.go +++ b/internal/distributed/proxy/service.go @@ -997,6 +997,22 @@ func (s *Server) RestoreRBAC(ctx context.Context, req *milvuspb.RestoreRBACMetaR return s.proxy.RestoreRBAC(ctx, req) } +func (s *Server) CreatePrivilegeGroup(ctx context.Context, req *milvuspb.CreatePrivilegeGroupRequest) (*commonpb.Status, error) { + return s.proxy.CreatePrivilegeGroup(ctx, req) +} + +func (s *Server) DropPrivilegeGroup(ctx context.Context, req *milvuspb.DropPrivilegeGroupRequest) (*commonpb.Status, error) { + return s.proxy.DropPrivilegeGroup(ctx, req) +} + +func (s *Server) ListPrivilegeGroups(ctx context.Context, req *milvuspb.ListPrivilegeGroupsRequest) (*milvuspb.ListPrivilegeGroupsResponse, error) { + return s.proxy.ListPrivilegeGroups(ctx, req) +} + +func (s *Server) OperatePrivilegeGroup(ctx context.Context, req *milvuspb.OperatePrivilegeGroupRequest) (*commonpb.Status, error) { + return s.proxy.OperatePrivilegeGroup(ctx, req) +} + func (s *Server) RefreshPolicyInfoCache(ctx context.Context, req *proxypb.RefreshPolicyInfoCacheRequest) (*commonpb.Status, error) { return s.proxy.RefreshPolicyInfoCache(ctx, req) } diff --git a/internal/distributed/rootcoord/client/client.go b/internal/distributed/rootcoord/client/client.go index cabb8a33610db..0ecaf83c7d63d 100644 --- a/internal/distributed/rootcoord/client/client.go +++ b/internal/distributed/rootcoord/client/client.go @@ -706,3 +706,51 @@ func (c *Client) RestoreRBAC(ctx context.Context, in *milvuspb.RestoreRBACMetaRe return client.RestoreRBAC(ctx, in) }) } + +func (c *Client) CreatePrivilegeGroup(ctx context.Context, in *milvuspb.CreatePrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + in = typeutil.Clone(in) + commonpbutil.UpdateMsgBase( + in.GetBase(), + commonpbutil.FillMsgBaseFromClient(paramtable.GetNodeID(), commonpbutil.WithTargetID(c.sess.ServerID)), + ) + + return wrapGrpcCall(ctx, c, func(client rootcoordpb.RootCoordClient) (*commonpb.Status, error) { + return client.CreatePrivilegeGroup(ctx, in) + }) +} + +func (c *Client) DropPrivilegeGroup(ctx context.Context, in *milvuspb.DropPrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + in = typeutil.Clone(in) + commonpbutil.UpdateMsgBase( + in.GetBase(), + commonpbutil.FillMsgBaseFromClient(paramtable.GetNodeID(), commonpbutil.WithTargetID(c.sess.ServerID)), + ) + + return wrapGrpcCall(ctx, c, func(client rootcoordpb.RootCoordClient) (*commonpb.Status, error) { + return client.DropPrivilegeGroup(ctx, in) + }) +} + +func (c *Client) ListPrivilegeGroups(ctx context.Context, in *milvuspb.ListPrivilegeGroupsRequest, opts ...grpc.CallOption) (*milvuspb.ListPrivilegeGroupsResponse, error) { + in = typeutil.Clone(in) + commonpbutil.UpdateMsgBase( + in.GetBase(), + commonpbutil.FillMsgBaseFromClient(paramtable.GetNodeID(), commonpbutil.WithTargetID(c.sess.ServerID)), + ) + + return wrapGrpcCall(ctx, c, func(client rootcoordpb.RootCoordClient) (*milvuspb.ListPrivilegeGroupsResponse, error) { + return client.ListPrivilegeGroups(ctx, in) + }) +} + +func (c *Client) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + in = typeutil.Clone(in) + commonpbutil.UpdateMsgBase( + in.GetBase(), + commonpbutil.FillMsgBaseFromClient(paramtable.GetNodeID(), commonpbutil.WithTargetID(c.sess.ServerID)), + ) + + return wrapGrpcCall(ctx, c, func(client rootcoordpb.RootCoordClient) (*commonpb.Status, error) { + return client.OperatePrivilegeGroup(ctx, in) + }) +} diff --git a/internal/distributed/rootcoord/client/client_test.go b/internal/distributed/rootcoord/client/client_test.go index 42d922dd41794..b70883dcf742f 100644 --- a/internal/distributed/rootcoord/client/client_test.go +++ b/internal/distributed/rootcoord/client/client_test.go @@ -240,10 +240,38 @@ func Test_NewClient(t *testing.T) { r, err := client.ListDatabases(ctx, nil) retCheck(retNotNil, r, err) } + { + r, err := client.AlterCollection(ctx, nil) + retCheck(retNotNil, r, err) + } { r, err := client.AlterDatabase(ctx, nil) retCheck(retNotNil, r, err) } + { + r, err := client.BackupRBAC(ctx, nil) + retCheck(retNotNil, r, err) + } + { + r, err := client.RestoreRBAC(ctx, nil) + retCheck(retNotNil, r, err) + } + { + r, err := client.CreatePrivilegeGroup(ctx, nil) + retCheck(retNotNil, r, err) + } + { + r, err := client.DropPrivilegeGroup(ctx, nil) + retCheck(retNotNil, r, err) + } + { + r, err := client.ListPrivilegeGroups(ctx, nil) + retCheck(retNotNil, r, err) + } + { + r, err := client.OperatePrivilegeGroup(ctx, nil) + retCheck(retNotNil, r, err) + } } client.(*Client).grpcClient = &mock.GRPCClientBase[rootcoordpb.RootCoordClient]{ diff --git a/internal/distributed/rootcoord/service.go b/internal/distributed/rootcoord/service.go index d49c3ae4a89f6..f80f9ece902da 100644 --- a/internal/distributed/rootcoord/service.go +++ b/internal/distributed/rootcoord/service.go @@ -550,3 +550,19 @@ func (s *Server) BackupRBAC(ctx context.Context, request *milvuspb.BackupRBACMet func (s *Server) RestoreRBAC(ctx context.Context, request *milvuspb.RestoreRBACMetaRequest) (*commonpb.Status, error) { return s.rootCoord.RestoreRBAC(ctx, request) } + +func (s *Server) CreatePrivilegeGroup(ctx context.Context, request *milvuspb.CreatePrivilegeGroupRequest) (*commonpb.Status, error) { + return s.rootCoord.CreatePrivilegeGroup(ctx, request) +} + +func (s *Server) DropPrivilegeGroup(ctx context.Context, request *milvuspb.DropPrivilegeGroupRequest) (*commonpb.Status, error) { + return s.rootCoord.DropPrivilegeGroup(ctx, request) +} + +func (s *Server) ListPrivilegeGroups(ctx context.Context, request *milvuspb.ListPrivilegeGroupsRequest) (*milvuspb.ListPrivilegeGroupsResponse, error) { + return s.rootCoord.ListPrivilegeGroups(ctx, request) +} + +func (s *Server) OperatePrivilegeGroup(ctx context.Context, request *milvuspb.OperatePrivilegeGroupRequest) (*commonpb.Status, error) { + return s.rootCoord.OperatePrivilegeGroup(ctx, request) +} diff --git a/internal/metastore/catalog.go b/internal/metastore/catalog.go index a3121eb9fefb0..2e8efac65d9b6 100644 --- a/internal/metastore/catalog.go +++ b/internal/metastore/catalog.go @@ -86,6 +86,11 @@ type RootCoordCatalog interface { BackupRBAC(ctx context.Context, tenant string) (*milvuspb.RBACMeta, error) RestoreRBAC(ctx context.Context, tenant string, meta *milvuspb.RBACMeta) error + GetPrivilegeGroup(ctx context.Context, groupName string) (*milvuspb.PrivilegeGroupInfo, error) + DropPrivilegeGroup(ctx context.Context, groupName string) error + AlterPrivilegeGroup(ctx context.Context, groupName string, privileges []*milvuspb.PrivilegeEntity) error + ListPrivilegeGroups(ctx context.Context) ([]*milvuspb.PrivilegeGroupInfo, error) + Close() } diff --git a/internal/metastore/kv/rootcoord/kv_catalog.go b/internal/metastore/kv/rootcoord/kv_catalog.go index 7af3c30485081..2fc8f30c62b0c 100644 --- a/internal/metastore/kv/rootcoord/kv_catalog.go +++ b/internal/metastore/kv/rootcoord/kv_catalog.go @@ -1332,10 +1332,16 @@ func (kc *Catalog) BackupRBAC(ctx context.Context, tenant string) (*milvuspb.RBA grantsEntity = append(grantsEntity, grants...) } + privGroups, err := kc.ListPrivilegeGroups(ctx) + if err != nil { + return nil, err + } + return &milvuspb.RBACMeta{ - Users: userInfos, - Roles: roleEntity, - Grants: grantsEntity, + Users: userInfos, + Roles: roleEntity, + Grants: grantsEntity, + PrivilegeGroups: privGroups, }, nil } @@ -1344,6 +1350,7 @@ func (kc *Catalog) RestoreRBAC(ctx context.Context, tenant string, meta *milvusp needRollbackUser := make([]*milvuspb.UserInfo, 0) needRollbackRole := make([]*milvuspb.RoleEntity, 0) needRollbackGrants := make([]*milvuspb.GrantEntity, 0) + needRollbackPrivilegeGroups := make([]*milvuspb.PrivilegeGroupInfo, 0) defer func() { if err != nil { log.Warn("failed to restore rbac, try to rollback", zap.Error(err)) @@ -1370,6 +1377,14 @@ func (kc *Catalog) RestoreRBAC(ctx context.Context, tenant string, meta *milvusp log.Warn("failed to rollback users after restore failed", zap.Error(err)) } } + + // roll back privilege group + for _, group := range needRollbackPrivilegeGroups { + err = kc.DropPrivilegeGroup(ctx, group.GroupName) + if err != nil { + log.Warn("failed to rollback privilege groups after restore failed", zap.Error(err)) + } + } } }() @@ -1392,9 +1407,42 @@ func (kc *Catalog) RestoreRBAC(ctx context.Context, tenant string, meta *milvusp needRollbackRole = append(needRollbackRole, role) } - // restore grant + // restore privilege group + existPrivGroups, err := kc.ListPrivilegeGroups(ctx) + if err != nil { + return err + } + existPrivGroupMap := lo.SliceToMap(existPrivGroups, func(entity *milvuspb.PrivilegeGroupInfo) (string, struct{}) { return entity.GroupName, struct{}{} }) + for _, group := range meta.PrivilegeGroups { + if _, ok := existPrivGroupMap[group.GroupName]; ok { + log.Warn("failed to restore, privilege group already exists", zap.String("group", group.GroupName)) + err = errors.Newf("privilege group [%s] already exists", group.GroupName) + return err + } + err = kc.AlterPrivilegeGroup(ctx, group.GroupName, group.Privileges) + if err != nil { + return err + } + needRollbackPrivilegeGroups = append(needRollbackPrivilegeGroups, group) + } + + // restore grant, list latest privilege group first + existPrivGroups, err = kc.ListPrivilegeGroups(ctx) + if err != nil { + return err + } + existPrivGroupMap = lo.SliceToMap(existPrivGroups, func(entity *milvuspb.PrivilegeGroupInfo) (string, struct{}) { return entity.GroupName, struct{}{} }) for _, grant := range meta.Grants { - grant.Grantor.Privilege.Name = util.PrivilegeNameForMetastore(grant.Grantor.Privilege.Name) + privName := grant.Grantor.Privilege.Name + if util.IsPrivilegeNameDefined(privName) { + grant.Grantor.Privilege.Name = util.PrivilegeNameForMetastore(privName) + } else if _, ok := existPrivGroupMap[privName]; ok { + grant.Grantor.Privilege.Name = util.PrivilegeGroupNameForMetastore(privName) + } else { + log.Warn("failed to restore, privilege group does not exist", zap.String("group", privName)) + err = errors.Newf("privilege group [%s] does not exist", privName) + return err + } err = kc.AlterGrant(ctx, tenant, grant, milvuspb.OperatePrivilegeType_Grant) if err != nil { return err @@ -1439,6 +1487,89 @@ func (kc *Catalog) RestoreRBAC(ctx context.Context, tenant string, meta *milvusp return err } +func (kc *Catalog) GetPrivilegeGroup(ctx context.Context, groupName string) (*milvuspb.PrivilegeGroupInfo, error) { + k := BuildPrivilegeGroupkey(groupName) + val, err := kc.Txn.Load(k) + if err != nil { + if errors.Is(err, merr.ErrIoKeyNotFound) { + return nil, fmt.Errorf("privilege group [%s] does not exist", groupName) + } + log.Error("failed to load privilege group", zap.String("group", groupName), zap.Error(err)) + return nil, err + } + privGroupInfo := &milvuspb.PrivilegeGroupInfo{} + err = proto.Unmarshal([]byte(val), privGroupInfo) + if err != nil { + log.Error("failed to unmarshal privilege group info", zap.Error(err)) + return nil, err + } + return privGroupInfo, nil +} + +func (kc *Catalog) DropPrivilegeGroup(ctx context.Context, groupName string) error { + k := BuildPrivilegeGroupkey(groupName) + err := kc.Txn.Remove(k) + if err != nil { + log.Warn("fail to drop privilege group", zap.String("key", k), zap.Error(err)) + return err + } + return nil +} + +func (kc *Catalog) AlterPrivilegeGroup(ctx context.Context, groupName string, privileges []*milvuspb.PrivilegeEntity) error { + // dedup privileges + dedupedMap := lo.SliceToMap(privileges, func(p *milvuspb.PrivilegeEntity) (string, *milvuspb.PrivilegeEntity) { + return p.Name, p + }) + dedupedPrivileges := lo.Values(dedupedMap) + + k := BuildPrivilegeGroupkey(groupName) + groupInfo := &milvuspb.PrivilegeGroupInfo{ + GroupName: groupName, + Privileges: dedupedPrivileges, + } + v, err := proto.Marshal(groupInfo) + if err != nil { + log.Error("failed to marshal privilege group info", zap.Error(err)) + return err + } + hasKey, err := kc.Txn.Has(k) + if err != nil { + log.Error("failed to check key existence", zap.String("key", k), zap.Error(err)) + return err + } + if hasKey { + if err = kc.Txn.Remove(k); err != nil { + log.Error("failed to remove key", zap.String("key", k), zap.Error(err)) + return err + } + } + if err = kc.Txn.Save(k, string(v)); err != nil { + log.Warn("fail to put privilege group", zap.String("key", k), zap.Error(err)) + return err + } + return nil +} + +func (kc *Catalog) ListPrivilegeGroups(ctx context.Context) ([]*milvuspb.PrivilegeGroupInfo, error) { + _, vals, err := kc.Txn.LoadWithPrefix(PrivilegeGroupPrefix) + if err != nil { + log.Error("failed to list privilege groups", zap.String("prefix", PrivilegeGroupPrefix), zap.Error(err)) + return nil, err + } + privGroups := make([]*milvuspb.PrivilegeGroupInfo, 0, len(vals)) + for _, val := range vals { + privGroupInfo := &milvuspb.PrivilegeGroupInfo{} + err = proto.Unmarshal([]byte(val), privGroupInfo) + if err != nil { + log.Error("failed to unmarshal privilege group info", zap.Error(err)) + return nil, err + } + privGroups = append(privGroups, privGroupInfo) + } + return privGroups, nil +} + func (kc *Catalog) Close() { // do nothing } diff --git a/internal/metastore/kv/rootcoord/kv_catalog_test.go b/internal/metastore/kv/rootcoord/kv_catalog_test.go index 7dfbc27b55240..43950a87510ff 100644 --- a/internal/metastore/kv/rootcoord/kv_catalog_test.go +++ b/internal/metastore/kv/rootcoord/kv_catalog_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/cockroachdb/errors" + "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -2701,6 +2702,8 @@ func TestRBAC_Backup(t *testing.T) { }) c.AlterUserRole(ctx, util.DefaultTenant, &milvuspb.UserEntity{Name: "user1"}, &milvuspb.RoleEntity{Name: "role1"}, milvuspb.OperateUserRoleType_AddUserToRole) + c.AlterPrivilegeGroup(ctx, "custom_group", []*milvuspb.PrivilegeEntity{{Name: "CreateCollection"}}) + // test backup success backup, err := c.BackupRBAC(ctx, util.DefaultTenant) assert.NoError(t, err) @@ -2711,6 +2714,9 @@ func TestRBAC_Backup(t *testing.T) { assert.Equal(t, "user1", backup.Users[0].User) assert.Equal(t, 1, len(backup.Users[0].Roles)) assert.Equal(t, 1, len(backup.Roles)) + assert.Equal(t, 1, len(backup.PrivilegeGroups)) + assert.Equal(t, "custom_group", backup.PrivilegeGroups[0].GroupName) + assert.Equal(t, "CreateCollection", backup.PrivilegeGroups[0].Privileges[0].Name) } func TestRBAC_Restore(t *testing.T) { @@ -2756,10 +2762,17 @@ func TestRBAC_Restore(t *testing.T) { DbName: util.DefaultDBName, Grantor: &milvuspb.GrantorEntity{ User: &milvuspb.UserEntity{Name: "user1"}, - Privilege: &milvuspb.PrivilegeEntity{Name: "PrivilegeLoad"}, + Privilege: &milvuspb.PrivilegeEntity{Name: "Load"}, }, }, }, + + PrivilegeGroups: []*milvuspb.PrivilegeGroupInfo{ + { + GroupName: "custom_group", + Privileges: []*milvuspb.PrivilegeEntity{{Name: "CreateCollection"}}, + }, + }, } // test restore success err := c.RestoreRBAC(ctx, util.DefaultTenant, rbacMeta) @@ -2785,6 +2798,13 @@ func TestRBAC_Restore(t *testing.T) { assert.Equal(t, "obj_name1", grants[0].ObjectName) assert.Equal(t, "role1", grants[0].Role.Name) assert.Equal(t, "user1", grants[0].Grantor.User.Name) + assert.Equal(t, "Load", grants[0].Grantor.Privilege.Name) + // check privilege group + privGroups, err := c.ListPrivilegeGroups(ctx) + assert.NoError(t, err) + assert.Len(t, privGroups, 1) + assert.Equal(t, "custom_group", privGroups[0].GroupName) + assert.Equal(t, "CreateCollection", privGroups[0].Privileges[0].Name) rbacMeta2 := &milvuspb.RBACMeta{ Users: []*milvuspb.UserInfo{ @@ -2821,10 +2841,17 @@ func TestRBAC_Restore(t *testing.T) { DbName: util.DefaultDBName, Grantor: &milvuspb.GrantorEntity{ User: &milvuspb.UserEntity{Name: "user2"}, - Privilege: &milvuspb.PrivilegeEntity{Name: "PrivilegeLoad"}, + Privilege: &milvuspb.PrivilegeEntity{Name: "Load"}, }, }, }, + + PrivilegeGroups: []*milvuspb.PrivilegeGroupInfo{ + { + GroupName: "custom_group2", + Privileges: []*milvuspb.PrivilegeEntity{{Name: "DropCollection"}}, + }, + }, } // test restore failed and roll back @@ -2846,6 +2873,150 @@ func TestRBAC_Restore(t *testing.T) { }) assert.NoError(t, err) assert.Len(t, grants, 1) + assert.Equal(t, grants[0].Grantor.Privilege.Name, "Load") + // check privilege group + privGroups, err = c.ListPrivilegeGroups(ctx) + assert.NoError(t, err) + assert.Len(t, privGroups, 1) + assert.Equal(t, "custom_group", privGroups[0].GroupName) + assert.Equal(t, "CreateCollection", privGroups[0].Privileges[0].Name) +} + +func TestRBAC_PrivilegeGroup(t *testing.T) { + ctx := context.TODO() + group1 := "group1" + group2 := "group2" + key1 := BuildPrivilegeGroupkey(group1) + key2 := BuildPrivilegeGroupkey(group2) + privGroupInfo1 := &milvuspb.PrivilegeGroupInfo{GroupName: group1, Privileges: []*milvuspb.PrivilegeEntity{{Name: "priv10"}, {Name: "priv11"}}} + privGroupInfo2 := &milvuspb.PrivilegeGroupInfo{GroupName: group2, Privileges: []*milvuspb.PrivilegeEntity{{Name: "priv20"}, {Name: "priv21"}}} + v1, _ := proto.Marshal(privGroupInfo1) + v2, _ := proto.Marshal(privGroupInfo2) + + t.Run("test GetPrivilegeGroup", func(t *testing.T) { + var ( + kvmock = mocks.NewTxnKV(t) + c = &Catalog{Txn: kvmock} + ) + kvmock.EXPECT().Load(key1).Return(string(v1), nil) + kvmock.EXPECT().Load(key2).Return("", merr.ErrIoKeyNotFound) + + tests := []struct { + description string + expectedErr error + groupName string + expectedPrivileges []string + }{ + {"group not found", fmt.Errorf("privilege group [%s] does not exist", group2), group2, nil}, + {"valid group", nil, group1, []string{"priv10", "priv11"}}, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + group, err := c.GetPrivilegeGroup(ctx, test.groupName) + if test.expectedErr != nil { + assert.Error(t, err, test.expectedErr) + } else { + assert.NoError(t, err) + assert.ElementsMatch(t, getPrivilegeNames(group.Privileges), test.expectedPrivileges) + } + }) + } + }) + + t.Run("test DropPrivilegeGroup", func(t *testing.T) { + var ( + kvmock = mocks.NewTxnKV(t) + c = &Catalog{Txn: kvmock} + ) + + kvmock.EXPECT().Remove(key1).Return(nil) + kvmock.EXPECT().Remove(key2).Return(errors.New("Mock remove failure")) + + tests := []struct { + description string + isValid bool + groupName string + }{ + {"valid group", true, group1}, + {"remove failure", false, group2}, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + err := c.DropPrivilegeGroup(ctx, test.groupName) + if test.isValid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } + }) + + t.Run("test AlterPrivilegeGroup", func(t *testing.T) { + var ( + kvmock = mocks.NewTxnKV(t) + c = &Catalog{Txn: kvmock} + ) + + kvmock.EXPECT().Has(key1).Return(true, nil) + kvmock.EXPECT().Remove(key1).Return(nil) + kvmock.EXPECT().Save(key1, mock.Anything).Return(nil) + + kvmock.EXPECT().Has(key2).Return(false, nil) + kvmock.EXPECT().Save(key2, mock.Anything).Return(nil) + + tests := []struct { + description string + isValid bool + groupName string + privileges []*milvuspb.PrivilegeEntity + }{ + {"valid group with existing key", true, group1, []*milvuspb.PrivilegeEntity{{Name: "priv10"}, {Name: "priv11"}}}, + {"valid group without existing key", true, group2, []*milvuspb.PrivilegeEntity{{Name: "priv10"}, {Name: "priv11"}}}, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + err := c.AlterPrivilegeGroup(ctx, test.groupName, test.privileges) + if test.isValid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } + }) + + t.Run("test ListPrivilegeGroups", func(t *testing.T) { + var ( + kvmock = mocks.NewTxnKV(t) + c = &Catalog{Txn: kvmock} + ) + + kvmock.EXPECT().LoadWithPrefix(PrivilegeGroupPrefix).Return( + []string{key1, key2}, + []string{string(v1), string(v2)}, + nil, + ) + groups, err := c.ListPrivilegeGroups(ctx) + assert.NoError(t, err) + groupNames := lo.Map(groups, func(g *milvuspb.PrivilegeGroupInfo, _ int) string { + return g.GroupName + }) + assert.ElementsMatch(t, groupNames, []string{group1, group2}) + assert.ElementsMatch(t, getPrivilegeNames(groups[0].Privileges), []string{"priv10", "priv11"}) + assert.ElementsMatch(t, getPrivilegeNames(groups[1].Privileges), []string{"priv20", "priv21"}) + }) +} + +func getPrivilegeNames(privileges []*milvuspb.PrivilegeEntity) []string { + if len(privileges) == 0 { + return []string{} + } + return lo.Map(privileges, func(p *milvuspb.PrivilegeEntity, _ int) string { + return p.Name + }) } func TestCatalog_AlterDatabase(t *testing.T) { diff --git a/internal/metastore/kv/rootcoord/rootcoord_constant.go b/internal/metastore/kv/rootcoord/rootcoord_constant.go index c48e6f619529d..7ea71f4fe4006 100644 --- a/internal/metastore/kv/rootcoord/rootcoord_constant.go +++ b/internal/metastore/kv/rootcoord/rootcoord_constant.go @@ -50,6 +50,9 @@ const ( // GranteeIDPrefix prefix for mapping among privilege and grantor GranteeIDPrefix = ComponentPrefix + CommonCredentialPrefix + "/grantee-id" + + // PrivilegeGroupPrefix prefix for privilege group + PrivilegeGroupPrefix = ComponentPrefix + "/privilege-group" ) func BuildDatabasePrefixWithDBID(dbID int64) string { @@ -70,3 +73,7 @@ func getDatabasePrefix(dbID int64) string { } return CollectionMetaPrefix } + +func BuildPrivilegeGroupkey(groupName string) string { + return fmt.Sprintf("%s/%s", PrivilegeGroupPrefix, groupName) +} diff --git a/internal/metastore/mocks/mock_rootcoord_catalog.go b/internal/metastore/mocks/mock_rootcoord_catalog.go index ca2ed2f27f18a..53f6db6567887 100644 --- a/internal/metastore/mocks/mock_rootcoord_catalog.go +++ b/internal/metastore/mocks/mock_rootcoord_catalog.go @@ -1828,3 +1828,198 @@ func NewRootCoordCatalog(t interface { return mock } + +// GetPrivilegeGroup provides a mock function with given fields: ctx, groupName +func (_m *RootCoordCatalog) GetPrivilegeGroup(ctx context.Context, groupName string) (*milvuspb.PrivilegeGroupInfo, error) { + ret := _m.Called(ctx, groupName) + + var r0 *milvuspb.PrivilegeGroupInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*milvuspb.PrivilegeGroupInfo, error)); ok { + return rf(ctx, groupName) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *milvuspb.PrivilegeGroupInfo); ok { + r0 = rf(ctx, groupName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*milvuspb.PrivilegeGroupInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, groupName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RootCoordCatalog_GetPrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPrivilegeGroup' +type RootCoordCatalog_GetPrivilegeGroup_Call struct { + *mock.Call +} + +// GetPrivilegeGroup is a helper method to define mock.On call +// - ctx context.Context +// - groupName string +func (_e *RootCoordCatalog_Expecter) GetPrivilegeGroup(ctx interface{}, groupName interface{}) *RootCoordCatalog_GetPrivilegeGroup_Call { + return &RootCoordCatalog_GetPrivilegeGroup_Call{Call: _e.mock.On("GetPrivilegeGroup", ctx, groupName)} +} + +func (_c *RootCoordCatalog_GetPrivilegeGroup_Call) Run(run func(ctx context.Context, groupName string)) *RootCoordCatalog_GetPrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *RootCoordCatalog_GetPrivilegeGroup_Call) Return(_a0 *milvuspb.PrivilegeGroupInfo, _a1 error) *RootCoordCatalog_GetPrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RootCoordCatalog_GetPrivilegeGroup_Call) RunAndReturn(run func(context.Context, string) (*milvuspb.PrivilegeGroupInfo,error)) *RootCoordCatalog_GetPrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +// DropPrivilegeGroup provides a mock function with given fields: ctx, groupName, privileges +func (_m *RootCoordCatalog) DropPrivilegeGroup(ctx context.Context, groupName string) error { + ret := _m.Called(ctx, groupName) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, groupName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RootCoordCatalog_DropPrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DropPrivilegeGroup' +type RootCoordCatalog_DropPrivilegeGroup_Call struct { + *mock.Call +} + +// AlterPrivilegeGroup is a helper method to define mock.On call +// - ctx context.Context +// - groupName string +func (_e *RootCoordCatalog_Expecter) DropPrivilegeGroup(ctx interface{}, groupName interface{}) *RootCoordCatalog_DropPrivilegeGroup_Call { + return &RootCoordCatalog_DropPrivilegeGroup_Call{Call: _e.mock.On("DropPrivilegeGroup", ctx, groupName)} +} + +func (_c *RootCoordCatalog_DropPrivilegeGroup_Call) Run(run func(ctx context.Context, groupName string)) *RootCoordCatalog_DropPrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *RootCoordCatalog_DropPrivilegeGroup_Call) Return(_a0 error) *RootCoordCatalog_DropPrivilegeGroup_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RootCoordCatalog_DropPrivilegeGroup_Call) RunAndReturn(run func(context.Context, string) error) *RootCoordCatalog_DropPrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +// AlterPrivilegeGroup provides a mock function with given fields: ctx, groupName, privileges +func (_m *RootCoordCatalog) AlterPrivilegeGroup(ctx context.Context, groupName string, privileges []*milvuspb.PrivilegeEntity) error { + ret := _m.Called(ctx, groupName, privileges) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, []*milvuspb.PrivilegeEntity) error); ok { + r0 = rf(ctx, groupName, privileges) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RootCoordCatalog_AlterPrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AlterPrivilegeGroup' +type RootCoordCatalog_AlterPrivilegeGroup_Call struct { + *mock.Call +} + +// AlterPrivilegeGroup is a helper method to define mock.On call +// - ctx context.Context +// - groupName string +func (_e *RootCoordCatalog_Expecter) AlterPrivilegeGroup(ctx interface{}, groupName interface{}, privileges interface{}) *RootCoordCatalog_AlterPrivilegeGroup_Call { + return &RootCoordCatalog_AlterPrivilegeGroup_Call{Call: _e.mock.On("AlterPrivilegeGroup", ctx, groupName, privileges)} +} + +func (_c *RootCoordCatalog_AlterPrivilegeGroup_Call) Run(run func(ctx context.Context, groupName string, privileges []*milvuspb.PrivilegeEntity)) *RootCoordCatalog_AlterPrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].([]*milvuspb.PrivilegeEntity)) + }) + return _c +} + +func (_c *RootCoordCatalog_AlterPrivilegeGroup_Call) Return(_a0 error) *RootCoordCatalog_AlterPrivilegeGroup_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *RootCoordCatalog_AlterPrivilegeGroup_Call) RunAndReturn(run func(context.Context, string, []*milvuspb.PrivilegeEntity) error) *RootCoordCatalog_AlterPrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +// ListPrivilegeGroups provides a mock function with given fields: ctx +func (_m *RootCoordCatalog) ListPrivilegeGroups(ctx context.Context) ([]*milvuspb.PrivilegeGroupInfo, error) { + ret := _m.Called(ctx) + + var r0 []*milvuspb.PrivilegeGroupInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]*milvuspb.PrivilegeGroupInfo, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []*milvuspb.PrivilegeGroupInfo); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*milvuspb.PrivilegeGroupInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RootCoordCatalog_ListPrivilegeGroups_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPrivilegeGroups' +type RootCoordCatalog_ListPrivilegeGroups_Call struct { + *mock.Call +} + +// ListPrivilegeGroups is a helper method to define mock.On call +// - ctx context.Context +func (_e *RootCoordCatalog_Expecter) ListPrivilegeGroups(ctx interface{}) *RootCoordCatalog_ListPrivilegeGroups_Call { + return &RootCoordCatalog_ListPrivilegeGroups_Call{Call: _e.mock.On("ListPrivilegeGroups", ctx)} +} + +func (_c *RootCoordCatalog_ListPrivilegeGroups_Call) Run(run func(ctx context.Context)) *RootCoordCatalog_ListPrivilegeGroups_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *RootCoordCatalog_ListPrivilegeGroups_Call) Return(_a0 []*milvuspb.PrivilegeGroupInfo, _a1 error) *RootCoordCatalog_ListPrivilegeGroups_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RootCoordCatalog_ListPrivilegeGroups_Call) RunAndReturn(run func(context.Context) ([]*milvuspb.PrivilegeGroupInfo, error)) *RootCoordCatalog_ListPrivilegeGroups_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/mocks/mock_proxy.go b/internal/mocks/mock_proxy.go index ce6fccf7c1aa9..ce17cd4167912 100644 --- a/internal/mocks/mock_proxy.go +++ b/internal/mocks/mock_proxy.go @@ -388,6 +388,226 @@ func (_c *MockProxy_BackupRBAC_Call) RunAndReturn(run func(context.Context, *mil return _c } +// CreatePrivilegeGroup provides a mock function with given fields: _a0, _a1 +func (_m *MockProxy) CreatePrivilegeGroup(_a0 context.Context, _a1 *milvuspb.CreatePrivilegeGroupRequest) (*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.CreatePrivilegeGroupRequest) (*commonpb.Status, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.CreatePrivilegeGroupRequest) *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.CreatePrivilegeGroupRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockProxy_CreatePrivielegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePrivilegeGroup' +type MockProxy_CreatePrivilegeGroup_Call struct { + *mock.Call +} + +// CreatePrivilegeGroup is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.CreatePrivilegeGroupRequest +func (_e *MockProxy_Expecter) CreatePrivilegeGroup(_a0 interface{}, _a1 interface{}) *MockProxy_CreatePrivilegeGroup_Call { + return &MockProxy_CreatePrivilegeGroup_Call{Call: _e.mock.On("CreatePrivilegeGroup", _a0, _a1)} +} + +func (_c *MockProxy_CreatePrivilegeGroup_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.CreatePrivilegeGroupRequest)) *MockProxy_CreatePrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.CreatePrivilegeGroupRequest)) + }) + return _c +} + +func (_c *MockProxy_CreatePrivilegeGroup_Call) Return(_a0 *commonpb.Status, _a1 error) *MockProxy_CreatePrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockProxy_CreatePrivilegeGroup_Call) RunAndReturn(run func(context.Context, *milvuspb.CreatePrivilegeGroupRequest) (*commonpb.Status, error)) *MockProxy_CreatePrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +// DropPrivilegeGroup provides a mock function with given fields: _a0, _a1 +func (_m *MockProxy) DropPrivilegeGroup(_a0 context.Context, _a1 *milvuspb.DropPrivilegeGroupRequest) (*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.DropPrivilegeGroupRequest) (*commonpb.Status, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.DropPrivilegeGroupRequest) *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.DropPrivilegeGroupRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockProxy_CreatePrivielegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DropPrivilegeGroup' +type MockProxy_DropPrivilegeGroup_Call struct { + *mock.Call +} + +// DropPrivilegeGroup is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.DropPrivilegeGroupRequest +func (_e *MockProxy_Expecter) DropPrivilegeGroup(_a0 interface{}, _a1 interface{}) *MockProxy_DropPrivilegeGroup_Call { + return &MockProxy_DropPrivilegeGroup_Call{Call: _e.mock.On("DropPrivilegeGroup", _a0, _a1)} +} + +func (_c *MockProxy_DropPrivilegeGroup_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.DropPrivilegeGroupRequest)) *MockProxy_DropPrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.DropPrivilegeGroupRequest)) + }) + return _c +} + +func (_c *MockProxy_DropPrivilegeGroup_Call) Return(_a0 *commonpb.Status, _a1 error) *MockProxy_DropPrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockProxy_DropPrivilegeGroup_Call) RunAndReturn(run func(context.Context, *milvuspb.DropPrivilegeGroupRequest) (*commonpb.Status, error)) *MockProxy_DropPrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +// ListPrivilegeGroups provides a mock function with given fields: _a0, _a1 +func (_m *MockProxy) ListPrivilegeGroups(_a0 context.Context, _a1 *milvuspb.ListPrivilegeGroupsRequest) (*milvuspb.ListPrivilegeGroupsResponse, error) { + ret := _m.Called(_a0, _a1) + + var r0 *milvuspb.ListPrivilegeGroupsResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.ListPrivilegeGroupsRequest) (*milvuspb.ListPrivilegeGroupsResponse, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.ListPrivilegeGroupsRequest) *milvuspb.ListPrivilegeGroupsResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*milvuspb.ListPrivilegeGroupsResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.ListPrivilegeGroupsRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockProxy_CreatePrivielegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPrivilegeGroups' +type MockProxy_ListPrivilegeGroups_Call struct { + *mock.Call +} + +// ListPrivilegeGroups is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.ListPrivilegeGroupsRequest +func (_e *MockProxy_Expecter) ListPrivilegeGroups(_a0 interface{}, _a1 interface{}) *MockProxy_ListPrivilegeGroups_Call { + return &MockProxy_ListPrivilegeGroups_Call{Call: _e.mock.On("ListPrivilegeGroups", _a0, _a1)} +} + +func (_c *MockProxy_ListPrivilegeGroups_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.ListPrivilegeGroupsRequest)) *MockProxy_ListPrivilegeGroups_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.ListPrivilegeGroupsRequest)) + }) + return _c +} + +func (_c *MockProxy_ListPrivilegeGroups_Call) Return(_a0 *milvuspb.ListPrivilegeGroupsResponse, _a1 error) *MockProxy_ListPrivilegeGroups_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockProxy_ListPrivilegeGroups_Call) RunAndReturn(run func(context.Context, *milvuspb.ListPrivilegeGroupsRequest) (*milvuspb.ListPrivilegeGroupsResponse, error)) *MockProxy_ListPrivilegeGroups_Call { + _c.Call.Return(run) + return _c +} + +// OperatePrivilegeGroup provides a mock function with given fields: _a0, _a1 +func (_m *MockProxy) OperatePrivilegeGroup(_a0 context.Context, _a1 *milvuspb.OperatePrivilegeGroupRequest) (*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.OperatePrivilegeGroupRequest) (*commonpb.Status, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.OperatePrivilegeGroupRequest) *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.OperatePrivilegeGroupRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockProxy_CreatePrivielegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OperatePrivilegeGroup' +type MockProxy_OperatePrivilegeGroup_Call struct { + *mock.Call +} + +// OperatePrivilegeGroup is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.OperatePrivilegeGroupRequest +func (_e *MockProxy_Expecter) OperatePrivilegeGroup(_a0 interface{}, _a1 interface{}) *MockProxy_OperatePrivilegeGroup_Call { + return &MockProxy_OperatePrivilegeGroup_Call{Call: _e.mock.On("OperatePrivilegeGroup", _a0, _a1)} +} + +func (_c *MockProxy_OperatePrivilegeGroup_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.OperatePrivilegeGroupRequest)) *MockProxy_OperatePrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.OperatePrivilegeGroupRequest)) + }) + return _c +} + +func (_c *MockProxy_OperatePrivilegeGroup_Call) Return(_a0 *commonpb.Status, _a1 error) *MockProxy_OperatePrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockProxy_OperatePrivilegeGroup_Call) RunAndReturn(run func(context.Context, *milvuspb.OperatePrivilegeGroupRequest) (*commonpb.Status, error)) *MockProxy_OperatePrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + // CalcDistance provides a mock function with given fields: _a0, _a1 func (_m *MockProxy) CalcDistance(_a0 context.Context, _a1 *milvuspb.CalcDistanceRequest) (*milvuspb.CalcDistanceResults, error) { ret := _m.Called(_a0, _a1) diff --git a/internal/mocks/mock_rootcoord.go b/internal/mocks/mock_rootcoord.go index a9fe3cfcce5a5..9079c69d883d8 100644 --- a/internal/mocks/mock_rootcoord.go +++ b/internal/mocks/mock_rootcoord.go @@ -390,6 +390,226 @@ func (_c *RootCoord_BackupRBAC_Call) RunAndReturn(run func(context.Context, *mil return _c } +// CreatePrivilegeGroup provides a mock function with given fields: _a0, _a1 +func (_m *RootCoord) CreatePrivilegeGroup(_a0 context.Context, _a1 *milvuspb.CreatePrivilegeGroupRequest) (*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.CreatePrivilegeGroupRequest) (*commonpb.Status, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.CreatePrivilegeGroupRequest) *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.CreatePrivilegeGroupRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RootCoord_CreatePrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePrivilegeGroup' +type RootCoord_CreatePrivilegeGroup_Call struct { + *mock.Call +} + +// CreatePrivilegeGroup is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.CreatePrivilegeGroupRequest +func (_e *RootCoord_Expecter) CreatePrivilegeGroup(_a0 interface{}, _a1 interface{}) *RootCoord_CreatePrivilegeGroup_Call { + return &RootCoord_CreatePrivilegeGroup_Call{Call: _e.mock.On("CreatePrivilegeGroup", _a0, _a1)} +} + +func (_c *RootCoord_CreatePrivilegeGroup_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.CreatePrivilegeGroupRequest)) *RootCoord_CreatePrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.CreatePrivilegeGroupRequest)) + }) + return _c +} + +func (_c *RootCoord_CreatePrivilegeGroup_Call) Return(_a0 *commonpb.Status, _a1 error) *RootCoord_CreatePrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RootCoord_CreatePrivilegeGroup_Call) RunAndReturn(run func(context.Context, *milvuspb.CreatePrivilegeGroupRequest) (*commonpb.Status, error)) *RootCoord_CreatePrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +// DropPrivilegeGroup provides a mock function with given fields: _a0, _a1 +func (_m *RootCoord) DropPrivilegeGroup(_a0 context.Context, _a1 *milvuspb.DropPrivilegeGroupRequest) (*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.DropPrivilegeGroupRequest) (*commonpb.Status, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.DropPrivilegeGroupRequest) *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.DropPrivilegeGroupRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RootCoord_DropPrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DropPrivilegeGroup' +type RootCoord_DropPrivilegeGroup_Call struct { + *mock.Call +} + +// DropPrivilegeGroup is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.DropPrivilegeGroupRequest +func (_e *RootCoord_Expecter) DropPrivilegeGroup(_a0 interface{}, _a1 interface{}) *RootCoord_DropPrivilegeGroup_Call { + return &RootCoord_DropPrivilegeGroup_Call{Call: _e.mock.On("DropPrivilegeGroup", _a0, _a1)} +} + +func (_c *RootCoord_DropPrivilegeGroup_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.DropPrivilegeGroupRequest)) *RootCoord_DropPrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.DropPrivilegeGroupRequest)) + }) + return _c +} + +func (_c *RootCoord_DropPrivilegeGroup_Call) Return(_a0 *commonpb.Status, _a1 error) *RootCoord_DropPrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RootCoord_DropPrivilegeGroup_Call) RunAndReturn(run func(context.Context, *milvuspb.DropPrivilegeGroupRequest) (*commonpb.Status, error)) *RootCoord_DropPrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +// ListPrivilegeGroups provides a mock function with given fields: _a0, _a1 +func (_m *RootCoord) ListPrivilegeGroups(_a0 context.Context, _a1 *milvuspb.ListPrivilegeGroupsRequest) (*milvuspb.ListPrivilegeGroupsResponse, error) { + ret := _m.Called(_a0, _a1) + + var r0 *milvuspb.ListPrivilegeGroupsResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.ListPrivilegeGroupsRequest) (*milvuspb.ListPrivilegeGroupsResponse, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.ListPrivilegeGroupsRequest) *milvuspb.ListPrivilegeGroupsResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*milvuspb.ListPrivilegeGroupsResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.ListPrivilegeGroupsRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RootCoord_ListPrivilegeGroups_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPrivilegeGroups' +type RootCoord_ListPrivilegeGroups_Call struct { + *mock.Call +} + +// ListPrivilegeGroups is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.ListPrivilegeGroupsRequest +func (_e *RootCoord_Expecter) ListPrivilegeGroups(_a0 interface{}, _a1 interface{}) *RootCoord_ListPrivilegeGroups_Call { + return &RootCoord_ListPrivilegeGroups_Call{Call: _e.mock.On("ListPrivilegeGroups", _a0, _a1)} +} + +func (_c *RootCoord_ListPrivilegeGroups_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.ListPrivilegeGroupsRequest)) *RootCoord_ListPrivilegeGroups_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.ListPrivilegeGroupsRequest)) + }) + return _c +} + +func (_c *RootCoord_ListPrivilegeGroups_Call) Return(_a0 *milvuspb.ListPrivilegeGroupsResponse, _a1 error) *RootCoord_ListPrivilegeGroups_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RootCoord_ListPrivilegeGroups_Call) RunAndReturn(run func(context.Context, *milvuspb.ListPrivilegeGroupsRequest) (*milvuspb.ListPrivilegeGroupsResponse, error)) *RootCoord_ListPrivilegeGroups_Call { + _c.Call.Return(run) + return _c +} + +// OperatePrivilegeGroup provides a mock function with given fields: _a0, _a1 +func (_m *RootCoord) OperatePrivilegeGroup(_a0 context.Context, _a1 *milvuspb.OperatePrivilegeGroupRequest) (*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.OperatePrivilegeGroupRequest) (*commonpb.Status, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.OperatePrivilegeGroupRequest) *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.OperatePrivilegeGroupRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RootCoord_OperatePrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OperatePrivilegeGroup' +type RootCoord_OperatePrivilegeGroup_Call struct { + *mock.Call +} + +// OperatePrivilegeGroup is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.OperatePrivilegeGroupRequest +func (_e *RootCoord_Expecter) OperatePrivilegeGroup(_a0 interface{}, _a1 interface{}) *RootCoord_OperatePrivilegeGroup_Call { + return &RootCoord_OperatePrivilegeGroup_Call{Call: _e.mock.On("OperatePrivilegeGroup", _a0, _a1)} +} + +func (_c *RootCoord_OperatePrivilegeGroup_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.OperatePrivilegeGroupRequest)) *RootCoord_OperatePrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.OperatePrivilegeGroupRequest)) + }) + return _c +} + +func (_c *RootCoord_OperatePrivilegeGroup_Call) Return(_a0 *commonpb.Status, _a1 error) *RootCoord_OperatePrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *RootCoord_OperatePrivilegeGroup_Call) RunAndReturn(run func(context.Context, *milvuspb.OperatePrivilegeGroupRequest) (*commonpb.Status, error)) *RootCoord_OperatePrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + // CheckHealth provides a mock function with given fields: _a0, _a1 func (_m *RootCoord) CheckHealth(_a0 context.Context, _a1 *milvuspb.CheckHealthRequest) (*milvuspb.CheckHealthResponse, error) { ret := _m.Called(_a0, _a1) diff --git a/internal/mocks/mock_rootcoord_client.go b/internal/mocks/mock_rootcoord_client.go index 58b0a40671fc4..93978f87d0478 100644 --- a/internal/mocks/mock_rootcoord_client.go +++ b/internal/mocks/mock_rootcoord_client.go @@ -477,6 +477,302 @@ func (_c *MockRootCoordClient_BackupRBAC_Call) RunAndReturn(run func(context.Con return _c } +// CreatePrivilegeGroup provides a mock function with given fields: ctx, in, opts +func (_m *MockRootCoordClient) CreatePrivilegeGroup(ctx context.Context, in *milvuspb.CreatePrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreatePrivilegeGroup") + } + + var r0 *commonpb.Status + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.CreatePrivilegeGroupRequest, ...grpc.CallOption) (*commonpb.Status, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.CreatePrivilegeGroupRequest, ...grpc.CallOption) *commonpb.Status); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*commonpb.Status) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.CreatePrivilegeGroupRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRootCoordClient_CreatePrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePrivilegeGroup' +type MockRootCoordClient_CreatePrivilegeGroup_Call struct { + *mock.Call +} + +// CreatePrivilegeGroup is a helper method to define mock.On call +// - ctx context.Context +// - in *milvuspb.CreatePrivilegeGroupRequest +// - opts ...grpc.CallOption +func (_e *MockRootCoordClient_Expecter) CreatePrivilegeGroup(ctx interface{}, in interface{}, opts ...interface{}) *MockRootCoordClient_CreatePrivilegeGroup_Call { + return &MockRootCoordClient_CreatePrivilegeGroup_Call{Call: _e.mock.On("CreatePrivilegeGroup", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *MockRootCoordClient_CreatePrivilegeGroup_Call) Run(run func(ctx context.Context, in *milvuspb.CreatePrivilegeGroupRequest, opts ...grpc.CallOption)) *MockRootCoordClient_CreatePrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]grpc.CallOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(grpc.CallOption) + } + } + run(args[0].(context.Context), args[1].(*milvuspb.CreatePrivilegeGroupRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockRootCoordClient_CreatePrivilegeGroup_Call) Return(_a0 *commonpb.Status, _a1 error) *MockRootCoordClient_CreatePrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRootCoordClient_CreatePrivilegeGroup_Call) RunAndReturn(run func(context.Context, *milvuspb.CreatePrivilegeGroupRequest, ...grpc.CallOption) (*commonpb.Status, error)) *MockRootCoordClient_CreatePrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +// DropPrivilegeGroup provides a mock function with given fields: ctx, in, opts +func (_m *MockRootCoordClient) DropPrivilegeGroup(ctx context.Context, in *milvuspb.DropPrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DropPrivilegeGroup") + } + + var r0 *commonpb.Status + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.DropPrivilegeGroupRequest, ...grpc.CallOption) (*commonpb.Status, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.DropPrivilegeGroupRequest, ...grpc.CallOption) *commonpb.Status); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*commonpb.Status) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.DropPrivilegeGroupRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRootCoordClient_DropPrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DropPrivilegeGroup' +type MockRootCoordClient_DropPrivilegeGroup_Call struct { + *mock.Call +} + +// DropPrivilegeGroup is a helper method to define mock.On call +// - ctx context.Context +// - in *milvuspb.DropPrivilegeGroupRequest +// - opts ...grpc.CallOption +func (_e *MockRootCoordClient_Expecter) DropPrivilegeGroup(ctx interface{}, in interface{}, opts ...interface{}) *MockRootCoordClient_DropPrivilegeGroup_Call { + return &MockRootCoordClient_DropPrivilegeGroup_Call{Call: _e.mock.On("DropPrivilegeGroup", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *MockRootCoordClient_DropPrivilegeGroup_Call) Run(run func(ctx context.Context, in *milvuspb.DropPrivilegeGroupRequest, opts ...grpc.CallOption)) *MockRootCoordClient_DropPrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]grpc.CallOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(grpc.CallOption) + } + } + run(args[0].(context.Context), args[1].(*milvuspb.DropPrivilegeGroupRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockRootCoordClient_DropPrivilegeGroup_Call) Return(_a0 *commonpb.Status, _a1 error) *MockRootCoordClient_DropPrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRootCoordClient_DropPrivilegeGroup_Call) RunAndReturn(run func(context.Context, *milvuspb.DropPrivilegeGroupRequest, ...grpc.CallOption) (*commonpb.Status, error)) *MockRootCoordClient_DropPrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +// ListPrivilegeGroups provides a mock function with given fields: ctx, in, opts +func (_m *MockRootCoordClient) ListPrivilegeGroups(ctx context.Context, in *milvuspb.ListPrivilegeGroupsRequest, opts ...grpc.CallOption) (*milvuspb.ListPrivilegeGroupsResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for ListPrivilegeGroups") + } + + var r0 *milvuspb.ListPrivilegeGroupsResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.ListPrivilegeGroupsRequest, ...grpc.CallOption) (*milvuspb.ListPrivilegeGroupsResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.ListPrivilegeGroupsRequest, ...grpc.CallOption) *milvuspb.ListPrivilegeGroupsResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*milvuspb.ListPrivilegeGroupsResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.ListPrivilegeGroupsRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRootCoordClient_ListPrivilegeGroups_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPrivilegeGroups' +type MockRootCoordClient_ListPrivilegeGroups_Call struct { + *mock.Call +} + +// ListPrivilegeGroups is a helper method to define mock.On call +// - ctx context.Context +// - in *milvuspb.ListPrivilegeGroupsRequest +// - opts ...grpc.CallOption +func (_e *MockRootCoordClient_Expecter) ListPrivilegeGroups(ctx interface{}, in interface{}, opts ...interface{}) *MockRootCoordClient_ListPrivilegeGroups_Call { + return &MockRootCoordClient_ListPrivilegeGroups_Call{Call: _e.mock.On("ListPrivilegeGroups", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *MockRootCoordClient_ListPrivilegeGroups_Call) Run(run func(ctx context.Context, in *milvuspb.ListPrivilegeGroupsRequest, opts ...grpc.CallOption)) *MockRootCoordClient_ListPrivilegeGroups_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]grpc.CallOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(grpc.CallOption) + } + } + run(args[0].(context.Context), args[1].(*milvuspb.ListPrivilegeGroupsRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockRootCoordClient_ListPrivilegeGroups_Call) Return(_a0 *milvuspb.ListPrivilegeGroupsResponse, _a1 error) *MockRootCoordClient_ListPrivilegeGroups_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRootCoordClient_ListPrivilegeGroups_Call) RunAndReturn(run func(context.Context, *milvuspb.ListPrivilegeGroupsRequest, ...grpc.CallOption) (*milvuspb.ListPrivilegeGroupsResponse, error)) *MockRootCoordClient_ListPrivilegeGroups_Call { + _c.Call.Return(run) + return _c +} + +// OperatePrivilegeGroup provides a mock function with given fields: ctx, in, opts +func (_m *MockRootCoordClient) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for OperatePrivilegeGroup") + } + + var r0 *commonpb.Status + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.OperatePrivilegeGroupRequest, ...grpc.CallOption) (*commonpb.Status, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.OperatePrivilegeGroupRequest, ...grpc.CallOption) *commonpb.Status); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*commonpb.Status) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.OperatePrivilegeGroupRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRootCoordClient_OperatePrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OperatePrivilegeGroup' +type MockRootCoordClient_OperatePrivilegeGroup_Call struct { + *mock.Call +} + +// OperatePrivilegeGroup is a helper method to define mock.On call +// - ctx context.Context +// - in *milvuspb.OperatePrivilegeGroupRequest +// - opts ...grpc.CallOption +func (_e *MockRootCoordClient_Expecter) OperatePrivilegeGroup(ctx interface{}, in interface{}, opts ...interface{}) *MockRootCoordClient_OperatePrivilegeGroup_Call { + return &MockRootCoordClient_OperatePrivilegeGroup_Call{Call: _e.mock.On("OperatePrivilegeGroup", + append([]interface{}{ctx, in}, opts...)...)} +} + +func (_c *MockRootCoordClient_OperatePrivilegeGroup_Call) Run(run func(ctx context.Context, in *milvuspb.OperatePrivilegeGroupRequest, opts ...grpc.CallOption)) *MockRootCoordClient_OperatePrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]grpc.CallOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(grpc.CallOption) + } + } + run(args[0].(context.Context), args[1].(*milvuspb.OperatePrivilegeGroupRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockRootCoordClient_OperatePrivilegeGroup_Call) Return(_a0 *commonpb.Status, _a1 error) *MockRootCoordClient_OperatePrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRootCoordClient_OperatePrivilegeGroup_Call) RunAndReturn(run func(context.Context, *milvuspb.OperatePrivilegeGroupRequest, ...grpc.CallOption) (*commonpb.Status, error)) *MockRootCoordClient_OperatePrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + // CheckHealth provides a mock function with given fields: ctx, in, opts func (_m *MockRootCoordClient) CheckHealth(ctx context.Context, in *milvuspb.CheckHealthRequest, opts ...grpc.CallOption) (*milvuspb.CheckHealthResponse, error) { _va := make([]interface{}, len(opts)) diff --git a/internal/proto/internal.proto b/internal/proto/internal.proto index 148cf1c3899cc..4a030debf2575 100644 --- a/internal/proto/internal.proto +++ b/internal/proto/internal.proto @@ -4,6 +4,7 @@ option go_package = "github.com/milvus-io/milvus/internal/proto/internalpb"; import "common.proto"; import "schema.proto"; +import "milvus.proto"; message GetTimeTickChannelRequest { } @@ -263,6 +264,7 @@ message ListPolicyResponse { common.Status status = 1; repeated string policy_infos = 2; repeated string user_roles = 3; + repeated milvus.PrivilegeGroupInfo privilege_groups = 4; } message ShowConfigurationsRequest { diff --git a/internal/proto/root_coord.proto b/internal/proto/root_coord.proto index fbdb1d0a5e7dc..ebbc813f71cc7 100644 --- a/internal/proto/root_coord.proto +++ b/internal/proto/root_coord.proto @@ -126,6 +126,10 @@ service RootCoord { rpc ListPolicy(internal.ListPolicyRequest) returns (internal.ListPolicyResponse) {} rpc BackupRBAC(milvus.BackupRBACMetaRequest) returns (milvus.BackupRBACMetaResponse){} rpc RestoreRBAC(milvus.RestoreRBACMetaRequest) returns (common.Status){} + rpc CreatePrivilegeGroup(milvus.CreatePrivilegeGroupRequest) returns (common.Status) {} + rpc DropPrivilegeGroup(milvus.DropPrivilegeGroupRequest) returns (common.Status) {} + rpc ListPrivilegeGroups(milvus.ListPrivilegeGroupsRequest) returns (milvus.ListPrivilegeGroupsResponse) {} + rpc OperatePrivilegeGroup(milvus.OperatePrivilegeGroupRequest) returns (common.Status) {} rpc CheckHealth(milvus.CheckHealthRequest) returns (milvus.CheckHealthResponse) {} diff --git a/internal/proxy/impl.go b/internal/proxy/impl.go index a1632b736d021..18dc235324aa3 100644 --- a/internal/proxy/impl.go +++ b/internal/proxy/impl.go @@ -6526,3 +6526,124 @@ func (node *Proxy) RegisterRestRouter(router gin.IRouter) { // Datanode request router.GET(http.DNodeSyncTasksPath, getDataComponentMetrics(node, metricsinfo.SyncTasks)) } + +func (node *Proxy) CreatePrivilegeGroup(ctx context.Context, req *milvuspb.CreatePrivilegeGroupRequest) (*commonpb.Status, error) { + ctx, sp := otel.Tracer(typeutil.ProxyRole).Start(ctx, "Proxy-CreatePrivilegeGroup") + defer sp.End() + + log := log.Ctx(ctx) + + log.Info("CreatePrivilegeGroup", zap.Any("req", req)) + 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 req.Base == nil { + req.Base = &commonpb.MsgBase{} + } + req.Base.MsgType = commonpb.MsgType_CreatePrivilegeGroup + + result, err := node.rootCoord.CreatePrivilegeGroup(ctx, req) + if err != nil { + log.Warn("fail to create privilege group", zap.Error(err)) + return merr.Status(err), nil + } + if merr.Ok(result) { + SendReplicateMessagePack(ctx, node.replicateMsgStream, req) + } + return result, nil +} + +func (node *Proxy) DropPrivilegeGroup(ctx context.Context, req *milvuspb.DropPrivilegeGroupRequest) (*commonpb.Status, error) { + ctx, sp := otel.Tracer(typeutil.ProxyRole).Start(ctx, "Proxy-DropPrivilegeGroup") + defer sp.End() + + log := log.Ctx(ctx) + + log.Info("DropPrivilegeGroup", zap.Any("req", req)) + 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 req.Base == nil { + req.Base = &commonpb.MsgBase{} + } + req.Base.MsgType = commonpb.MsgType_DropPrivilegeGroup + + result, err := node.rootCoord.DropPrivilegeGroup(ctx, req) + if err != nil { + log.Warn("fail to drop privilege group", zap.Error(err)) + return merr.Status(err), nil + } + if merr.Ok(result) { + SendReplicateMessagePack(ctx, node.replicateMsgStream, req) + } + return result, nil +} + +func (node *Proxy) ListPrivilegeGroups(ctx context.Context, req *milvuspb.ListPrivilegeGroupsRequest) (*milvuspb.ListPrivilegeGroupsResponse, error) { + ctx, sp := otel.Tracer(typeutil.ProxyRole).Start(ctx, "Proxy-ListPrivilegeGroups") + defer sp.End() + + log := log.Ctx(ctx).With( + zap.String("role", typeutil.ProxyRole)) + + log.Debug("ListPrivilegeGroups") + if err := merr.CheckHealthy(node.GetStateCode()); err != nil { + return &milvuspb.ListPrivilegeGroupsResponse{Status: merr.Status(err)}, nil + } + if req.Base == nil { + req.Base = &commonpb.MsgBase{} + } + req.Base.MsgType = commonpb.MsgType_ListPrivilegeGroups + rootCoordReq := &milvuspb.ListPrivilegeGroupsRequest{ + Base: commonpbutil.NewMsgBase( + commonpbutil.WithMsgType(commonpb.MsgType_ListPrivilegeGroups), + ), + } + resp, err := node.rootCoord.ListPrivilegeGroups(ctx, rootCoordReq) + if err != nil { + return &milvuspb.ListPrivilegeGroupsResponse{ + Status: merr.Status(err), + }, nil + } + return resp, nil +} + +func (node *Proxy) OperatePrivilegeGroup(ctx context.Context, req *milvuspb.OperatePrivilegeGroupRequest) (*commonpb.Status, error) { + ctx, sp := otel.Tracer(typeutil.ProxyRole).Start(ctx, "Proxy-OperatePrivilegeGroup") + defer sp.End() + + log := log.Ctx(ctx) + + log.Info("OperatePrivilegeGroup", zap.Any("req", req)) + 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 + } + for _, priv := range req.GetPrivileges() { + if err := ValidatePrivilege(priv.Name); err != nil { + return merr.Status(err), nil + } + } + if req.Base == nil { + req.Base = &commonpb.MsgBase{} + } + req.Base.MsgType = commonpb.MsgType_OperatePrivilegeGroup + + result, err := node.rootCoord.OperatePrivilegeGroup(ctx, req) + if err != nil { + log.Warn("fail to operate privilege group", zap.Error(err)) + return merr.Status(err), nil + } + if merr.Ok(result) { + SendReplicateMessagePack(ctx, node.replicateMsgStream, req) + } + return result, nil +} diff --git a/internal/proxy/meta_cache.go b/internal/proxy/meta_cache.go index 34c6db0817723..7f223404c60aa 100644 --- a/internal/proxy/meta_cache.go +++ b/internal/proxy/meta_cache.go @@ -28,6 +28,7 @@ import ( "github.com/samber/lo" "go.uber.org/atomic" "go.uber.org/zap" + "google.golang.org/protobuf/proto" "github.com/milvus-io/milvus-proto/go-api/v2/commonpb" "github.com/milvus-io/milvus-proto/go-api/v2/milvuspb" @@ -82,9 +83,10 @@ type Cache interface { UpdateCredential(credInfo *internalpb.CredentialInfo) GetPrivilegeInfo(ctx context.Context) []string + GetGroupPrivileges(groupName string) map[string]struct{} GetUserRole(username string) []string RefreshPolicyInfo(op typeutil.CacheOp) error - InitPolicyInfo(info []string, userRoles []string) + InitPolicyInfo(info []string, userRoles []string, privilegeGroups []*milvuspb.PrivilegeGroupInfo) error RemoveDatabase(ctx context.Context, database string) HasDatabase(ctx context.Context, database string) bool @@ -340,18 +342,19 @@ type MetaCache struct { rootCoord types.RootCoordClient queryCoord types.QueryCoordClient - dbInfo map[string]*databaseInfo // database -> db_info - collInfo map[string]map[string]*collectionInfo // database -> collectionName -> collection_info - collLeader map[string]map[string]*shardLeaders // database -> collectionName -> collection_leaders - credMap map[string]*internalpb.CredentialInfo // cache for credential, lazy load - privilegeInfos map[string]struct{} // privileges cache - userToRoles map[string]map[string]struct{} // user to role cache - mu sync.RWMutex - credMut sync.RWMutex - leaderMut sync.RWMutex - shardMgr shardClientMgr - sfGlobal conc.Singleflight[*collectionInfo] - sfDB conc.Singleflight[*databaseInfo] + dbInfo map[string]*databaseInfo // database -> db_info + collInfo map[string]map[string]*collectionInfo // database -> collectionName -> collection_info + collLeader map[string]map[string]*shardLeaders // database -> collectionName -> collection_leaders + credMap map[string]*internalpb.CredentialInfo // cache for credential, lazy load + privilegeInfos map[string]struct{} // privileges cache + privilegeGroups map[string]map[string]struct{} // privilege group -> privileges + userToRoles map[string]map[string]struct{} // user to role cache + mu sync.RWMutex + credMut sync.RWMutex + leaderMut sync.RWMutex + shardMgr shardClientMgr + sfGlobal conc.Singleflight[*collectionInfo] + sfDB conc.Singleflight[*databaseInfo] IDStart int64 IDCount int64 @@ -376,7 +379,10 @@ func InitMetaCache(ctx context.Context, rootCoord types.RootCoordClient, queryCo log.Error("fail to init meta cache", zap.Error(err)) return err } - globalMetaCache.InitPolicyInfo(resp.PolicyInfos, resp.UserRoles) + err = globalMetaCache.InitPolicyInfo(resp.PolicyInfos, resp.UserRoles, resp.PrivilegeGroups) + if err != nil { + return err + } log.Info("success to init meta cache", zap.Strings("policy_infos", resp.PolicyInfos)) return nil } @@ -384,15 +390,16 @@ func InitMetaCache(ctx context.Context, rootCoord types.RootCoordClient, queryCo // NewMetaCache creates a MetaCache with provided RootCoord and QueryNode func NewMetaCache(rootCoord types.RootCoordClient, queryCoord types.QueryCoordClient, shardMgr shardClientMgr) (*MetaCache, error) { return &MetaCache{ - rootCoord: rootCoord, - queryCoord: queryCoord, - dbInfo: map[string]*databaseInfo{}, - collInfo: map[string]map[string]*collectionInfo{}, - collLeader: map[string]map[string]*shardLeaders{}, - credMap: map[string]*internalpb.CredentialInfo{}, - shardMgr: shardMgr, - privilegeInfos: map[string]struct{}{}, - userToRoles: map[string]map[string]struct{}{}, + rootCoord: rootCoord, + queryCoord: queryCoord, + dbInfo: map[string]*databaseInfo{}, + collInfo: map[string]map[string]*collectionInfo{}, + collLeader: map[string]map[string]*shardLeaders{}, + credMap: map[string]*internalpb.CredentialInfo{}, + shardMgr: shardMgr, + privilegeInfos: map[string]struct{}{}, + privilegeGroups: map[string]map[string]struct{}{}, + userToRoles: map[string]map[string]struct{}{}, }, nil } @@ -1062,7 +1069,7 @@ func (m *MetaCache) InvalidateShardLeaderCache(collections []int64) { } } -func (m *MetaCache) InitPolicyInfo(info []string, userRoles []string) { +func (m *MetaCache) InitPolicyInfo(info []string, userRoles []string, privilegeGroups []*milvuspb.PrivilegeGroupInfo) error { defer func() { err := getEnforcer().LoadPolicy() if err != nil { @@ -1071,10 +1078,13 @@ func (m *MetaCache) InitPolicyInfo(info []string, userRoles []string) { }() m.mu.Lock() defer m.mu.Unlock() - m.unsafeInitPolicyInfo(info, userRoles) + if err := m.unsafeInitPolicyInfo(info, userRoles, privilegeGroups); err != nil { + return err + } + return nil } -func (m *MetaCache) unsafeInitPolicyInfo(info []string, userRoles []string) { +func (m *MetaCache) unsafeInitPolicyInfo(info []string, userRoles []string, privilegeGroups []*milvuspb.PrivilegeGroupInfo) error { m.privilegeInfos = util.StringSet(info) for _, userRole := range userRoles { user, role, err := funcutil.DecodeUserRoleCache(userRole) @@ -1087,6 +1097,19 @@ func (m *MetaCache) unsafeInitPolicyInfo(info []string, userRoles []string) { } m.userToRoles[user][role] = struct{}{} } + for _, g := range privilegeGroups { + if m.privilegeGroups[g.GroupName] == nil { + m.privilegeGroups[g.GroupName] = make(map[string]struct{}) + } + for _, priv := range g.Privileges { + if !util.IsPrivilegeNameDefined(priv.Name) { + return fmt.Errorf("undefined privilege name: %s", priv.Name) + } + dbPrivName := util.PrivilegeNameForMetastore(priv.Name) + m.privilegeGroups[g.GroupName][dbPrivName] = struct{}{} + } + } + return nil } func (m *MetaCache) GetPrivilegeInfo(ctx context.Context) []string { @@ -1096,6 +1119,12 @@ func (m *MetaCache) GetPrivilegeInfo(ctx context.Context) []string { return util.StringList(m.privilegeInfos) } +func (m *MetaCache) GetGroupPrivileges(groupName string) map[string]struct{} { + m.mu.RLock() + defer m.mu.RUnlock() + return m.privilegeGroups[groupName] +} + func (m *MetaCache) GetUserRole(user string) []string { m.mu.RLock() defer m.mu.RUnlock() @@ -1143,6 +1172,33 @@ func (m *MetaCache) RefreshPolicyInfo(op typeutil.CacheOp) (err error) { if m.userToRoles[user] != nil { delete(m.userToRoles[user], role) } + case typeutil.CacheDropPrivilegeGroup: + delete(m.privilegeGroups, op.OpKey) + case typeutil.CacheAddPrivilegesToGroup: + groupInfo := &milvuspb.PrivilegeGroupInfo{} + err = proto.Unmarshal([]byte(op.OpKey), groupInfo) + if err != nil { + log.Error("failed to unmarshal privilege group info", zap.Error(err)) + return err + } + if m.privilegeGroups[groupInfo.GroupName] == nil { + m.privilegeGroups[groupInfo.GroupName] = make(map[string]struct{}) + } + for _, p := range groupInfo.Privileges { + m.privilegeGroups[groupInfo.GroupName][p.Name] = struct{}{} + } + case typeutil.CacheRemovePrivilegesFromGroup: + groupInfo := &milvuspb.PrivilegeGroupInfo{} + err = proto.Unmarshal([]byte(op.OpKey), groupInfo) + if err != nil { + log.Error("failed to unmarshal privilege group info", zap.Error(err)) + return err + } + if m.privilegeGroups[groupInfo.GroupName] != nil { + for _, p := range groupInfo.Privileges { + delete(m.privilegeGroups[groupInfo.GroupName], p.Name) + } + } case typeutil.CacheDeleteUser: delete(m.userToRoles, op.OpKey) case typeutil.CacheDropRole: @@ -1172,8 +1228,9 @@ func (m *MetaCache) RefreshPolicyInfo(op typeutil.CacheOp) (err error) { m.mu.Lock() defer m.mu.Unlock() m.userToRoles = make(map[string]map[string]struct{}) + m.privilegeGroups = make(map[string]map[string]struct{}) m.privilegeInfos = make(map[string]struct{}) - m.unsafeInitPolicyInfo(resp.PolicyInfos, resp.UserRoles) + m.unsafeInitPolicyInfo(resp.PolicyInfos, resp.UserRoles, resp.PrivilegeGroups) default: return fmt.Errorf("invalid opType, op_type: %d, op_key: %s", int(op.OpType), op.OpKey) } diff --git a/internal/proxy/meta_cache_test.go b/internal/proxy/meta_cache_test.go index 3f97a1dca6bbc..9ee07251350e8 100644 --- a/internal/proxy/meta_cache_test.go +++ b/internal/proxy/meta_cache_test.go @@ -30,6 +30,7 @@ import ( "github.com/stretchr/testify/require" uatomic "go.uber.org/atomic" "google.golang.org/grpc" + "google.golang.org/protobuf/proto" "github.com/milvus-io/milvus-proto/go-api/v2/commonpb" "github.com/milvus-io/milvus-proto/go-api/v2/milvuspb" @@ -626,6 +627,10 @@ func TestMetaCache_PolicyInfo(t *testing.T) { client := &MockRootCoordClientInterface{} qc := &mocks.MockQueryCoordClient{} mgr := newShardClientMgr() + privGroups := []*milvuspb.PrivilegeGroupInfo{{ + GroupName: "pg1", + Privileges: []*milvuspb.PrivilegeEntity{{Name: "CreateCollection"}, {Name: "DescribeCollection"}}, + }} t.Run("InitMetaCache", func(t *testing.T) { client.listPolicy = func(ctx context.Context, in *internalpb.ListPolicyRequest) (*internalpb.ListPolicyResponse, error) { @@ -636,8 +641,9 @@ func TestMetaCache_PolicyInfo(t *testing.T) { client.listPolicy = func(ctx context.Context, in *internalpb.ListPolicyRequest) (*internalpb.ListPolicyResponse, error) { return &internalpb.ListPolicyResponse{ - Status: merr.Success(), - PolicyInfos: []string{"policy1", "policy2", "policy3"}, + Status: merr.Success(), + PolicyInfos: []string{"policy1", "policy2", "policy3"}, + PrivilegeGroups: privGroups, }, nil } err = InitMetaCache(context.Background(), client, qc, mgr) @@ -647,9 +653,10 @@ func TestMetaCache_PolicyInfo(t *testing.T) { t.Run("GetPrivilegeInfo", func(t *testing.T) { client.listPolicy = func(ctx context.Context, in *internalpb.ListPolicyRequest) (*internalpb.ListPolicyResponse, error) { return &internalpb.ListPolicyResponse{ - Status: merr.Success(), - PolicyInfos: []string{"policy1", "policy2", "policy3"}, - UserRoles: []string{funcutil.EncodeUserRoleCache("foo", "role1"), funcutil.EncodeUserRoleCache("foo", "role2"), funcutil.EncodeUserRoleCache("foo2", "role2")}, + Status: merr.Success(), + PolicyInfos: []string{"policy1", "policy2", "policy3"}, + UserRoles: []string{funcutil.EncodeUserRoleCache("foo", "role1"), funcutil.EncodeUserRoleCache("foo", "role2"), funcutil.EncodeUserRoleCache("foo2", "role2")}, + PrivilegeGroups: privGroups, }, nil } err := InitMetaCache(context.Background(), client, qc, mgr) @@ -658,14 +665,17 @@ func TestMetaCache_PolicyInfo(t *testing.T) { assert.Equal(t, 3, len(policyInfos)) roles := globalMetaCache.GetUserRole("foo") assert.Equal(t, 2, len(roles)) + groups := globalMetaCache.GetGroupPrivileges("pg1") + assert.Equal(t, 2, len(groups)) }) t.Run("GetPrivilegeInfo", func(t *testing.T) { client.listPolicy = func(ctx context.Context, in *internalpb.ListPolicyRequest) (*internalpb.ListPolicyResponse, error) { return &internalpb.ListPolicyResponse{ - Status: merr.Success(), - PolicyInfos: []string{"policy1", "policy2", "policy3"}, - UserRoles: []string{funcutil.EncodeUserRoleCache("foo", "role1"), funcutil.EncodeUserRoleCache("foo", "role2"), funcutil.EncodeUserRoleCache("foo2", "role2")}, + Status: merr.Success(), + PolicyInfos: []string{"policy1", "policy2", "policy3"}, + UserRoles: []string{funcutil.EncodeUserRoleCache("foo", "role1"), funcutil.EncodeUserRoleCache("foo", "role2"), funcutil.EncodeUserRoleCache("foo2", "role2")}, + PrivilegeGroups: privGroups, }, nil } err := InitMetaCache(context.Background(), client, qc, mgr) @@ -691,6 +701,27 @@ func TestMetaCache_PolicyInfo(t *testing.T) { roles = globalMetaCache.GetUserRole("foo") assert.Equal(t, 2, len(roles)) + groupInfo := &milvuspb.PrivilegeGroupInfo{GroupName: "pg1", Privileges: []*milvuspb.PrivilegeEntity{{Name: "DropCollection"}}} + v, err := proto.Marshal(groupInfo) + assert.NoError(t, err) + err = globalMetaCache.RefreshPolicyInfo(typeutil.CacheOp{OpType: typeutil.CacheAddPrivilegesToGroup, OpKey: string(v)}) + assert.NoError(t, err) + groups := globalMetaCache.GetGroupPrivileges("pg1") + assert.Equal(t, 3, len(groups)) + + groupInfo = &milvuspb.PrivilegeGroupInfo{GroupName: "pg1", Privileges: []*milvuspb.PrivilegeEntity{{Name: "DropCollection"}, {Name: "RenameCollection"}}} + v, err = proto.Marshal(groupInfo) + assert.NoError(t, err) + err = globalMetaCache.RefreshPolicyInfo(typeutil.CacheOp{OpType: typeutil.CacheRemovePrivilegesFromGroup, OpKey: string(v)}) + assert.NoError(t, err) + groups = globalMetaCache.GetGroupPrivileges("pg1") + assert.Equal(t, 2, len(groups)) + + err = globalMetaCache.RefreshPolicyInfo(typeutil.CacheOp{OpType: typeutil.CacheDropPrivilegeGroup, OpKey: "pg1"}) + assert.NoError(t, err) + groups = globalMetaCache.GetGroupPrivileges("pg1") + assert.Equal(t, 0, len(groups)) + err = globalMetaCache.RefreshPolicyInfo(typeutil.CacheOp{OpType: typeutil.CacheGrantPrivilege, OpKey: ""}) assert.Error(t, err) err = globalMetaCache.RefreshPolicyInfo(typeutil.CacheOp{OpType: 100, OpKey: "policyX"}) @@ -706,7 +737,8 @@ func TestMetaCache_PolicyInfo(t *testing.T) { "policy2", "policy3", }, - UserRoles: []string{funcutil.EncodeUserRoleCache("foo", "role1"), funcutil.EncodeUserRoleCache("foo", "role2"), funcutil.EncodeUserRoleCache("foo2", "role2"), funcutil.EncodeUserRoleCache("foo2", "role3")}, + UserRoles: []string{funcutil.EncodeUserRoleCache("foo", "role1"), funcutil.EncodeUserRoleCache("foo", "role2"), funcutil.EncodeUserRoleCache("foo2", "role2"), funcutil.EncodeUserRoleCache("foo2", "role3")}, + PrivilegeGroups: privGroups, }, nil } err := InitMetaCache(context.Background(), client, qc, mgr) diff --git a/internal/proxy/mock_cache.go b/internal/proxy/mock_cache.go index cb1697b7f2ace..c1082456de42e 100644 --- a/internal/proxy/mock_cache.go +++ b/internal/proxy/mock_cache.go @@ -5,6 +5,7 @@ package proxy import ( context "context" + "github.com/milvus-io/milvus-proto/go-api/v2/milvuspb" internalpb "github.com/milvus-io/milvus/internal/proto/internalpb" mock "github.com/stretchr/testify/mock" @@ -914,9 +915,22 @@ func (_c *MockCache_HasDatabase_Call) RunAndReturn(run func(context.Context, str return _c } -// InitPolicyInfo provides a mock function with given fields: info, userRoles -func (_m *MockCache) InitPolicyInfo(info []string, userRoles []string) { - _m.Called(info, userRoles) +// InitPolicyInfo provides a mock function with given fields: info, userRoles, privilegeGroups +func (_m *MockCache) InitPolicyInfo(info []string, userRoles []string, privilegeGroups []*milvuspb.PrivilegeGroupInfo) error { + ret := _m.Called(info, userRoles, privilegeGroups) + + if len(ret) == 0 { + panic("no return value specified for InitPolicyInfo") + } + + var r0 error + if rf, ok := ret.Get(0).(func([]string, []string, []*milvuspb.PrivilegeGroupInfo) error); ok { + r0 = rf(info, userRoles, privilegeGroups) + } else { + r0 = ret.Error(0) + } + + return r0 } // MockCache_InitPolicyInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InitPolicyInfo' @@ -927,23 +941,23 @@ type MockCache_InitPolicyInfo_Call struct { // InitPolicyInfo is a helper method to define mock.On call // - info []string // - userRoles []string -func (_e *MockCache_Expecter) InitPolicyInfo(info interface{}, userRoles interface{}) *MockCache_InitPolicyInfo_Call { - return &MockCache_InitPolicyInfo_Call{Call: _e.mock.On("InitPolicyInfo", info, userRoles)} +func (_e *MockCache_Expecter) InitPolicyInfo(info interface{}, userRoles interface{}, privilegeGroups interface{}) *MockCache_InitPolicyInfo_Call { + return &MockCache_InitPolicyInfo_Call{Call: _e.mock.On("InitPolicyInfo", info, userRoles, privilegeGroups)} } -func (_c *MockCache_InitPolicyInfo_Call) Run(run func(info []string, userRoles []string)) *MockCache_InitPolicyInfo_Call { +func (_c *MockCache_InitPolicyInfo_Call) Run(run func(info []string, userRoles []string, privilegeGroups []*milvuspb.PrivilegeGroupInfo)) *MockCache_InitPolicyInfo_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].([]string), args[1].([]string)) + run(args[0].([]string), args[1].([]string), args[2].([]*milvuspb.PrivilegeGroupInfo)) }) return _c } -func (_c *MockCache_InitPolicyInfo_Call) Return() *MockCache_InitPolicyInfo_Call { - _c.Call.Return() +func (_c *MockCache_InitPolicyInfo_Call) Return(_a0 error) *MockCache_InitPolicyInfo_Call { + _c.Call.Return(_a0) return _c } -func (_c *MockCache_InitPolicyInfo_Call) RunAndReturn(run func([]string, []string)) *MockCache_InitPolicyInfo_Call { +func (_c *MockCache_InitPolicyInfo_Call) RunAndReturn(run func([]string, []string, []*milvuspb.PrivilegeGroupInfo) error) *MockCache_InitPolicyInfo_Call { _c.Call.Return(run) return _c } @@ -1247,6 +1261,54 @@ func (_c *MockCache_UpdateCredential_Call) RunAndReturn(run func(*internalpb.Cre return _c } +// GetGroupPrivileges provides a mock function with given fields: groupName +func (_m *MockCache) GetGroupPrivileges(groupName string) map[string]struct{} { + ret := _m.Called(groupName) + + if len(ret) == 0 { + panic("no return value specified for GetGroupPrivileges") + } + + var r0 map[string]struct{} + if rf, ok := ret.Get(0).(func(string) map[string]struct{}); ok { + r0 = rf(groupName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]struct{}) + } + } + + return r0 +} + +// MockCache_GetGroupPrivileges_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetGroupPrivileges' +type MockCache_GetGroupPrivileges_Call struct { + *mock.Call +} + +// GetGroupPrivileges is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockCache_Expecter) GetGroupPrivileges(groupName interface{}) *MockCache_GetGroupPrivileges_Call { + return &MockCache_GetGroupPrivileges_Call{Call: _e.mock.On("GetGroupPrivileges", groupName)} +} + +func (_c *MockCache_GetGroupPrivileges_Call) Run(run func(groupName string)) *MockCache_GetGroupPrivileges_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockCache_GetGroupPrivileges_Call) Return(_a0 map[string]struct{}) *MockCache_GetGroupPrivileges_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCache_GetGroupPrivileges_Call) RunAndReturn(run func(string) map[string]struct{}) *MockCache_GetGroupPrivileges_Call { + _c.Call.Return(run) + return _c +} + // NewMockCache creates a new instance of MockCache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockCache(t interface { diff --git a/internal/proxy/privilege_interceptor.go b/internal/proxy/privilege_interceptor.go index fc78fc35383dd..e1da5ddbd3d04 100644 --- a/internal/proxy/privilege_interceptor.go +++ b/internal/proxy/privilege_interceptor.go @@ -283,6 +283,11 @@ func PrivilegeGroupContains(args ...interface{}) (interface{}, error) { _, ok := adminPrivileges[requestPrivilege] return ok, nil default: - return false, nil + if !collMatch(requestObj, policyObj) { + return false, nil + } + privileges := globalMetaCache.GetGroupPrivileges(policyPrivilege) + _, ok := privileges[requestPrivilege] + return ok, nil } } diff --git a/internal/proxy/privilege_interceptor_test.go b/internal/proxy/privilege_interceptor_test.go index 92621d010eac7..23965bf8816b3 100644 --- a/internal/proxy/privilege_interceptor_test.go +++ b/internal/proxy/privilege_interceptor_test.go @@ -66,6 +66,16 @@ func TestPrivilegeInterceptor(t *testing.T) { funcutil.EncodeUserRoleCache("alice", "role1"), funcutil.EncodeUserRoleCache("fooo", "role2"), }, + PrivilegeGroups: []*milvuspb.PrivilegeGroupInfo{ + { + GroupName: "privilege_group1", + Privileges: []*milvuspb.PrivilegeEntity{{Name: "CreateCollection"}, {Name: "DescribeCollection"}}, + }, + { + GroupName: "privilege_group2", + Privileges: []*milvuspb.PrivilegeEntity{{Name: "DropCollection"}}, + }, + }, }, nil } @@ -202,6 +212,20 @@ func TestResourceGroupPrivilege(t *testing.T) { UserRoles: []string{ funcutil.EncodeUserRoleCache("fooo", "role1"), }, + PrivilegeGroups: []*milvuspb.PrivilegeGroupInfo{ + { + GroupName: "privilege_group1", + Privileges: []*milvuspb.PrivilegeEntity{{Name: "CreateCollection"}, {Name: "DropCollection"}}, + }, + { + GroupName: "privilege_group2", + Privileges: []*milvuspb.PrivilegeEntity{{Name: "DescribeCollection"}}, + }, + { + GroupName: "privilege_group3", + Privileges: []*milvuspb.PrivilegeEntity{{Name: "ShowCollections"}}, + }, + }, }, nil } InitMetaCache(ctx, client, queryCoord, mgr) @@ -232,7 +256,7 @@ func TestResourceGroupPrivilege(t *testing.T) { }) } -func TestPrivilegeGroup(t *testing.T) { +func TestBuiltinPrivilegeGroup(t *testing.T) { ctx := context.Background() t.Run("grant ReadOnly to single collection", func(t *testing.T) { @@ -563,3 +587,126 @@ func TestPrivilegeGroup(t *testing.T) { assert.NoError(t, err) }) } + +func TestCustomPrivilegeGroup(t *testing.T) { + ctx := context.Background() + + t.Run("grant privilege group operation privilege", 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() + + client.listPolicy = func(ctx context.Context, in *internalpb.ListPolicyRequest) (*internalpb.ListPolicyResponse, error) { + return &internalpb.ListPolicyResponse{ + Status: merr.Success(), + PolicyInfos: []string{ + funcutil.PolicyForPrivilege("role1", commonpb.ObjectType_Global.String(), "*", commonpb.ObjectPrivilege_PrivilegeCreatePrivilegeGroup.String(), "default"), + funcutil.PolicyForPrivilege("role1", commonpb.ObjectType_Global.String(), "*", commonpb.ObjectPrivilege_PrivilegeListPrivilegeGroups.String(), "default"), + }, + UserRoles: []string{ + funcutil.EncodeUserRoleCache("fooo", "role1"), + }, + }, nil + } + InitMetaCache(ctx, client, queryCoord, mgr) + defer CleanPrivilegeCache() + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.CreatePrivilegeGroupRequest{}) + assert.NoError(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.ListPrivilegeGroupsRequest{}) + assert.NoError(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.DropPrivilegeGroupRequest{}) + assert.Error(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.OperatePrivilegeGroupRequest{}) + assert.Error(t, err) + }) + + t.Run("grant custom privilege group to single collection", 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() + groupName := "customGroup" + + client.listPolicy = func(ctx context.Context, in *internalpb.ListPolicyRequest) (*internalpb.ListPolicyResponse, error) { + return &internalpb.ListPolicyResponse{ + Status: merr.Success(), + PolicyInfos: []string{ + funcutil.PolicyForPrivilege("role1", commonpb.ObjectType_Collection.String(), "col1", groupName, "default"), + }, + UserRoles: []string{ + funcutil.EncodeUserRoleCache("fooo", "role1"), + }, + PrivilegeGroups: []*milvuspb.PrivilegeGroupInfo{{ + GroupName: groupName, + Privileges: []*milvuspb.PrivilegeEntity{{Name: "Query"}, {Name: "Delete"}}, + }}, + }, nil + } + InitMetaCache(ctx, client, queryCoord, mgr) + defer CleanPrivilegeCache() + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.QueryRequest{CollectionName: "col1"}) + assert.NoError(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.DeleteRequest{CollectionName: "col1"}) + assert.NoError(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.QueryRequest{CollectionName: "col2"}) + assert.Error(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.DeleteRequest{CollectionName: "col2"}) + assert.Error(t, err) + }) + + t.Run("grant custom privilege group to all collection", 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() + groupName := "customGroup" + + client.listPolicy = func(ctx context.Context, in *internalpb.ListPolicyRequest) (*internalpb.ListPolicyResponse, error) { + return &internalpb.ListPolicyResponse{ + Status: merr.Success(), + PolicyInfos: []string{ + funcutil.PolicyForPrivilege("role1", commonpb.ObjectType_Global.String(), "*", groupName, "default"), + }, + UserRoles: []string{ + funcutil.EncodeUserRoleCache("fooo", "role1"), + }, + PrivilegeGroups: []*milvuspb.PrivilegeGroupInfo{{ + GroupName: groupName, + Privileges: []*milvuspb.PrivilegeEntity{{Name: "CreateCollection"}, {Name: "DescribeCollection"}}, + }}, + }, nil + } + InitMetaCache(ctx, client, queryCoord, mgr) + defer CleanPrivilegeCache() + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.CreateCollectionRequest{}) + assert.NoError(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.DescribeCollectionRequest{}) + assert.NoError(t, err) + + _, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.DropCollectionRequest{}) + assert.Error(t, err) + }) +} diff --git a/internal/proxy/rootcoord_mock_test.go b/internal/proxy/rootcoord_mock_test.go index b67246ffd006f..915b679db74b7 100644 --- a/internal/proxy/rootcoord_mock_test.go +++ b/internal/proxy/rootcoord_mock_test.go @@ -1134,6 +1134,22 @@ func (coord *RootCoordMock) RestoreRBAC(ctx context.Context, in *milvuspb.Restor return &commonpb.Status{}, nil } +func (coord *RootCoordMock) CreatePrivilegeGroup(ctx context.Context, req *milvuspb.CreatePrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + return &commonpb.Status{}, nil +} + +func (coord *RootCoordMock) DropPrivilegeGroup(ctx context.Context, req *milvuspb.DropPrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + return &commonpb.Status{}, nil +} + +func (coord *RootCoordMock) ListPrivilegeGroups(ctx context.Context, req *milvuspb.ListPrivilegeGroupsRequest, opts ...grpc.CallOption) (*milvuspb.ListPrivilegeGroupsResponse, error) { + return &milvuspb.ListPrivilegeGroupsResponse{}, nil +} + +func (coord *RootCoordMock) OperatePrivilegeGroup(ctx context.Context, req *milvuspb.OperatePrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + return &commonpb.Status{}, nil +} + type DescribeCollectionFunc func(ctx context.Context, request *milvuspb.DescribeCollectionRequest, opts ...grpc.CallOption) (*milvuspb.DescribeCollectionResponse, error) type ShowPartitionsFunc func(ctx context.Context, request *milvuspb.ShowPartitionsRequest, opts ...grpc.CallOption) (*milvuspb.ShowPartitionsResponse, error) diff --git a/internal/proxy/util.go b/internal/proxy/util.go index 602df517cfb3d..e8ddfc9b24126 100644 --- a/internal/proxy/util.go +++ b/internal/proxy/util.go @@ -1071,14 +1071,6 @@ func ValidateObjectType(entity string) error { return validateName(entity, "ObjectType") } -func ValidatePrincipalName(entity string) error { - return validateName(entity, "PrincipalName") -} - -func ValidatePrincipalType(entity string) error { - return validateName(entity, "PrincipalType") -} - func ValidatePrivilege(entity string) error { if util.IsAnyWord(entity) { return nil @@ -1129,6 +1121,13 @@ func AppendUserInfoForRPC(ctx context.Context) context.Context { return ctx } +func GetGroupPrivileges(groupName string) (map[string]struct{}, error) { + if globalMetaCache == nil { + return map[string]struct{}{}, merr.WrapErrServiceUnavailable("internal: Milvus Proxy is not ready yet. please wait") + } + return globalMetaCache.GetGroupPrivileges(groupName), nil +} + func GetRole(username string) ([]string, error) { if globalMetaCache == nil { return []string{}, merr.WrapErrServiceUnavailable("internal: Milvus Proxy is not ready yet. please wait") diff --git a/internal/proxy/util_test.go b/internal/proxy/util_test.go index bcf17fd62d113..f4f1b814e0703 100644 --- a/internal/proxy/util_test.go +++ b/internal/proxy/util_test.go @@ -804,8 +804,6 @@ func TestValidateName(t *testing.T) { assert.Nil(t, ValidateRoleName(name)) assert.Nil(t, ValidateObjectName(name)) assert.Nil(t, ValidateObjectType(name)) - assert.Nil(t, ValidatePrincipalName(name)) - assert.Nil(t, ValidatePrincipalType(name)) assert.Nil(t, ValidatePrivilege(name)) } @@ -828,8 +826,6 @@ func TestValidateName(t *testing.T) { assert.NotNil(t, validateName(name, nameType)) assert.NotNil(t, ValidateRoleName(name)) assert.NotNil(t, ValidateObjectType(name)) - assert.NotNil(t, ValidatePrincipalName(name)) - assert.NotNil(t, ValidatePrincipalType(name)) assert.NotNil(t, ValidatePrivilege(name)) } assert.NotNil(t, ValidateObjectName(" ")) @@ -923,6 +919,25 @@ func TestGetRole(t *testing.T) { assert.Equal(t, 1, len(roles)) } +func TestGetGroupPrivileges(t *testing.T) { + globalMetaCache = nil + _, err := GetGroupPrivileges("foo") + assert.Error(t, err) + mockCache := NewMockCache(t) + mockCache.On("GetGroupPrivileges", + mock.AnythingOfType("string"), + ).Return(func(groupName string) map[string]struct{} { + return map[string]struct{}{ + "CreateCollection": {}, + "DescribeCollection": {}, + } + }) + globalMetaCache = mockCache + privileges, err := GetGroupPrivileges("group1") + assert.NoError(t, err) + assert.Equal(t, 2, len(privileges)) +} + func TestPasswordVerify(t *testing.T) { username := "user-test00" password := "PasswordVerify" diff --git a/internal/rootcoord/meta_table.go b/internal/rootcoord/meta_table.go index 54ba01c55492d..130308e8b4a78 100644 --- a/internal/rootcoord/meta_table.go +++ b/internal/rootcoord/meta_table.go @@ -99,6 +99,11 @@ type IMetaTable interface { ListUserRole(tenant string) ([]string, error) BackupRBAC(ctx context.Context, tenant string) (*milvuspb.RBACMeta, error) RestoreRBAC(ctx context.Context, tenant string, meta *milvuspb.RBACMeta) error + IsCustomPrivilegeGroup(groupName string) (bool, error) + CreatePrivilegeGroup(groupName string) error + DropPrivilegeGroup(groupName string) error + ListPrivilegeGroups() ([]*milvuspb.PrivilegeGroupInfo, error) + OperatePrivilegeGroup(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error } // MetaTable is a persistent meta set of all databases, collections and partitions. @@ -1456,3 +1461,146 @@ func (mt *MetaTable) RestoreRBAC(ctx context.Context, tenant string, meta *milvu return mt.catalog.RestoreRBAC(mt.ctx, tenant, meta) } + +// check if the privielge group name is defined by users +func (mt *MetaTable) IsCustomPrivilegeGroup(groupName string) (bool, error) { + privGroups, err := mt.catalog.ListPrivilegeGroups(mt.ctx) + if err != nil { + return false, err + } + for _, group := range privGroups { + if group.GroupName == groupName { + return true, nil + } + } + return false, nil +} + +func (mt *MetaTable) CreatePrivilegeGroup(groupName string) error { + if funcutil.IsEmptyString(groupName) { + return fmt.Errorf("the privilege group name is empty") + } + mt.permissionLock.Lock() + defer mt.permissionLock.Unlock() + + definedByUsers, err := mt.IsCustomPrivilegeGroup(groupName) + if err != nil { + return err + } + if definedByUsers { + return merr.WrapErrParameterInvalidMsg("privilege group name [%s] is defined by users", groupName) + } + if util.IsPrivilegeNameDefined(groupName) { + return merr.WrapErrParameterInvalidMsg("privilege group name [%s] is defined by built in privileges or privilege groups in system", groupName) + } + privileges := make([]*milvuspb.PrivilegeEntity, 0) + return mt.catalog.AlterPrivilegeGroup(mt.ctx, groupName, privileges) +} + +func (mt *MetaTable) DropPrivilegeGroup(groupName string) error { + if funcutil.IsEmptyString(groupName) { + return fmt.Errorf("the privilege group name is empty") + } + mt.permissionLock.Lock() + defer mt.permissionLock.Unlock() + + definedByUsers, err := mt.IsCustomPrivilegeGroup(groupName) + if err != nil { + return err + } + if !definedByUsers { + return merr.WrapErrParameterInvalidMsg("there is no privilege group name [%s] to drop", groupName) + } + // check if the group is used by any role + roles, err := mt.catalog.ListRole(mt.ctx, util.DefaultTenant, nil, false) + if err != nil { + return err + } + roleEntity := lo.Map(roles, func(entity *milvuspb.RoleResult, _ int) *milvuspb.RoleEntity { + return entity.GetRole() + }) + for _, role := range roleEntity { + grants, err := mt.catalog.ListGrant(mt.ctx, util.DefaultTenant, &milvuspb.GrantEntity{ + Role: role, + DbName: util.AnyWord, + }) + if err != nil { + return err + } + for _, grant := range grants { + if grant.Grantor.Privilege.Name == groupName { + return errors.Newf("privilege group [%s] is used by role [%s], Use REVOKE API to revoke it first", groupName, role.GetName()) + } + } + } + return mt.catalog.DropPrivilegeGroup(mt.ctx, groupName) +} + +func (mt *MetaTable) ListPrivilegeGroups() ([]*milvuspb.PrivilegeGroupInfo, error) { + mt.permissionLock.Lock() + defer mt.permissionLock.Unlock() + + return mt.catalog.ListPrivilegeGroups(mt.ctx) +} + +func (mt *MetaTable) OperatePrivilegeGroup(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error { + if funcutil.IsEmptyString(groupName) { + return fmt.Errorf("the privilege group name is empty") + } + mt.permissionLock.Lock() + defer mt.permissionLock.Unlock() + + // validate input params + definedByUsers, err := mt.IsCustomPrivilegeGroup(groupName) + if err != nil { + return err + } + if !definedByUsers { + return merr.WrapErrParameterInvalidMsg("there is no privilege group name [%s] to operate", groupName) + } + for _, p := range privileges { + if util.IsPrivilegeNameDefined(p.Name) { + continue + } + groups, err := mt.ListPrivilegeGroups() + if err != nil { + return err + } + for _, group := range groups { + // add privileges for custom privilege group + if group.GroupName == p.Name { + privileges = append(privileges, group.Privileges...) + } else { + return merr.WrapErrParameterInvalidMsg("there is no privilege name or privielge group name [%s] defined in system to operate", p.Name) + } + } + } + + // merge with current privileges + group, err := mt.catalog.GetPrivilegeGroup(mt.ctx, groupName) + if err != nil { + log.Warn("fail to get privilege group", zap.String("privilege_group", groupName), zap.Error(err)) + return err + } + privSet := lo.SliceToMap(group.Privileges, func(p *milvuspb.PrivilegeEntity) (string, struct{}) { + return p.Name, struct{}{} + }) + switch operateType { + case milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup: + for _, p := range privileges { + privSet[p.Name] = struct{}{} + } + case milvuspb.OperatePrivilegeGroupType_RemovePrivilegesFromGroup: + for _, p := range privileges { + delete(privSet, p.Name) + } + default: + log.Warn("unsupported operate type", zap.Any("operate_type", operateType)) + return fmt.Errorf("unsupported operate type: %v", operateType) + } + + mergedPrivs := lo.Map(lo.Keys(privSet), func(priv string, _ int) *milvuspb.PrivilegeEntity { + return &milvuspb.PrivilegeEntity{Name: priv} + }) + return mt.catalog.AlterPrivilegeGroup(mt.ctx, groupName, mergedPrivs) +} diff --git a/internal/rootcoord/meta_table_test.go b/internal/rootcoord/meta_table_test.go index 7bd666f7ca3c4..6a6cdf3f0377d 100644 --- a/internal/rootcoord/meta_table_test.go +++ b/internal/rootcoord/meta_table_test.go @@ -2071,3 +2071,32 @@ func TestMetaTable_RestoreRBAC(t *testing.T) { err = mt.RestoreRBAC(context.TODO(), util.DefaultTenant, &milvuspb.RBACMeta{}) assert.Error(t, err) } + +func TestMetaTable_PrivilegeGroup(t *testing.T) { + catalog := mocks.NewRootCoordCatalog(t) + catalog.EXPECT().ListPrivilegeGroups(mock.Anything).Return([]*milvuspb.PrivilegeGroupInfo{ + { + GroupName: "pg1", + Privileges: []*milvuspb.PrivilegeEntity{{Name: "CreateCollection"}, {Name: "DescribeCollection"}}, + }, + }, nil) + catalog.EXPECT().ListRole(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) + catalog.EXPECT().AlterPrivilegeGroup(mock.Anything, mock.Anything, mock.Anything).Return(nil) + catalog.EXPECT().DropPrivilegeGroup(mock.Anything, mock.Anything).Return(nil) + mt := &MetaTable{ + dbName2Meta: map[string]*model.Database{ + "not_commit": model.NewDatabase(1, "not_commit", pb.DatabaseState_DatabaseCreated, nil), + }, + names: newNameDb(), + aliases: newNameDb(), + catalog: catalog, + } + err := mt.CreatePrivilegeGroup("pg1") + assert.Error(t, err) + err = mt.CreatePrivilegeGroup("pg2") + assert.NoError(t, err) + err = mt.DropPrivilegeGroup("pg1") + assert.NoError(t, err) + _, err = mt.ListPrivilegeGroups() + assert.NoError(t, err) +} diff --git a/internal/rootcoord/mock_test.go b/internal/rootcoord/mock_test.go index fa384a781b068..0bb82803f22c0 100644 --- a/internal/rootcoord/mock_test.go +++ b/internal/rootcoord/mock_test.go @@ -97,6 +97,10 @@ type mockMetaTable struct { ListPolicyFunc func(tenant string) ([]string, error) ListUserRoleFunc func(tenant string) ([]string, error) DescribeDatabaseFunc func(ctx context.Context, dbName string) (*model.Database, error) + CreatePrivilegeGroupFunc func(groupName string) error + DropPrivilegeGroupFunc func(groupName string) error + ListPrivilegeGroupsFunc func() ([]*milvuspb.PrivilegeGroupInfo, error) + OperatePrivilegeGroupFunc func(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error } func (m mockMetaTable) GetDatabaseByName(ctx context.Context, dbName string, ts Timestamp) (*model.Database, error) { @@ -251,6 +255,22 @@ func (m mockMetaTable) ListUserRole(tenant string) ([]string, error) { return m.ListUserRoleFunc(tenant) } +func (m mockMetaTable) CreatePrivilegeGroup(groupName string) error { + return m.CreatePrivilegeGroupFunc(groupName) +} + +func (m mockMetaTable) DropPrivilegeGroup(groupName string) error { + return m.DropPrivilegeGroupFunc(groupName) +} + +func (m mockMetaTable) ListPrivilegeGroups() ([]*milvuspb.PrivilegeGroupInfo, error) { + return m.ListPrivilegeGroupsFunc() +} + +func (m mockMetaTable) OperatePrivilegeGroup(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error { + return m.OperatePrivilegeGroupFunc(groupName, privileges, operateType) +} + func newMockMetaTable() *mockMetaTable { return &mockMetaTable{} } @@ -527,6 +547,18 @@ func withInvalidMeta() Opt { meta.DescribeDatabaseFunc = func(ctx context.Context, dbName string) (*model.Database, error) { return nil, errors.New("error mock DescribeDatabase") } + meta.CreatePrivilegeGroupFunc = func(groupName string) error { + return errors.New("error mock CreatePrivilegeGroup") + } + meta.DropPrivilegeGroupFunc = func(groupName string) error { + return errors.New("error mock DropPrivilegeGroup") + } + meta.ListPrivilegeGroupsFunc = func() ([]*milvuspb.PrivilegeGroupInfo, error) { + return nil, errors.New("error mock ListPrivilegeGroups") + } + meta.OperatePrivilegeGroupFunc = func(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error { + return errors.New("error mock OperatePrivilegeGroup") + } return withMeta(meta) } diff --git a/internal/rootcoord/mocks/meta_table.go b/internal/rootcoord/mocks/meta_table.go index 243adbd726ed6..d54a138f5f6a1 100644 --- a/internal/rootcoord/mocks/meta_table.go +++ b/internal/rootcoord/mocks/meta_table.go @@ -423,6 +423,263 @@ func (_c *IMetaTable_BackupRBAC_Call) RunAndReturn(run func(context.Context, str return _c } + +// IsCustomPrivilegeGroup provides a mock function with given fields: groupName +func (_m *IMetaTable) IsCustomPrivilegeGroup(groupName string) (bool, error) { + ret := _m.Called(groupName) + + if len(ret) == 0 { + panic("no return value specified for IsCustomPrivilegeGroup") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(string) (bool, error)); ok { + return rf(groupName) + } + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(groupName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(bool) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(groupName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IMetaTable_IsCustomPrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCustomPrivilegeGroup' +type IMetaTable_IsCustomPrivilegeGroup_Call struct { + *mock.Call +} + +// IsCustomPrivilegeGroup is a helper method to define mock.On call +// - groupName string +func (_e *IMetaTable_Expecter) IsCustomPrivilegeGroup(groupName interface{}) *IMetaTable_IsCustomPrivilegeGroup_Call { + return &IMetaTable_IsCustomPrivilegeGroup_Call{Call: _e.mock.On("IsCustomPrivilegeGroup", groupName)} +} + +func (_c *IMetaTable_IsCustomPrivilegeGroup_Call) Run(run func(groupName string)) *IMetaTable_IsCustomPrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *IMetaTable_IsCustomPrivilegeGroup_Call) Return(_a0 bool, _a1 error) *IMetaTable_IsCustomPrivilegeGroup_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *IMetaTable_IsCustomPrivilegeGroup_Call) RunAndReturn(run func(string) (bool, error)) *IMetaTable_IsCustomPrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + + +// CreatePrivilegeGroup provides a mock function with given fields: groupName +func (_m *IMetaTable) CreatePrivilegeGroup(groupName string) error { + ret := _m.Called(groupName) + + if len(ret) == 0 { + panic("no return value specified for CreatePrivilegeGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(groupName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// IMetaTable_CreatePrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePrivilegeGroup' +type IMetaTable_CreatePrivilegeGroup_Call struct { + *mock.Call +} + +// CreatePrivilegeGroup is a helper method to define mock.On call +// - groupName string +func (_e *IMetaTable_Expecter) CreatePrivilegeGroup(groupName interface{}) *IMetaTable_CreatePrivilegeGroup_Call { + return &IMetaTable_CreatePrivilegeGroup_Call{Call: _e.mock.On("CreatePrivilegeGroup", groupName)} +} + +func (_c *IMetaTable_CreatePrivilegeGroup_Call) Run(run func(groupName string)) *IMetaTable_CreatePrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *IMetaTable_CreatePrivilegeGroup_Call) Return(_a0 error) *IMetaTable_CreatePrivilegeGroup_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *IMetaTable_CreatePrivilegeGroup_Call) RunAndReturn(run func(string) error) *IMetaTable_CreatePrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +// DropPrivilegeGroup provides a mock function with given fields: groupName +func (_m *IMetaTable) DropPrivilegeGroup(groupName string) error { + ret := _m.Called(groupName) + + if len(ret) == 0 { + panic("no return value specified for DropPrivilegeGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(groupName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// IMetaTable_DropPrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DropPrivilegeGroup' +type IMetaTable_DropPrivilegeGroup_Call struct { + *mock.Call +} + +// DropPrivilegeGroup is a helper method to define mock.On call +// - ctx context.Context +// - groupName string +func (_e *IMetaTable_Expecter) DropPrivilegeGroup(groupName interface{}) *IMetaTable_DropPrivilegeGroup_Call { + return &IMetaTable_DropPrivilegeGroup_Call{Call: _e.mock.On("DropPrivilegeGroup", groupName)} +} + +func (_c *IMetaTable_DropPrivilegeGroup_Call) Run(run func(groupName string)) *IMetaTable_DropPrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *IMetaTable_DropPrivilegeGroup_Call) Return(_a0 error) *IMetaTable_DropPrivilegeGroup_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *IMetaTable_DropPrivilegeGroup_Call) RunAndReturn(run func(context.Context, string) error) *IMetaTable_DropPrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + +func (_m *IMetaTable) ListPrivilegeGroups() ([]*milvuspb.PrivilegeGroupInfo, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ListPrivilegeGroups") + } + + var r0 []*milvuspb.PrivilegeGroupInfo + var r1 error + if rf, ok := ret.Get(0).(func() ([]*milvuspb.PrivilegeGroupInfo, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() []*milvuspb.PrivilegeGroupInfo); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*milvuspb.PrivilegeGroupInfo) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// IMetaTable_ListPrivilegeGroups_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPrivilegeGroups' +type IMetaTable_ListPrivilegeGroups_Call struct { + *mock.Call +} + +// ListPrivilegeGroups is a helper method to define mock.On call +func (_e *IMetaTable_Expecter) ListPrivilegeGroups() *IMetaTable_ListPrivilegeGroups_Call { + return &IMetaTable_ListPrivilegeGroups_Call{Call: _e.mock.On("ListPrivilegeGroups")} +} + +func (_c *IMetaTable_ListPrivilegeGroups_Call) Run(run func()) *IMetaTable_ListPrivilegeGroups_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *IMetaTable_ListPrivilegeGroups_Call) Return(_a0 []*milvuspb.PrivilegeGroupInfo, _a1 error) *IMetaTable_ListPrivilegeGroups_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *IMetaTable_ListPrivilegeGroups_Call) RunAndReturn(run func() ([]*milvuspb.PrivilegeGroupInfo, error)) *IMetaTable_ListPrivilegeGroups_Call { + _c.Call.Return(run) + return _c +} + +// OperatePrivilegeGroup provides a mock function with given fields: groupName, privileges, operateType +func (_m *IMetaTable) OperatePrivilegeGroup(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error { + ret := _m.Called(groupName, privileges, operateType) + + if len(ret) == 0 { + panic("no return value specified for OperatePrivilegeGroup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, []*milvuspb.PrivilegeEntity, milvuspb.OperatePrivilegeGroupType) error); ok { + r0 = rf(groupName, privileges, operateType) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// IMetaTable_OperatePrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OperatePrivilegeGroup' +type IMetaTable_OperatePrivilegeGroup_Call struct { + *mock.Call +} + +// OperatePrivilegeGroup is a helper method to define mock.On call +// - groupName string +// - privileges []*milvuspb.PrivilegeEntity +// - operateType milvuspb.OperatePrivilegeGroupType +func (_e *IMetaTable_Expecter) OperatePrivilegeGroup(groupName interface{}, privileges interface{}, operateType interface{}) *IMetaTable_OperatePrivilegeGroup_Call { + return &IMetaTable_OperatePrivilegeGroup_Call{Call: _e.mock.On("OperatePrivilegeGroup", groupName, privileges, operateType)} +} + +func (_c *IMetaTable_OperatePrivilegeGroup_Call) Run(run func(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType)) *IMetaTable_OperatePrivilegeGroup_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].([]*milvuspb.PrivilegeEntity), args[2].(milvuspb.OperatePrivilegeGroupType)) + }) + return _c +} + +func (_c *IMetaTable_OperatePrivilegeGroup_Call) Return(_a0 error) *IMetaTable_OperatePrivilegeGroup_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *IMetaTable_OperatePrivilegeGroup_Call) RunAndReturn(run func(string, []*milvuspb.PrivilegeEntity, milvuspb.OperatePrivilegeGroupType) error) *IMetaTable_OperatePrivilegeGroup_Call { + _c.Call.Return(run) + return _c +} + // ChangeCollectionState provides a mock function with given fields: ctx, collectionID, state, ts func (_m *IMetaTable) ChangeCollectionState(ctx context.Context, collectionID int64, state etcdpb.CollectionState, ts uint64) error { ret := _m.Called(ctx, collectionID, state, ts) diff --git a/internal/rootcoord/root_coord.go b/internal/rootcoord/root_coord.go index ad6ef5fc597f2..ee20c22f8f326 100644 --- a/internal/rootcoord/root_coord.go +++ b/internal/rootcoord/root_coord.go @@ -32,6 +32,7 @@ import ( "go.uber.org/atomic" "go.uber.org/zap" "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/proto" "github.com/milvus-io/milvus-proto/go-api/v2/commonpb" "github.com/milvus-io/milvus-proto/go-api/v2/milvuspb" @@ -626,7 +627,11 @@ func (c *Core) initBuiltinRoles() error { for _, privilege := range privilegesJSON[util.RoleConfigPrivileges] { privilegeName := privilege[util.RoleConfigPrivilege] if !util.IsAnyWord(privilege[util.RoleConfigPrivilege]) { - privilegeName = util.PrivilegeNameForMetastore(privilege[util.RoleConfigPrivilege]) + dbPrivName, err := c.getMetastorePrivilegeName(privilege[util.RoleConfigPrivilege]) + if err != nil { + return errors.Wrapf(err, "failed to get metastore privilege name for: %s", privilege[util.RoleConfigPrivilege]) + } + privilegeName = dbPrivName } err := c.meta.OperatePrivilege(util.DefaultTenant, &milvuspb.GrantEntity{ Role: &milvuspb.RoleEntity{Name: role}, @@ -2494,11 +2499,8 @@ func (c *Core) isValidGrantor(entity *milvuspb.GrantorEntity, object string) err if entity == nil { return errors.New("the grantor entity is nil") } - if entity.User == nil { - return errors.New("the user entity in the grantor entity is nil") - } - if entity.User.Name == "" { - return errors.New("the name in the user entity of the grantor entity is empty") + 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)) @@ -2510,17 +2512,25 @@ func (c *Core) isValidGrantor(entity *milvuspb.GrantorEntity, object string) err if util.IsAnyWord(entity.Privilege.Name) { return nil } - if privilegeName := util.PrivilegeNameForMetastore(entity.Privilege.Name); privilegeName == "" { - return fmt.Errorf("not found the privilege name[%s]", entity.Privilege.Name) + // check object privileges for built-in privileges + if util.IsPrivilegeNameDefined(entity.Privilege.Name) { + 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 { + return nil + } + } } - 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)) + // check if it is a custom privilege group + customPrivGroup, err := c.meta.IsCustomPrivilegeGroup(entity.Privilege.Name) + if err != nil { + return err } - for _, privilege := range privileges { - if privilege == entity.Privilege.Name { - return nil - } + if customPrivGroup { + return nil } return fmt.Errorf("not found the privilege name[%s] in object[%s]", entity.Privilege.Name, object) } @@ -2565,11 +2575,18 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil } - ctxLog.Debug("before PrivilegeNameForMetastore", zap.String("privilege", in.Entity.Grantor.Privilege.Name)) - if !util.IsAnyWord(in.Entity.Grantor.Privilege.Name) { - in.Entity.Grantor.Privilege.Name = util.PrivilegeNameForMetastore(in.Entity.Grantor.Privilege.Name) + // 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 { + return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil + } + in.Entity.Grantor.Privilege.Name = dbPrivName } - ctxLog.Debug("after PrivilegeNameForMetastore", zap.String("privilege", in.Entity.Grantor.Privilege.Name)) + ctxLog.Debug("after PrivilegeNameForMetastore", zap.String("privilege", privName)) + if in.Entity.Object.Name == commonpb.ObjectType_Global.String() { in.Entity.ObjectName = util.AnyWord } @@ -2617,6 +2634,22 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile return merr.Success(), nil } +func (c *Core) getMetastorePrivilegeName(privName string) (string, error) { + // if it is built-in privilege, return the privilege name directly + if util.IsPrivilegeNameDefined(privName) { + return util.PrivilegeNameForMetastore(privName), nil + } + // return the privilege group name if it is a custom privilege group + customGroup, err := c.meta.IsCustomPrivilegeGroup(privName) + if err != nil { + return "", err + } + if customGroup { + return util.PrivilegeGroupNameForMetastore(privName), nil + } + return "", errors.New("not found the privilege name") +} + // SelectGrant select grant // - check the node health // - check if the principal entity is valid @@ -2709,14 +2742,23 @@ func (c *Core) ListPolicy(ctx context.Context, in *internalpb.ListPolicyRequest) Status: merr.StatusWithErrorCode(errors.New(errMsg), commonpb.ErrorCode_ListPolicyFailure), }, nil } + privGroups, err := c.meta.ListPrivilegeGroups() + if err != nil { + errMsg := "fail to list privilege groups" + ctxLog.Warn(errMsg, zap.Error(err)) + return &internalpb.ListPolicyResponse{ + Status: merr.StatusWithErrorCode(errors.New(errMsg), commonpb.ErrorCode_ListPolicyFailure), + }, nil + } ctxLog.Debug(method + " success") metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.SuccessLabel).Inc() metrics.RootCoordDDLReqLatency.WithLabelValues(method).Observe(float64(tr.ElapseSpan().Milliseconds())) return &internalpb.ListPolicyResponse{ - Status: merr.Success(), - PolicyInfos: policies, - UserRoles: userRoles, + Status: merr.Success(), + PolicyInfos: policies, + UserRoles: userRoles, + PrivilegeGroups: privGroups, }, nil } @@ -2917,3 +2959,161 @@ func (c *Core) CheckHealth(ctx context.Context, in *milvuspb.CheckHealthRequest) return &milvuspb.CheckHealthResponse{Status: merr.Success(), IsHealthy: true, Reasons: []string{}}, nil } + +func (c *Core) CreatePrivilegeGroup(ctx context.Context, in *milvuspb.CreatePrivilegeGroupRequest) (*commonpb.Status, error) { + method := "CreatePrivilegeGroup" + metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.TotalLabel).Inc() + tr := timerecord.NewTimeRecorder(method) + 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 err := c.meta.CreatePrivilegeGroup(in.GroupName); err != nil { + ctxLog.Warn("fail to create privilege group", zap.Error(err)) + return merr.Status(err), nil + } + + ctxLog.Debug(method + " success") + metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.SuccessLabel).Inc() + metrics.RootCoordDDLReqLatency.WithLabelValues(method).Observe(float64(tr.ElapseSpan().Milliseconds())) + metrics.RootCoordNumOfPrivilegeGroups.Inc() + return merr.Success(), nil +} + +func (c *Core) DropPrivilegeGroup(ctx context.Context, in *milvuspb.DropPrivilegeGroupRequest) (*commonpb.Status, error) { + method := "DropPrivilegeGroup" + metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.TotalLabel).Inc() + tr := timerecord.NewTimeRecorder(method) + 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 + } + + redoTask := newBaseRedoTask(c.stepExecutor) + redoTask.AddSyncStep(NewSimpleStep("drop privilege group meta data", func(ctx context.Context) ([]nestedStep, error) { + if err := c.meta.DropPrivilegeGroup(in.GroupName); err != nil { + ctxLog.Warn("fail to drop privilege group", zap.Error(err)) + return nil, err + } + return nil, nil + })) + + redoTask.AddAsyncStep(NewSimpleStep("drop privilege group cache", func(ctx context.Context) ([]nestedStep, error) { + if err := c.proxyClientManager.RefreshPolicyInfoCache(ctx, &proxypb.RefreshPolicyInfoCacheRequest{ + OpType: int32(typeutil.CacheDropPrivilegeGroup), + OpKey: in.GroupName, + }); err != nil { + log.Warn("drop privilege group cache failed", zap.Any("in", in), zap.Error(err)) + return nil, err + } + return nil, nil + })) + + err := redoTask.Execute(ctx) + if err != nil { + errMsg := "fail to execute task when dropping the privilege group" + log.Warn(errMsg, zap.Error(err)) + return merr.StatusWithErrorCode(err, commonpb.ErrorCode_DropPrivilegeGroupFailure), nil + } + + ctxLog.Debug(method + " success") + metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.SuccessLabel).Inc() + metrics.RootCoordDDLReqLatency.WithLabelValues(method).Observe(float64(tr.ElapseSpan().Milliseconds())) + metrics.RootCoordNumOfPrivilegeGroups.Desc() + return merr.Success(), nil +} + +func (c *Core) ListPrivilegeGroups(ctx context.Context, in *milvuspb.ListPrivilegeGroupsRequest) (*milvuspb.ListPrivilegeGroupsResponse, error) { + method := "ListPrivilegeGroups" + metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.TotalLabel).Inc() + tr := timerecord.NewTimeRecorder(method) + 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 &milvuspb.ListPrivilegeGroupsResponse{ + Status: merr.Status(err), + }, nil + } + + privGroups, err := c.meta.ListPrivilegeGroups() + if err != nil { + ctxLog.Warn("fail to list privilege group", zap.Error(err)) + return &milvuspb.ListPrivilegeGroupsResponse{ + Status: merr.StatusWithErrorCode(err, commonpb.ErrorCode_ListPrivilegeGroupsFailure), + }, nil + } + + ctxLog.Debug(method + " success") + metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.SuccessLabel).Inc() + metrics.RootCoordDDLReqLatency.WithLabelValues(method).Observe(float64(tr.ElapseSpan().Milliseconds())) + return &milvuspb.ListPrivilegeGroupsResponse{ + Status: merr.Success(), + PrivilegeGroups: privGroups, + }, nil +} + +func (c *Core) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePrivilegeGroupRequest) (*commonpb.Status, error) { + method := "OperatePrivilegeGroup-" + in.Type.String() + metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.TotalLabel).Inc() + tr := timerecord.NewTimeRecorder(method) + 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 + } + + redoTask := newBaseRedoTask(c.stepExecutor) + redoTask.AddSyncStep(NewSimpleStep("operate privilege group meta data", func(ctx context.Context) ([]nestedStep, error) { + err := c.meta.OperatePrivilegeGroup(in.GroupName, in.Privileges, in.Type) + if err != nil && !common.IsIgnorableError(err) { + log.Warn("fail to operate privilege group", zap.Error(err)) + return nil, err + } + return nil, nil + })) + redoTask.AddAsyncStep(NewSimpleStep("operate privilege group cache", func(ctx context.Context) ([]nestedStep, error) { + var opType int32 + switch in.Type { + case milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup: + opType = int32(typeutil.CacheAddPrivilegesToGroup) + case milvuspb.OperatePrivilegeGroupType_RemovePrivilegesFromGroup: + opType = int32(typeutil.CacheRemovePrivilegesFromGroup) + default: + errMsg := "invalid operate type for the OperatePrivilegeGroup api" + log.Warn(errMsg, zap.Any("in", in)) + return nil, nil + } + groupInfo := &milvuspb.PrivilegeGroupInfo{GroupName: in.GroupName, Privileges: in.Privileges} + v, err := proto.Marshal(groupInfo) + if err != nil { + log.Error("failed to marshal privilege group info during operate privilege group", zap.Error(err)) + return nil, err + } + if err := c.proxyClientManager.RefreshPolicyInfoCache(ctx, &proxypb.RefreshPolicyInfoCacheRequest{ + OpType: opType, + OpKey: string(v), + }); err != nil { + log.Warn("fail to refresh policy info cache", zap.Any("in", in), zap.Error(err)) + return nil, err + } + return nil, nil + })) + err := redoTask.Execute(ctx) + if err != nil { + errMsg := "fail to execute task when operate privilege group" + log.Warn(errMsg, zap.Error(err)) + return merr.StatusWithErrorCode(errors.New(errMsg), commonpb.ErrorCode_OperatePrivilegeGroupFailure), nil + } + + ctxLog.Debug(method + " success") + metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.SuccessLabel).Inc() + metrics.RootCoordDDLReqLatency.WithLabelValues(method).Observe(float64(tr.ElapseSpan().Milliseconds())) + return merr.Success(), nil +} diff --git a/internal/rootcoord/root_coord_test.go b/internal/rootcoord/root_coord_test.go index 0e5d555ac3983..a3ad2b376f8c2 100644 --- a/internal/rootcoord/root_coord_test.go +++ b/internal/rootcoord/root_coord_test.go @@ -1748,6 +1748,9 @@ func TestRootCoord_RBACError(t *testing.T) { mockMeta.SelectRoleFunc = func(tenant string, entity *milvuspb.RoleEntity, includeUserInfo bool) ([]*milvuspb.RoleResult, error) { return nil, nil } + mockMeta.ListPrivilegeGroupsFunc = func() ([]*milvuspb.PrivilegeGroupInfo, error) { + return nil, nil + } { resp, err := c.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{Entity: &milvuspb.GrantEntity{ Role: &milvuspb.RoleEntity{Name: "foo"}, @@ -1879,6 +1882,9 @@ func TestRootCoord_BuiltinRoles(t *testing.T) { mockMeta.OperatePrivilegeFunc = func(tenant string, entity *milvuspb.GrantEntity, operateType milvuspb.OperatePrivilegeType) error { return nil } + mockMeta.ListPrivilegeGroupsFunc = func() ([]*milvuspb.PrivilegeGroupInfo, error) { + return nil, nil + } err := c.initBuiltinRoles() assert.Equal(t, nil, err) assert.True(t, util.IsBuiltinRole(roleDbAdmin)) diff --git a/internal/util/mock/grpc_rootcoord_client.go b/internal/util/mock/grpc_rootcoord_client.go index 2f3901df2d8a4..ea75308bb96d5 100644 --- a/internal/util/mock/grpc_rootcoord_client.go +++ b/internal/util/mock/grpc_rootcoord_client.go @@ -274,6 +274,22 @@ func (m *GrpcRootCoordClient) RestoreRBAC(ctx context.Context, in *milvuspb.Rest return &commonpb.Status{}, m.Err } +func (m *GrpcRootCoordClient) CreatePrivilegeGroup(ctx context.Context, in *milvuspb.CreatePrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + return &commonpb.Status{}, m.Err +} + +func (m *GrpcRootCoordClient) DropPrivilegeGroup(ctx context.Context, in *milvuspb.DropPrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + return &commonpb.Status{}, m.Err +} + +func (m *GrpcRootCoordClient) ListPrivilegeGroups(ctx context.Context, in *milvuspb.ListPrivilegeGroupsRequest, opts ...grpc.CallOption) (*milvuspb.ListPrivilegeGroupsResponse, error) { + return &milvuspb.ListPrivilegeGroupsResponse{}, m.Err +} + +func (m *GrpcRootCoordClient) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePrivilegeGroupRequest, opts ...grpc.CallOption) (*commonpb.Status, error) { + return &commonpb.Status{}, m.Err +} + func (m *GrpcRootCoordClient) Close() error { return nil } diff --git a/pkg/go.mod b/pkg/go.mod index c8b02ff79019d..5b0dad8085e1f 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -26,8 +26,6 @@ require ( github.com/shirou/gopsutil/v3 v3.22.9 github.com/sirupsen/logrus v1.9.0 github.com/spaolacci/murmur3 v1.1.0 - github.com/spf13/cast v1.3.1 - github.com/spf13/viper v1.8.1 github.com/streamnative/pulsarctl v0.5.0 github.com/stretchr/testify v1.9.0 github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c @@ -57,6 +55,7 @@ require ( google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.28.6 ) @@ -86,7 +85,6 @@ require ( github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect - github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/getsentry/sentry-go v0.12.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -99,7 +97,6 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect @@ -107,19 +104,16 @@ require ( github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/ianlancetaylor/cgosymbolizer v0.0.0-20221217025313-27d3c9f66b6a // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/linkedin/goavro/v2 v2.11.1 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.8 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/minio/highwayhash v1.0.2 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mtibben/percent v0.2.1 // indirect @@ -128,7 +122,6 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/opencontainers/runtime-spec v1.0.2 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pelletier/go-toml v1.9.3 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/pingcap/errors v0.11.5-0.20211224045212-9687c2b0f87c // indirect @@ -144,14 +137,10 @@ require ( github.com/prometheus/procfs v0.9.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect - github.com/smartystreets/assertions v1.1.0 // indirect github.com/soheilhy/cmux v0.1.5 // indirect - github.com/spf13/afero v1.6.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stathat/consistent v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/subosito/gotenv v1.2.0 // indirect github.com/tiancaiamao/gp v0.0.0-20221230034425-4025bc8a4d4a // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect @@ -180,9 +169,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) @@ -197,3 +184,5 @@ replace ( ) exclude github.com/apache/pulsar-client-go/oauth2 v0.0.0-20211108044248-fe3b7c4e445b + +replace github.com/milvus-io/milvus-proto/go-api/v2 => github.com/shaoting-huang/milvus-proto/go-api/v2 v2.0.0-20241106022119-cd9f3e43c08c diff --git a/pkg/go.sum b/pkg/go.sum index 0ce2c42e4740c..e780a4c51249c 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -335,8 +335,6 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= -github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= @@ -374,7 +372,6 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -417,7 +414,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -439,7 +435,6 @@ github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ib github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -493,8 +488,6 @@ 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.20240909041258-8f8ca67816cd h1:x0b0+foTe23sKcVFseR1DE8+BB08EH6ViiRHaz8PEik= -github.com/milvus-io/milvus-proto/go-api/v2 v2.3.4-0.20240909041258-8f8ca67816cd/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= @@ -507,8 +500,6 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -557,8 +548,6 @@ github.com/panjf2000/ants/v2 v2.7.2 h1:2NUt9BaZFO5kQzrieOmK/wdb/tQ/K+QHaxN8sOgD6 github.com/panjf2000/ants/v2 v2.7.2/go.mod h1:KIBmYG9QQX5U2qzFP/yQJaq/nSb6rahS9iEHkrCMgM8= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= @@ -581,7 +570,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -647,6 +635,8 @@ github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtm github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shaoting-huang/milvus-proto/go-api/v2 v2.0.0-20241106022119-cd9f3e43c08c h1:DhD0iqNocvSYgE6BO1XNj1KrpAoSVLm0wWTM5hUleVU= +github.com/shaoting-huang/milvus-proto/go-api/v2 v2.0.0-20241106022119-cd9f3e43c08c/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= github.com/shirou/gopsutil/v3 v3.22.9 h1:yibtJhIVEMcdw+tCTbOPiF1VcsuDeTE4utJ8Dm4c5eA= github.com/shirou/gopsutil/v3 v3.22.9/go.mod h1:bBYl1kjgEJpWpxeHmLI+dVHWtyAwfcmSBLDsp2TNT8A= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -657,9 +647,6 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= -github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= @@ -668,25 +655,17 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stathat/consistent v1.0.0 h1:ZFJ1QTRn8npNBKW065raSZ8xfOqhpb8vLOkfp4CcL/U= github.com/stathat/consistent v1.0.0/go.mod h1:uajTPbgSygZBJ+V+0mY7meZ8i0XAcZs7AQ6V121XSxw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -708,7 +687,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= @@ -853,7 +831,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1002,7 +979,6 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1283,8 +1259,6 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= diff --git a/pkg/metrics/rootcoord_metrics.go b/pkg/metrics/rootcoord_metrics.go index 86ad947aaf3f5..9ec5118da19b7 100644 --- a/pkg/metrics/rootcoord_metrics.go +++ b/pkg/metrics/rootcoord_metrics.go @@ -146,6 +146,15 @@ var ( Help: "The number of roles", }) + // RootCoordNumOfPrivilegeGroups counts the number of credentials. + RootCoordNumOfPrivilegeGroups = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: milvusNamespace, + Subsystem: typeutil.RootCoordRole, + Name: "num_of_privilege_groups", + Help: "The number of privilege groups", + }) + // RootCoordTtDelay records the max time tick delay of flow graphs in DataNodes and QueryNodes. RootCoordTtDelay = prometheus.NewGaugeVec( prometheus.GaugeOpts{ diff --git a/pkg/util/constant.go b/pkg/util/constant.go index e51206875a2d4..d68ac7fce276a 100644 --- a/pkg/util/constant.go +++ b/pkg/util/constant.go @@ -21,6 +21,7 @@ import ( "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" ) // Meta Prefix consts @@ -114,6 +115,10 @@ var ( MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGetFlushState.String()), MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupReadOnly.String()), MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupReadWrite.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreatePrivilegeGroup.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropPrivilegeGroup.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListPrivilegeGroups.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeOperatePrivilegeGroup.String()), }, commonpb.ObjectType_Global.String(): { MetaStore2API(commonpb.ObjectPrivilege_PrivilegeAll.String()), @@ -151,6 +156,10 @@ var ( MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDescribeAlias.String()), MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListAliases.String()), MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupAdmin.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreatePrivilegeGroup.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropPrivilegeGroup.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListPrivilegeGroups.String()), + MetaStore2API(commonpb.ObjectPrivilege_PrivilegeOperatePrivilegeGroup.String()), }, commonpb.ObjectType_User.String(): { MetaStore2API(commonpb.ObjectPrivilege_PrivilegeUpdateUser.String()), @@ -306,6 +315,9 @@ func MetaStore2API(name string) string { func PrivilegeNameForAPI(name string) string { _, ok := commonpb.ObjectPrivilege_value[name] if !ok { + if strings.HasPrefix(name, PrivilegeGroupWord) { + return typeutil.After(name, PrivilegeGroupWord) + } return "" } return MetaStore2API(name) @@ -327,6 +339,15 @@ func PrivilegeNameForMetastore(name string) string { return dbPrivilege } +// check if the name is defined by built in privileges or privilege groups in system +func IsPrivilegeNameDefined(name string) bool { + return PrivilegeNameForMetastore(name) != "" +} + +func PrivilegeGroupNameForMetastore(name string) string { + return PrivilegeGroupWord + name +} + func IsAnyWord(word string) bool { return word == AnyWord } diff --git a/pkg/util/typeutil/cache.go b/pkg/util/typeutil/cache.go index 0d82addaa4b71..31ac1d9a6181d 100644 --- a/pkg/util/typeutil/cache.go +++ b/pkg/util/typeutil/cache.go @@ -10,6 +10,9 @@ const ( CacheDeleteUser CacheDropRole CacheRefresh + CacheDropPrivilegeGroup + CacheAddPrivilegesToGroup + CacheRemovePrivilegesFromGroup ) type CacheOp struct { diff --git a/scripts/download_milvus_proto.sh b/scripts/download_milvus_proto.sh index 623ec1df52a5d..a77523cee91ca 100755 --- a/scripts/download_milvus_proto.sh +++ b/scripts/download_milvus_proto.sh @@ -2,15 +2,15 @@ SCRIPTS_DIR=$(dirname "$0") THIRD_PARTY_DIR=$SCRIPTS_DIR/../cmake_build/thirdparty -API_VERSION=$(go list -m github.com/milvus-io/milvus-proto/go-api/v2 | awk -F' ' '{print $2}') +API_VERSION=$(go list -m github.com/shaoting-huang/milvus-proto/go-api/v2@v2.0.0-20241106022119-cd9f3e43c08c | awk -F' ' '{print $2}') if [ ! -d "$THIRD_PARTY_DIR/milvus-proto" ]; then mkdir -p $THIRD_PARTY_DIR pushd $THIRD_PARTY_DIR - git clone https://github.com/milvus-io/milvus-proto.git + git clone https://github.com/shaoting-huang/milvus-proto.git cd milvus-proto # try tagged version first - COMMIT_ID=$(git ls-remote https://github.com/milvus-io/milvus-proto.git refs/tags/${API_VERSION} | cut -f 1) + COMMIT_ID=$(git ls-remote https://github.com/shaoting-huang/milvus-proto.git refs/tags/${API_VERSION} | cut -f 1) if [[ -z $COMMIT_ID ]]; then # parse commit from pseudo version (eg v0.0.0-20230608062631-c453ef1b870a => c453ef1b870a) COMMIT_ID=$(echo $API_VERSION | awk -F'-' '{print $3}') diff --git a/tests/integration/rbac/privilege_group_test.go b/tests/integration/rbac/privilege_group_test.go index da89b603a472e..8e8ab745ce09c 100644 --- a/tests/integration/rbac/privilege_group_test.go +++ b/tests/integration/rbac/privilege_group_test.go @@ -41,57 +41,110 @@ func (s *PrivilegeGroupTestSuite) SetupSuite() { paramtable.Get().Save(paramtable.Get().CommonCfg.AuthorizationEnabled.Key, "true") } -func (s *PrivilegeGroupTestSuite) TestPrivilegeGroup() { +func (s *PrivilegeGroupTestSuite) TestBuiltinPrivilegeGroup() { ctx := GetContext(context.Background(), "root:123456") - // test empty rbac content + + // Test empty RBAC content resp, err := s.Cluster.Proxy.BackupRBAC(ctx, &milvuspb.BackupRBACMetaRequest{}) s.NoError(err) s.True(merr.Ok(resp.GetStatus())) s.Equal("", resp.GetRBACMeta().String()) - // generate some rbac content + // Generate some RBAC content roleName := "test_role" - resp1, err := s.Cluster.Proxy.CreateRole(ctx, &milvuspb.CreateRoleRequest{ - Entity: &milvuspb.RoleEntity{ - Name: roleName, - }, + createRoleResp, err := s.Cluster.Proxy.CreateRole(ctx, &milvuspb.CreateRoleRequest{ + Entity: &milvuspb.RoleEntity{Name: roleName}, }) s.NoError(err) - s.True(merr.Ok(resp1)) - resp2, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ - Type: milvuspb.OperatePrivilegeType_Grant, - Entity: &milvuspb.GrantEntity{ - Role: &milvuspb.RoleEntity{Name: roleName}, - Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()}, - ObjectName: util.AnyWord, - DbName: util.AnyWord, - Grantor: &milvuspb.GrantorEntity{ - User: &milvuspb.UserEntity{Name: util.UserRoot}, - Privilege: &milvuspb.PrivilegeEntity{Name: "ReadOnly"}, - }, - }, + s.True(merr.Ok(createRoleResp)) + + s.grantPrivilege(ctx, roleName, "ReadOnly", commonpb.ObjectType_Collection.String(), util.AnyWord, util.AnyWord) + s.grantPrivilege(ctx, roleName, "ReadWrite", commonpb.ObjectType_Collection.String(), util.AnyWord, util.AnyWord) + s.grantPrivilege(ctx, roleName, "Admin", commonpb.ObjectType_Global.String(), util.AnyWord, util.AnyWord) + + s.validateGrants(ctx, roleName, commonpb.ObjectType_Global.String(), util.AnyWord, util.AnyWord, 1) + s.validateGrants(ctx, roleName, commonpb.ObjectType_Collection.String(), util.AnyWord, util.AnyWord, 2) +} + +func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() { + ctx := GetContext(context.Background(), "root:123456") + groupName := "test_privilege_group" + + // 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 + listAndValidatePrivilegeGroup := func(expectedGroupName string, expectedPrivilegeCount int) []*milvuspb.PrivilegeEntity { + resp, err := s.Cluster.Proxy.ListPrivilegeGroups(ctx, &milvuspb.ListPrivilegeGroupsRequest{}) + s.NoError(err) + privGroupInfo := resp.PrivilegeGroups[0] + s.Equal(expectedGroupName, privGroupInfo.GroupName) + s.Equal(expectedPrivilegeCount, len(privGroupInfo.Privileges)) + return privGroupInfo.Privileges + } + + // Test creating a privilege group + createResp, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{ + GroupName: groupName, }) s.NoError(err) - s.True(merr.Ok(resp2)) + s.True(merr.Ok(createResp)) - resp3, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ - Type: milvuspb.OperatePrivilegeType_Grant, - Entity: &milvuspb.GrantEntity{ - Role: &milvuspb.RoleEntity{Name: roleName}, - Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()}, - ObjectName: util.AnyWord, - DbName: util.AnyWord, - Grantor: &milvuspb.GrantorEntity{ - User: &milvuspb.UserEntity{Name: util.UserRoot}, - Privilege: &milvuspb.PrivilegeEntity{Name: "ReadWrite"}, - }, - }, + // Validate the group was created + listAndValidatePrivilegeGroup(groupName, 0) + + // Test adding privileges + operatePrivilegeGroup(groupName, milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, []*milvuspb.PrivilegeEntity{ + {Name: "CreateCollection"}, + {Name: "DescribeCollection"}, + }) + listAndValidatePrivilegeGroup(groupName, 2) + + // Test adding more privileges (one duplicate, one new) + operatePrivilegeGroup(groupName, milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, []*milvuspb.PrivilegeEntity{ + {Name: "DescribeCollection"}, + {Name: "DropCollection"}, + }) + listAndValidatePrivilegeGroup(groupName, 3) + + // Test removing privileges (including a non-existent one) + operatePrivilegeGroup(groupName, milvuspb.OperatePrivilegeGroupType_RemovePrivilegesFromGroup, []*milvuspb.PrivilegeEntity{ + {Name: "DescribeCollection"}, + {Name: "DropCollection"}, + {Name: "RenameCollection"}, + }) + privileges := listAndValidatePrivilegeGroup(groupName, 1) + s.Equal("CreateCollection", privileges[0].Name) + + // Test grant privilege group + roleName := "test_role" + createRoleResp, err := s.Cluster.Proxy.CreateRole(ctx, &milvuspb.CreateRoleRequest{ + Entity: &milvuspb.RoleEntity{Name: roleName}, }) s.NoError(err) - s.True(merr.Ok(resp3)) + s.True(merr.Ok(createRoleResp)) - resp4, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ - Type: milvuspb.OperatePrivilegeType_Grant, + // Grant privileges + s.grantPrivilege(ctx, roleName, groupName, commonpb.ObjectType_Global.String(), util.AnyWord, util.AnyWord) + s.validateGrants(ctx, roleName, commonpb.ObjectType_Global.String(), util.AnyWord, util.AnyWord, 1) + + // Drop the group during any role usage will cause error + dropResp, _ := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ + GroupName: groupName, + }) + s.Error(merr.Error(dropResp)) + + // Revoke privilege group + revokeResp, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ + Type: milvuspb.OperatePrivilegeType_Revoke, Entity: &milvuspb.GrantEntity{ Role: &milvuspb.RoleEntity{Name: roleName}, Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()}, @@ -99,36 +152,63 @@ func (s *PrivilegeGroupTestSuite) TestPrivilegeGroup() { DbName: util.AnyWord, Grantor: &milvuspb.GrantorEntity{ User: &milvuspb.UserEntity{Name: util.UserRoot}, - Privilege: &milvuspb.PrivilegeEntity{Name: "Admin"}, + Privilege: &milvuspb.PrivilegeEntity{Name: groupName}, }, }, }) s.NoError(err) - s.True(merr.Ok(resp4)) + s.True(merr.Ok(revokeResp)) + + // Drop the privilege group after revoking the privilege will succeed + dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ + GroupName: groupName, + }) + s.NoError(err) + 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: roleName, + }) + s.NoError(err) + s.True(merr.Ok(dropRoleResp)) +} - resp5, err := s.Cluster.Proxy.SelectGrant(ctx, &milvuspb.SelectGrantRequest{ +func (s *PrivilegeGroupTestSuite) grantPrivilege(ctx context.Context, roleName, privilegeName, objectType, objectName, dbName string) { + resp, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ + Type: milvuspb.OperatePrivilegeType_Grant, Entity: &milvuspb.GrantEntity{ Role: &milvuspb.RoleEntity{Name: roleName}, - Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()}, - ObjectName: util.AnyWord, - DbName: util.AnyWord, + Object: &milvuspb.ObjectEntity{Name: objectType}, + ObjectName: objectName, + DbName: dbName, + Grantor: &milvuspb.GrantorEntity{ + User: &milvuspb.UserEntity{Name: util.UserRoot}, + Privilege: &milvuspb.PrivilegeEntity{Name: privilegeName}, + }, }, }) s.NoError(err) - s.True(merr.Ok(resp5.GetStatus())) - s.Len(resp5.GetEntities(), 1) + s.True(merr.Ok(resp)) +} - resp6, err := s.Cluster.Proxy.SelectGrant(ctx, &milvuspb.SelectGrantRequest{ +func (s *PrivilegeGroupTestSuite) validateGrants(ctx context.Context, roleName, objectType, objectName, dbName string, expectedCount int) { + resp, err := s.Cluster.Proxy.SelectGrant(ctx, &milvuspb.SelectGrantRequest{ Entity: &milvuspb.GrantEntity{ Role: &milvuspb.RoleEntity{Name: roleName}, - Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()}, - ObjectName: util.AnyWord, - DbName: util.AnyWord, + Object: &milvuspb.ObjectEntity{Name: objectType}, + ObjectName: objectName, + DbName: dbName, }, }) s.NoError(err) - s.True(merr.Ok(resp6.GetStatus())) - s.Len(resp6.GetEntities(), 2) + s.True(merr.Ok(resp.GetStatus())) + s.Len(resp.GetEntities(), expectedCount) } func TestPrivilegeGroup(t *testing.T) { diff --git a/tests/integration/rbac/rbac_backup_test.go b/tests/integration/rbac/rbac_backup_test.go index de4e271e9163a..5bb1e2adb7260 100644 --- a/tests/integration/rbac/rbac_backup_test.go +++ b/tests/integration/rbac/rbac_backup_test.go @@ -20,6 +20,7 @@ import ( "strings" "testing" + "github.com/samber/lo" "github.com/stretchr/testify/suite" "google.golang.org/grpc/metadata" @@ -63,131 +64,161 @@ func GetContext(ctx context.Context, originValue string) context.Context { func (s *RBACBackupTestSuite) TestBackup() { ctx := GetContext(context.Background(), "root:123456") + + createRole := func(name string) { + resp, err := s.Cluster.Proxy.CreateRole(ctx, &milvuspb.CreateRoleRequest{ + Entity: &milvuspb.RoleEntity{Name: name}, + }) + s.NoError(err) + s.True(merr.Ok(resp)) + } + + operatePrivilege := func(role, privilege, objectName, dbName string, operateType milvuspb.OperatePrivilegeType) { + 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_Collection.String()}, + ObjectName: objectName, + DbName: dbName, + Grantor: &milvuspb.GrantorEntity{User: &milvuspb.UserEntity{Name: util.UserRoot}, Privilege: &milvuspb.PrivilegeEntity{Name: privilege}}, + }, + }) + s.NoError(err) + s.True(merr.Ok(resp)) + } + // test empty rbac content - resp, err := s.Cluster.Proxy.BackupRBAC(ctx, &milvuspb.BackupRBACMetaRequest{}) + emptyBackupRBACResp, 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(emptyBackupRBACResp.GetStatus())) + s.Equal("", emptyBackupRBACResp.GetRBACMeta().String()) // generate some rbac content + // create role test_role roleName := "test_role" - resp1, err := s.Cluster.Proxy.CreateRole(ctx, &milvuspb.CreateRoleRequest{ - Entity: &milvuspb.RoleEntity{ - Name: roleName, - }, + createRole(roleName) + + // grant collection level search privilege to role test_role + operatePrivilege(roleName, "Search", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + + // create privielge group test_group + groupName := "test_group" + createPrivGroupResp, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{ + GroupName: groupName, }) s.NoError(err) - s.True(merr.Ok(resp1)) - resp2, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ - Type: milvuspb.OperatePrivilegeType_Grant, - Entity: &milvuspb.GrantEntity{ - Role: &milvuspb.RoleEntity{Name: roleName}, - Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()}, - ObjectName: util.AnyWord, - DbName: util.AnyWord, - Grantor: &milvuspb.GrantorEntity{ - User: &milvuspb.UserEntity{Name: util.UserRoot}, - Privilege: &milvuspb.PrivilegeEntity{Name: "Search"}, - }, - }, + s.True(merr.Ok(createPrivGroupResp)) + + // add query and insert privilege to group test_group + addPrivsToGroupResp, err := s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{ + GroupName: groupName, + Privileges: []*milvuspb.PrivilegeEntity{{Name: "Query"}, {Name: "Insert"}}, + Type: milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, }) s.NoError(err) - s.True(merr.Ok(resp2)) - s.Equal("", resp2.GetReason()) + s.True(merr.Ok(addPrivsToGroupResp)) + + // grant privilege group test_group to role test_role + operatePrivilege(roleName, groupName, util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant) + userName := "test_user" passwd := "test_passwd" - resp3, err := s.Cluster.Proxy.CreateCredential(ctx, &milvuspb.CreateCredentialRequest{ + createCredResp, err := s.Cluster.Proxy.CreateCredential(ctx, &milvuspb.CreateCredentialRequest{ Username: userName, Password: crypto.Base64Encode(passwd), }) s.NoError(err) - s.True(merr.Ok(resp3)) - resp4, err := s.Cluster.Proxy.OperateUserRole(ctx, &milvuspb.OperateUserRoleRequest{ + s.True(merr.Ok(createCredResp)) + operateUserRoleResp, err := s.Cluster.Proxy.OperateUserRole(ctx, &milvuspb.OperateUserRoleRequest{ Username: userName, RoleName: roleName, }) s.NoError(err) - s.True(merr.Ok(resp4)) + s.True(merr.Ok(operateUserRoleResp)) - // test back up rbac - resp5, err := s.Cluster.Proxy.BackupRBAC(ctx, &milvuspb.BackupRBACMetaRequest{}) + // test back up rbac, grants should contain + backupRBACResp, err := s.Cluster.Proxy.BackupRBAC(ctx, &milvuspb.BackupRBACMetaRequest{}) s.NoError(err) - s.True(merr.Ok(resp5.GetStatus())) + s.True(merr.Ok(backupRBACResp.GetStatus())) + s.Equal(2, len(backupRBACResp.GetRBACMeta().Grants)) + grants := lo.SliceToMap(backupRBACResp.GetRBACMeta().Grants, func(g *milvuspb.GrantEntity) (string, *milvuspb.GrantEntity) { + return g.Grantor.Privilege.Name, g + }) + s.True(grants["Search"] != nil) + s.True(grants[groupName] != nil) + s.Equal(groupName, backupRBACResp.GetRBACMeta().PrivilegeGroups[0].GroupName) + s.Equal(2, len(backupRBACResp.GetRBACMeta().PrivilegeGroups[0].Privileges)) // test restore, expect to failed due to role/user already exist - resp6, err := s.Cluster.Proxy.RestoreRBAC(ctx, &milvuspb.RestoreRBACMetaRequest{ - RBACMeta: resp5.GetRBACMeta(), + restoreRBACResp, err := s.Cluster.Proxy.RestoreRBAC(ctx, &milvuspb.RestoreRBACMetaRequest{ + RBACMeta: backupRBACResp.GetRBACMeta(), }) s.NoError(err) - s.False(merr.Ok(resp6)) - - // drop exist role/user, successful to restore - resp7, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ - Type: milvuspb.OperatePrivilegeType_Revoke, - Entity: &milvuspb.GrantEntity{ - Role: &milvuspb.RoleEntity{Name: roleName}, - Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()}, - ObjectName: util.AnyWord, - DbName: util.AnyWord, - Grantor: &milvuspb.GrantorEntity{ - User: &milvuspb.UserEntity{Name: util.UserRoot}, - Privilege: &milvuspb.PrivilegeEntity{Name: "Search"}, - }, - }, + s.False(merr.Ok(restoreRBACResp)) + + // revoke privilege search from role test_role before dropping the role + operatePrivilege(roleName, "Search", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + + // revoke privilege group test_group from role test_role before dropping the role + operatePrivilege(roleName, groupName, util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + + // drop privilege group test_group + dropPrivGroupResp, err := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ + GroupName: groupName, }) s.NoError(err) - s.True(merr.Ok(resp7)) - resp8, err := s.Cluster.Proxy.DropRole(ctx, &milvuspb.DropRoleRequest{ + s.True(merr.Ok(dropPrivGroupResp)) + + // drop role test_role + dropRoleResp, err := s.Cluster.Proxy.DropRole(ctx, &milvuspb.DropRoleRequest{ RoleName: roleName, }) s.NoError(err) - s.True(merr.Ok(resp8)) - resp9, err := s.Cluster.Proxy.DeleteCredential(ctx, &milvuspb.DeleteCredentialRequest{ + s.True(merr.Ok(dropRoleResp)) + + // delete credential + delCredResp, err := s.Cluster.Proxy.DeleteCredential(ctx, &milvuspb.DeleteCredentialRequest{ Username: userName, }) s.NoError(err) - s.True(merr.Ok(resp9)) + s.True(merr.Ok(delCredResp)) - resp10, err := s.Cluster.Proxy.RestoreRBAC(ctx, &milvuspb.RestoreRBACMetaRequest{ - RBACMeta: resp5.GetRBACMeta(), + // restore rbac + restoreRBACResp, err = s.Cluster.Proxy.RestoreRBAC(ctx, &milvuspb.RestoreRBACMetaRequest{ + RBACMeta: backupRBACResp.GetRBACMeta(), }) s.NoError(err) - s.True(merr.Ok(resp10)) + s.True(merr.Ok(restoreRBACResp)) // check the restored rbac, should be same as the original one - resp11, err := s.Cluster.Proxy.BackupRBAC(ctx, &milvuspb.BackupRBACMetaRequest{}) + backupRBACResp2, err := s.Cluster.Proxy.BackupRBAC(ctx, &milvuspb.BackupRBACMetaRequest{}) s.NoError(err) - s.True(merr.Ok(resp11.GetStatus())) - s.Equal(resp11.GetRBACMeta().String(), resp5.GetRBACMeta().String()) + s.True(merr.Ok(backupRBACResp2.GetStatus())) + s.Equal(backupRBACResp2.GetRBACMeta().String(), backupRBACResp.GetRBACMeta().String()) // clean rbac meta - resp12, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{ - Type: milvuspb.OperatePrivilegeType_Revoke, - Entity: &milvuspb.GrantEntity{ - Role: &milvuspb.RoleEntity{Name: roleName}, - Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()}, - ObjectName: util.AnyWord, - DbName: util.AnyWord, - Grantor: &milvuspb.GrantorEntity{ - User: &milvuspb.UserEntity{Name: util.UserRoot}, - Privilege: &milvuspb.PrivilegeEntity{Name: "Search"}, - }, - }, + operatePrivilege(roleName, "Search", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + + operatePrivilege(roleName, groupName, util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke) + + dropPrivGroupResp2, err := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{ + GroupName: groupName, }) s.NoError(err) - s.True(merr.Ok(resp12)) + s.True(merr.Ok(dropPrivGroupResp2)) - resp13, err := s.Cluster.Proxy.DropRole(ctx, &milvuspb.DropRoleRequest{ + dropRoleResp2, err := s.Cluster.Proxy.DropRole(ctx, &milvuspb.DropRoleRequest{ RoleName: roleName, }) s.NoError(err) - s.True(merr.Ok(resp13)) + s.True(merr.Ok(dropRoleResp2)) - resp14, err := s.Cluster.Proxy.DeleteCredential(ctx, &milvuspb.DeleteCredentialRequest{ + delCredResp2, err := s.Cluster.Proxy.DeleteCredential(ctx, &milvuspb.DeleteCredentialRequest{ Username: userName, }) s.NoError(err) - s.True(merr.Ok(resp14)) + s.True(merr.Ok(delCredResp2)) } func TestRBACBackup(t *testing.T) {