From 9e8b6ace6d3c374514ab06e3fa7b5978242ba753 Mon Sep 17 00:00:00 2001
From: sthuang <167743503+shaoting-huang@users.noreply.github.com>
Date: Mon, 11 Nov 2024 14:20:29 +0800
Subject: [PATCH] enhance: [2.4] RBAC custom privilege group (#37560)

Cherry-pick from master
pr: https://github.com/milvus-io/milvus/pull/37087,
https://github.com/milvus-io/milvus/pull/37558
issue: #37031

---------

Signed-off-by: shaoting-huang <shaoting.huang@zilliz.com>
---
 go.mod                                        |   2 +-
 go.sum                                        |   4 +-
 internal/datacoord/mock_test.go               |  16 +
 internal/datanode/importv2/pool_test.go       |   3 +-
 .../distributed/proxy/httpserver/constant.go  |  46 +--
 .../proxy/httpserver/handler_v2.go            |  86 +++++
 .../proxy/httpserver/handler_v2_test.go       |  28 +-
 .../proxy/httpserver/request_v2.go            |   5 +
 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 | 124 ++++++-
 .../metastore/kv/rootcoord/kv_catalog_test.go | 173 +++++++++-
 .../kv/rootcoord/rootcoord_constant.go        |   7 +
 .../metastore/mocks/mock_rootcoord_catalog.go | 195 +++++++++++
 internal/mocks/mock_proxy.go                  | 220 ++++++++++++
 internal/mocks/mock_querynode.go              |   4 -
 internal/mocks/mock_querynode_client.go       |   4 -
 internal/mocks/mock_rootcoord.go              | 220 ++++++++++++
 internal/mocks/mock_rootcoord_client.go       | 280 +++++++++++++++
 internal/proto/internal.proto                 |   2 +
 internal/proto/root_coord.proto               |   4 +
 internal/proxy/impl.go                        | 121 +++++++
 internal/proxy/meta_cache.go                  |  10 +-
 internal/proxy/rootcoord_mock_test.go         |  16 +
 internal/proxy/util.go                        |   8 -
 internal/proxy/util_test.go                   |   4 -
 internal/rootcoord/meta_table.go              | 191 +++++++++++
 internal/rootcoord/meta_table_test.go         |  41 +++
 internal/rootcoord/mock_test.go               |  40 +++
 internal/rootcoord/mocks/meta_table.go        | 311 +++++++++++++++++
 internal/rootcoord/root_coord.go              | 318 ++++++++++++++++--
 internal/rootcoord/root_coord_test.go         |  39 +++
 internal/util/mock/grpc_rootcoord_client.go   |  16 +
 pkg/go.mod                                    |   2 +-
 pkg/go.sum                                    |   4 +-
 pkg/metrics/rootcoord_metrics.go              |   9 +
 pkg/util/constant.go                          |  21 ++
 pkg/util/funcutil/policy.go                   |  11 +
 .../integration/rbac/privilege_group_test.go  | 248 +++++++++++---
 tests/integration/rbac/rbac_backup_test.go    | 179 ++++++----
 43 files changed, 2914 insertions(+), 211 deletions(-)

diff --git a/go.mod b/go.mod
index 1f209810b967a..d2f274bcf773f 100644
--- a/go.mod
+++ b/go.mod
@@ -26,7 +26,7 @@ require (
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
 	github.com/klauspost/compress v1.17.9
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
-	github.com/milvus-io/milvus-proto/go-api/v2 v2.4.16-0.20241106083218-955997f1a757
+	github.com/milvus-io/milvus-proto/go-api/v2 v2.4.16-0.20241110064419-549e4694a7e7
 	github.com/minio/minio-go/v7 v7.0.73
 	github.com/pingcap/log v1.1.1-0.20221015072633-39906604fb81
 	github.com/prometheus/client_golang v1.14.0
diff --git a/go.sum b/go.sum
index 2d0b44e3dad02..06f4622fe0917 100644
--- a/go.sum
+++ b/go.sum
@@ -608,8 +608,8 @@ github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119 h1:9VXijWu
 github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119/go.mod h1:DvXTE/K/RtHehxU8/GtDs4vFtfw64jJ3PaCnFri8CRg=
 github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b h1:TfeY0NxYxZzUfIfYe5qYDBzt4ZYRqzUjTR6CvUzjat8=
 github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b/go.mod h1:iwW+9cWfIzzDseEBCCeDSN5SD16Tidvy8cwQ7ZY8Qj4=
-github.com/milvus-io/milvus-proto/go-api/v2 v2.4.16-0.20241106083218-955997f1a757 h1:t7B2lyq//BG8S+azUNEfohYxRtU5V9NAy8z0G+QAPo4=
-github.com/milvus-io/milvus-proto/go-api/v2 v2.4.16-0.20241106083218-955997f1a757/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs=
+github.com/milvus-io/milvus-proto/go-api/v2 v2.4.16-0.20241110064419-549e4694a7e7 h1:gq5xxDS2EIYVk3ujO+sQgDWrhTTpsmV+r6Gm7dfFrt8=
+github.com/milvus-io/milvus-proto/go-api/v2 v2.4.16-0.20241110064419-549e4694a7e7/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs=
 github.com/milvus-io/milvus-storage/go v0.0.0-20231227072638-ebd0b8e56d70 h1:Z+sp64fmAOxAG7mU0dfVOXvAXlwRB0c8a96rIM5HevI=
 github.com/milvus-io/milvus-storage/go v0.0.0-20231227072638-ebd0b8e56d70/go.mod h1:GPETMcTZq1gLY1WA6Na5kiNAKnq8SEMMiVKUZrM3sho=
 github.com/milvus-io/pulsar-client-go v0.6.10 h1:eqpJjU+/QX0iIhEo3nhOqMNXL+TyInAs1IAHZCrCM/A=
diff --git a/internal/datacoord/mock_test.go b/internal/datacoord/mock_test.go
index aa0aee9c27e94..5012e309fca51 100644
--- a/internal/datacoord/mock_test.go
+++ b/internal/datacoord/mock_test.go
@@ -724,6 +724,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 55b37063aa68a..4d4a3f8fcb2cb 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 (
@@ -116,11 +119,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 5deb4bd0f9408..24092e75d5ca9 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 &PrivilegeGroupReq{} }, wrapperTraceLog(h.createPrivilegeGroup))))
+	router.POST(PrivilegeGroupCategory+DropAction, timeoutMiddleware(wrapperPost(func() any { return &PrivilegeGroupReq{} }, wrapperTraceLog(h.dropPrivilegeGroup))))
+	router.POST(PrivilegeGroupCategory+ListAction, timeoutMiddleware(wrapperPost(func() any { return &PrivilegeGroupReq{} }, wrapperTraceLog(h.listPrivilegeGroups))))
+	router.POST(PrivilegeGroupCategory+AddPrivilegesToGroupAction, timeoutMiddleware(wrapperPost(func() any { return &PrivilegeGroupReq{} }, wrapperTraceLog(h.addPrivilegesToGroup))))
+	router.POST(PrivilegeGroupCategory+RemovePrivilegesFromGroupAction, timeoutMiddleware(wrapperPost(func() any { return &PrivilegeGroupReq{} }, 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)))))
 
@@ -1711,6 +1718,85 @@ 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: lo.Map(httpReq.Privileges, func(p string, _ int) *milvuspb.PrivilegeEntity {
+			return &milvuspb.PrivilegeEntity{Name: p}
+		}),
+		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/handler_v2_test.go b/internal/distributed/proxy/httpserver/handler_v2_test.go
index aca560b902598..96aaa4289f9ed 100644
--- a/internal/distributed/proxy/httpserver/handler_v2_test.go
+++ b/internal/distributed/proxy/httpserver/handler_v2_test.go
@@ -897,6 +897,10 @@ func TestMethodGet(t *testing.T) {
 		Status: &StatusSuccess,
 		Alias:  DefaultAliasName,
 	}, nil).Once()
+	mp.EXPECT().ListPrivilegeGroups(mock.Anything, mock.Anything).Return(&milvuspb.ListPrivilegeGroupsResponse{
+		Status:          &StatusSuccess,
+		PrivilegeGroups: []*milvuspb.PrivilegeGroupInfo{{GroupName: "group1", Privileges: []*milvuspb.PrivilegeEntity{{Name: "*"}}}},
+	}, nil).Once()
 
 	testEngine := initHTTPServerV2(mp, false)
 	queryTestCases := []rawTestCase{}
@@ -987,6 +991,9 @@ func TestMethodGet(t *testing.T) {
 	queryTestCases = append(queryTestCases, rawTestCase{
 		path: versionalV2(AliasCategory, DescribeAction),
 	})
+	queryTestCases = append(queryTestCases, rawTestCase{
+		path: versionalV2(PrivilegeGroupCategory, ListAction),
+	})
 
 	for _, testcase := range queryTestCases {
 		t.Run(testcase.path, func(t *testing.T) {
@@ -996,7 +1003,8 @@ func TestMethodGet(t *testing.T) {
 				`"indexName": "` + DefaultIndexName + `",` +
 				`"userName": "` + util.UserRoot + `",` +
 				`"roleName": "` + util.RoleAdmin + `",` +
-				`"aliasName": "` + DefaultAliasName + `"` +
+				`"aliasName": "` + DefaultAliasName + `",` +
+				`"privilegeGroupName": "pg"` +
 				`}`))
 			req := httptest.NewRequest(http.MethodPost, testcase.path, bodyReader)
 			w := httptest.NewRecorder()
@@ -1037,6 +1045,7 @@ func TestMethodDelete(t *testing.T) {
 	mp.EXPECT().DropRole(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once()
 	mp.EXPECT().DropIndex(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once()
 	mp.EXPECT().DropAlias(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once()
+	mp.EXPECT().DropPrivilegeGroup(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once()
 	testEngine := initHTTPServerV2(mp, false)
 	queryTestCases := []rawTestCase{}
 	queryTestCases = append(queryTestCases, rawTestCase{
@@ -1057,10 +1066,13 @@ func TestMethodDelete(t *testing.T) {
 	queryTestCases = append(queryTestCases, rawTestCase{
 		path: versionalV2(AliasCategory, DropAction),
 	})
+	queryTestCases = append(queryTestCases, rawTestCase{
+		path: versionalV2(PrivilegeGroupCategory, DropAction),
+	})
 	for _, testcase := range queryTestCases {
 		t.Run(testcase.path, func(t *testing.T) {
 			bodyReader := bytes.NewReader([]byte(`{"collectionName": "` + DefaultCollectionName + `", "partitionName": "` + DefaultPartitionName +
-				`", "userName": "` + util.UserRoot + `", "roleName": "` + util.RoleAdmin + `", "indexName": "` + DefaultIndexName + `", "aliasName": "` + DefaultAliasName + `"}`))
+				`", "userName": "` + util.UserRoot + `", "roleName": "` + util.RoleAdmin + `", "indexName": "` + DefaultIndexName + `", "aliasName": "` + DefaultAliasName + `", "privilegeGroupName": "pg"}`))
 			req := httptest.NewRequest(http.MethodPost, testcase.path, bodyReader)
 			w := httptest.NewRecorder()
 			testEngine.ServeHTTP(w, req)
@@ -1099,6 +1111,8 @@ func TestMethodPost(t *testing.T) {
 	mp.EXPECT().CreateIndex(mock.Anything, mock.Anything).Return(commonErrorStatus, nil).Once()
 	mp.EXPECT().CreateAlias(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once()
 	mp.EXPECT().AlterAlias(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once()
+	mp.EXPECT().CreatePrivilegeGroup(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once()
+	mp.EXPECT().OperatePrivilegeGroup(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Twice()
 	mp.EXPECT().ImportV2(mock.Anything, mock.Anything).Return(&internalpb.ImportResponse{
 		Status: commonSuccessStatus, JobID: "1234567890",
 	}, nil).Once()
@@ -1191,6 +1205,15 @@ func TestMethodPost(t *testing.T) {
 	queryTestCases = append(queryTestCases, rawTestCase{
 		path: versionalV2(ImportJobCategory, DescribeAction),
 	})
+	queryTestCases = append(queryTestCases, rawTestCase{
+		path: versionalV2(PrivilegeGroupCategory, CreateAction),
+	})
+	queryTestCases = append(queryTestCases, rawTestCase{
+		path: versionalV2(PrivilegeGroupCategory, AddPrivilegesToGroupAction),
+	})
+	queryTestCases = append(queryTestCases, rawTestCase{
+		path: versionalV2(PrivilegeGroupCategory, RemovePrivilegesFromGroupAction),
+	})
 
 	for _, testcase := range queryTestCases {
 		t.Run(testcase.path, func(t *testing.T) {
@@ -1201,6 +1224,7 @@ func TestMethodPost(t *testing.T) {
 				`"indexParams": [{"indexName": "` + DefaultIndexName + `", "fieldName": "book_intro", "metricType": "L2", "params": {"nlist": 30, "index_type": "IVF_FLAT"}}],` +
 				`"userName": "` + util.UserRoot + `", "password": "Milvus", "newPassword": "milvus", "roleName": "` + util.RoleAdmin + `",` +
 				`"roleName": "` + util.RoleAdmin + `", "objectType": "Global", "objectName": "*", "privilege": "*",` +
+				`"privilegeGroupName": "pg", "privileges": ["create", "drop"],` +
 				`"aliasName": "` + DefaultAliasName + `",` +
 				`"jobId": "1234567890",` +
 				`"files": [["book.json"]]` +
diff --git a/internal/distributed/proxy/httpserver/request_v2.go b/internal/distributed/proxy/httpserver/request_v2.go
index e7f1364a70ae9..fc7d82dc1fbd4 100644
--- a/internal/distributed/proxy/httpserver/request_v2.go
+++ b/internal/distributed/proxy/httpserver/request_v2.go
@@ -258,6 +258,11 @@ func (req *RoleReq) GetRoleName() string {
 	return req.RoleName
 }
 
+type PrivilegeGroupReq struct {
+	PrivilegeGroupName string   `json:"privilegeGroupName" binding:"required"`
+	Privileges         []string `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 9b6a070c65d28..b941663cecc61 100644
--- a/internal/distributed/proxy/service.go
+++ b/internal/distributed/proxy/service.go
@@ -1060,6 +1060,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 fbf8d2657f16e..6de68c45f7d66 100644
--- a/internal/distributed/rootcoord/client/client.go
+++ b/internal/distributed/rootcoord/client/client.go
@@ -694,3 +694,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 07c092e687439..5661298327fe6 100644
--- a/internal/distributed/rootcoord/client/client_test.go
+++ b/internal/distributed/rootcoord/client/client_test.go
@@ -236,10 +236,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 8210cf3085727..4f245d1b07654 100644
--- a/internal/distributed/rootcoord/service.go
+++ b/internal/distributed/rootcoord/service.go
@@ -545,3 +545,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 b40ee8d5e7e79..2d23104912ee5 100644
--- a/internal/metastore/catalog.go
+++ b/internal/metastore/catalog.go
@@ -85,6 +85,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
+	SavePrivilegeGroup(ctx context.Context, data *milvuspb.PrivilegeGroupInfo) 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 28705ff8964db..7ed617d00a9da 100644
--- a/internal/metastore/kv/rootcoord/kv_catalog.go
+++ b/internal/metastore/kv/rootcoord/kv_catalog.go
@@ -1295,10 +1295,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
 }
 
@@ -1307,6 +1313,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))
@@ -1333,6 +1340,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))
+				}
+			}
 		}
 	}()
 
@@ -1355,9 +1370,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.SavePrivilegeGroup(ctx, group)
+		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
@@ -1402,6 +1450,72 @@ 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) SavePrivilegeGroup(ctx context.Context, data *milvuspb.PrivilegeGroupInfo) error {
+	k := BuildPrivilegeGroupkey(data.GroupName)
+	groupInfo := &milvuspb.PrivilegeGroupInfo{
+		GroupName:  data.GroupName,
+		Privileges: lo.Uniq(data.Privileges),
+	}
+	v, err := proto.Marshal(groupInfo)
+	if err != nil {
+		log.Error("failed to marshal privilege group info", 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 71817ae1098b7..f65f522dd6904 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"
@@ -2595,6 +2596,11 @@ func TestRBAC_Backup(t *testing.T) {
 	})
 	c.AlterUserRole(ctx, util.DefaultTenant, &milvuspb.UserEntity{Name: "user1"}, &milvuspb.RoleEntity{Name: "role1"}, milvuspb.OperateUserRoleType_AddUserToRole)
 
+	c.SavePrivilegeGroup(ctx, &milvuspb.PrivilegeGroupInfo{
+		GroupName:  "custom_group",
+		Privileges: []*milvuspb.PrivilegeEntity{{Name: "CreateCollection"}},
+	})
+
 	// test backup success
 	backup, err := c.BackupRBAC(ctx, util.DefaultTenant)
 	assert.NoError(t, err)
@@ -2605,6 +2611,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) {
@@ -2650,10 +2659,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)
@@ -2679,6 +2695,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{
@@ -2715,10 +2738,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
@@ -2740,6 +2770,145 @@ 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 SavePrivilegeGroup", func(t *testing.T) {
+		var (
+			kvmock = mocks.NewTxnKV(t)
+			c      = &Catalog{Txn: kvmock}
+		)
+
+		kvmock.EXPECT().Save(key1, mock.Anything).Return(nil)
+		kvmock.EXPECT().Save(key2, mock.Anything).Return(nil)
+
+		tests := []struct {
+			description string
+			isValid     bool
+			group       *milvuspb.PrivilegeGroupInfo
+		}{
+			{"valid group with existing key", true, &milvuspb.PrivilegeGroupInfo{GroupName: group1, Privileges: []*milvuspb.PrivilegeEntity{{Name: "priv10"}, {Name: "priv11"}}}},
+			{"valid group without existing key", true, &milvuspb.PrivilegeGroupInfo{GroupName: group2, Privileges: []*milvuspb.PrivilegeEntity{{Name: "priv10"}, {Name: "priv11"}}}},
+		}
+
+		for _, test := range tests {
+			t.Run(test.description, func(t *testing.T) {
+				err := c.SavePrivilegeGroup(ctx, test.group)
+				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 b250216048a37..9a912f76fc34b 100644
--- a/internal/metastore/kv/rootcoord/rootcoord_constant.go
+++ b/internal/metastore/kv/rootcoord/rootcoord_constant.go
@@ -49,6 +49,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 {
@@ -69,3 +72,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..646eb849ae756 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
+}
+
+// DropPrivilegeGroup 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
+}
+
+// SavePrivilegeGroup provides a mock function with given fields: ctx, groupName, privileges
+func (_m *RootCoordCatalog) SavePrivilegeGroup(ctx context.Context, data *milvuspb.PrivilegeGroupInfo) error {
+	ret := _m.Called(ctx, data)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.PrivilegeGroupInfo) error); ok {
+		r0 = rf(ctx, data)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// RootCoordCatalog_SavePrivilegeGroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SavePrivilegeGroup'
+type RootCoordCatalog_SavePrivilegeGroup_Call struct {
+	*mock.Call
+}
+
+// SavePrivilegeGroup is a helper method to define mock.On call
+//   - ctx context.Context
+//   - groupName string
+func (_e *RootCoordCatalog_Expecter) SavePrivilegeGroup(ctx interface{}, data interface{}) *RootCoordCatalog_SavePrivilegeGroup_Call {
+	return &RootCoordCatalog_SavePrivilegeGroup_Call{Call: _e.mock.On("SavePrivilegeGroup", ctx, data)}
+}
+
+func (_c *RootCoordCatalog_SavePrivilegeGroup_Call) Run(run func(ctx context.Context, data *milvuspb.PrivilegeGroupInfo)) *RootCoordCatalog_SavePrivilegeGroup_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*milvuspb.PrivilegeGroupInfo))
+	})
+	return _c
+}
+
+func (_c *RootCoordCatalog_SavePrivilegeGroup_Call) Return(_a0 error) *RootCoordCatalog_SavePrivilegeGroup_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *RootCoordCatalog_SavePrivilegeGroup_Call) RunAndReturn(run func(context.Context, *milvuspb.PrivilegeGroupInfo) error) *RootCoordCatalog_SavePrivilegeGroup_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 36555c92ef429..2fc1856e907be 100644
--- a/internal/mocks/mock_proxy.go
+++ b/internal/mocks/mock_proxy.go
@@ -859,6 +859,61 @@ func (_c *MockProxy_CreatePartition_Call) RunAndReturn(run func(context.Context,
 	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_CreatePrivilegeGroup_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
+}
+
 // CreateResourceGroup provides a mock function with given fields: _a0, _a1
 func (_m *MockProxy) CreateResourceGroup(_a0 context.Context, _a1 *milvuspb.CreateResourceGroupRequest) (*commonpb.Status, error) {
 	ret := _m.Called(_a0, _a1)
@@ -1684,6 +1739,61 @@ func (_c *MockProxy_DropPartition_Call) RunAndReturn(run func(context.Context, *
 	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_DropPrivilegeGroup_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
+}
+
 // DropResourceGroup provides a mock function with given fields: _a0, _a1
 func (_m *MockProxy) DropResourceGroup(_a0 context.Context, _a1 *milvuspb.DropResourceGroupRequest) (*commonpb.Status, error) {
 	ret := _m.Called(_a0, _a1)
@@ -4184,6 +4294,61 @@ func (_c *MockProxy_ListIndexedSegment_Call) RunAndReturn(run func(context.Conte
 	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_ListPrivilegeGroups_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
+}
+
 // ListResourceGroups provides a mock function with given fields: _a0, _a1
 func (_m *MockProxy) ListResourceGroups(_a0 context.Context, _a1 *milvuspb.ListResourceGroupsRequest) (*milvuspb.ListResourceGroupsResponse, error) {
 	ret := _m.Called(_a0, _a1)
@@ -4514,6 +4679,61 @@ func (_c *MockProxy_OperatePrivilege_Call) RunAndReturn(run func(context.Context
 	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_OperatePrivilegeGroup_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
+}
+
 // OperateUserRole provides a mock function with given fields: _a0, _a1
 func (_m *MockProxy) OperateUserRole(_a0 context.Context, _a1 *milvuspb.OperateUserRoleRequest) (*commonpb.Status, error) {
 	ret := _m.Called(_a0, _a1)
diff --git a/internal/mocks/mock_querynode.go b/internal/mocks/mock_querynode.go
index ccb27fa884e8d..abcf83fade8d0 100644
--- a/internal/mocks/mock_querynode.go
+++ b/internal/mocks/mock_querynode.go
@@ -89,10 +89,6 @@ func (_c *MockQueryNode_Delete_Call) RunAndReturn(run func(context.Context, *que
 func (_m *MockQueryNode) DeleteBatch(_a0 context.Context, _a1 *querypb.DeleteBatchRequest) (*querypb.DeleteBatchResponse, error) {
 	ret := _m.Called(_a0, _a1)
 
-	if len(ret) == 0 {
-		panic("no return value specified for DeleteBatch")
-	}
-
 	var r0 *querypb.DeleteBatchResponse
 	var r1 error
 	if rf, ok := ret.Get(0).(func(context.Context, *querypb.DeleteBatchRequest) (*querypb.DeleteBatchResponse, error)); ok {
diff --git a/internal/mocks/mock_querynode_client.go b/internal/mocks/mock_querynode_client.go
index 10e179518285a..e7777eb6ab7c6 100644
--- a/internal/mocks/mock_querynode_client.go
+++ b/internal/mocks/mock_querynode_client.go
@@ -153,10 +153,6 @@ func (_m *MockQueryNodeClient) DeleteBatch(ctx context.Context, in *querypb.Dele
 	_ca = append(_ca, _va...)
 	ret := _m.Called(_ca...)
 
-	if len(ret) == 0 {
-		panic("no return value specified for DeleteBatch")
-	}
-
 	var r0 *querypb.DeleteBatchResponse
 	var r1 error
 	if rf, ok := ret.Get(0).(func(context.Context, *querypb.DeleteBatchRequest, ...grpc.CallOption) (*querypb.DeleteBatchResponse, error)); ok {
diff --git a/internal/mocks/mock_rootcoord.go b/internal/mocks/mock_rootcoord.go
index 00c997d90e9e0..877174804472f 100644
--- a/internal/mocks/mock_rootcoord.go
+++ b/internal/mocks/mock_rootcoord.go
@@ -696,6 +696,61 @@ func (_c *RootCoord_CreatePartition_Call) RunAndReturn(run func(context.Context,
 	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
+}
+
 // CreateRole provides a mock function with given fields: _a0, _a1
 func (_m *RootCoord) CreateRole(_a0 context.Context, _a1 *milvuspb.CreateRoleRequest) (*commonpb.Status, error) {
 	ret := _m.Called(_a0, _a1)
@@ -1246,6 +1301,61 @@ func (_c *RootCoord_DropPartition_Call) RunAndReturn(run func(context.Context, *
 	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
+}
+
 // DropRole provides a mock function with given fields: _a0, _a1
 func (_m *RootCoord) DropRole(_a0 context.Context, _a1 *milvuspb.DropRoleRequest) (*commonpb.Status, error) {
 	ret := _m.Called(_a0, _a1)
@@ -2002,6 +2112,61 @@ func (_c *RootCoord_ListPolicy_Call) RunAndReturn(run func(context.Context, *int
 	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
+}
+
 // OperatePrivilege provides a mock function with given fields: _a0, _a1
 func (_m *RootCoord) OperatePrivilege(_a0 context.Context, _a1 *milvuspb.OperatePrivilegeRequest) (*commonpb.Status, error) {
 	ret := _m.Called(_a0, _a1)
@@ -2057,6 +2222,61 @@ func (_c *RootCoord_OperatePrivilege_Call) RunAndReturn(run func(context.Context
 	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
+}
+
 // OperateUserRole provides a mock function with given fields: _a0, _a1
 func (_m *RootCoord) OperateUserRole(_a0 context.Context, _a1 *milvuspb.OperateUserRoleRequest) (*commonpb.Status, error) {
 	ret := _m.Called(_a0, _a1)
diff --git a/internal/mocks/mock_rootcoord_client.go b/internal/mocks/mock_rootcoord_client.go
index dbb566bc3d4c3..3d3db95c6963d 100644
--- a/internal/mocks/mock_rootcoord_client.go
+++ b/internal/mocks/mock_rootcoord_client.go
@@ -914,6 +914,76 @@ func (_c *MockRootCoordClient_CreatePartition_Call) RunAndReturn(run func(contex
 	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...)
+
+	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
+}
+
 // CreateRole provides a mock function with given fields: ctx, in, opts
 func (_m *MockRootCoordClient) CreateRole(ctx context.Context, in *milvuspb.CreateRoleRequest, opts ...grpc.CallOption) (*commonpb.Status, error) {
 	_va := make([]interface{}, len(opts))
@@ -1614,6 +1684,76 @@ func (_c *MockRootCoordClient_DropPartition_Call) RunAndReturn(run func(context.
 	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...)
+
+	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
+}
+
 // DropRole provides a mock function with given fields: ctx, in, opts
 func (_m *MockRootCoordClient) DropRole(ctx context.Context, in *milvuspb.DropRoleRequest, opts ...grpc.CallOption) (*commonpb.Status, error) {
 	_va := make([]interface{}, len(opts))
@@ -2524,6 +2664,76 @@ func (_c *MockRootCoordClient_ListPolicy_Call) RunAndReturn(run func(context.Con
 	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...)
+
+	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
+}
+
 // OperatePrivilege provides a mock function with given fields: ctx, in, opts
 func (_m *MockRootCoordClient) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivilegeRequest, opts ...grpc.CallOption) (*commonpb.Status, error) {
 	_va := make([]interface{}, len(opts))
@@ -2594,6 +2804,76 @@ func (_c *MockRootCoordClient_OperatePrivilege_Call) RunAndReturn(run func(conte
 	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...)
+
+	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
+}
+
 // OperateUserRole provides a mock function with given fields: ctx, in, opts
 func (_m *MockRootCoordClient) OperateUserRole(ctx context.Context, in *milvuspb.OperateUserRoleRequest, opts ...grpc.CallOption) (*commonpb.Status, error) {
 	_va := make([]interface{}, len(opts))
diff --git a/internal/proto/internal.proto b/internal/proto/internal.proto
index 86bc546523841..7db4bd9a7c265 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 {
 }
@@ -255,6 +256,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 a1408f6e29656..bc1aadc6d6525 100644
--- a/internal/proto/root_coord.proto
+++ b/internal/proto/root_coord.proto
@@ -134,6 +134,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 b186ffe91c52c..008a7ce98d83c 100644
--- a/internal/proxy/impl.go
+++ b/internal/proxy/impl.go
@@ -6388,3 +6388,124 @@ func DeregisterSubLabel(subLabel string) {
 	rateCol.DeregisterSubLabel(internalpb.RateType_DQLQuery.String(), subLabel)
 	rateCol.DeregisterSubLabel(internalpb.RateType_DQLSearch.String(), subLabel)
 }
+
+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 870b6ce12df02..16b16c3ce10c0 100644
--- a/internal/proxy/meta_cache.go
+++ b/internal/proxy/meta_cache.go
@@ -1120,9 +1120,15 @@ func (m *MetaCache) RefreshPolicyInfo(op typeutil.CacheOp) (err error) {
 
 	switch op.OpType {
 	case typeutil.CacheGrantPrivilege:
-		m.privilegeInfos[op.OpKey] = struct{}{}
+		keys := funcutil.PrivilegesForPolicy(op.OpKey)
+		for _, key := range keys {
+			m.privilegeInfos[key] = struct{}{}
+		}
 	case typeutil.CacheRevokePrivilege:
-		delete(m.privilegeInfos, op.OpKey)
+		keys := funcutil.PrivilegesForPolicy(op.OpKey)
+		for _, key := range keys {
+			delete(m.privilegeInfos, key)
+		}
 	case typeutil.CacheAddUserToRole:
 		user, role, err := funcutil.DecodeUserRoleCache(op.OpKey)
 		if err != nil {
diff --git a/internal/proxy/rootcoord_mock_test.go b/internal/proxy/rootcoord_mock_test.go
index a7c8f96e24cad..1fb988cbf554c 100644
--- a/internal/proxy/rootcoord_mock_test.go
+++ b/internal/proxy/rootcoord_mock_test.go
@@ -1130,6 +1130,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 dc048daeb3fb5..dae3c88c56fe4 100644
--- a/internal/proxy/util.go
+++ b/internal/proxy/util.go
@@ -906,14 +906,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
diff --git a/internal/proxy/util_test.go b/internal/proxy/util_test.go
index f95a9299252c5..0aed56b3a4b09 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(" "))
diff --git a/internal/rootcoord/meta_table.go b/internal/rootcoord/meta_table.go
index f86eb82297c66..31364f431b055 100644
--- a/internal/rootcoord/meta_table.go
+++ b/internal/rootcoord/meta_table.go
@@ -22,6 +22,7 @@ import (
 	"sync"
 
 	"github.com/cockroachdb/errors"
+	"github.com/samber/lo"
 	"go.uber.org/zap"
 	"golang.org/x/exp/maps"
 
@@ -96,6 +97,12 @@ 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
+	GetPrivilegeGroupRoles(groupName string) ([]*milvuspb.RoleEntity, error)
 }
 
 type MetaTable struct {
@@ -1425,3 +1432,187 @@ 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)
+	}
+	data := &milvuspb.PrivilegeGroupInfo{
+		GroupName:  groupName,
+		Privileges: make([]*milvuspb.PrivilegeEntity, 0),
+	}
+	return mt.catalog.SavePrivilegeGroup(mt.ctx, data)
+}
+
+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 nil
+	}
+	// 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)
+	}
+	groups, err := mt.catalog.ListPrivilegeGroups(mt.ctx)
+	if err != nil {
+		return err
+	}
+	for _, p := range privileges {
+		if util.IsPrivilegeNameDefined(p.Name) {
+			continue
+		}
+		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}
+	})
+	data := &milvuspb.PrivilegeGroupInfo{
+		GroupName:  groupName,
+		Privileges: mergedPrivs,
+	}
+	return mt.catalog.SavePrivilegeGroup(mt.ctx, data)
+}
+
+func (mt *MetaTable) GetPrivilegeGroupRoles(groupName string) ([]*milvuspb.RoleEntity, error) {
+	if funcutil.IsEmptyString(groupName) {
+		return nil, fmt.Errorf("the privilege group name is empty")
+	}
+	mt.permissionLock.RLock()
+	defer mt.permissionLock.RUnlock()
+
+	// get all roles
+	roles, err := mt.catalog.ListRole(mt.ctx, util.DefaultTenant, nil, false)
+	if err != nil {
+		return nil, err
+	}
+	roleEntity := lo.Map(roles, func(entity *milvuspb.RoleResult, _ int) *milvuspb.RoleEntity {
+		return entity.GetRole()
+	})
+
+	rolesMap := make(map[*milvuspb.RoleEntity]struct{})
+	for _, role := range roleEntity {
+		grants, err := mt.catalog.ListGrant(mt.ctx, util.DefaultTenant, &milvuspb.GrantEntity{
+			Role:   role,
+			DbName: util.AnyWord,
+		})
+		if err != nil {
+			return nil, err
+		}
+		for _, grant := range grants {
+			if grant.Grantor.Privilege.Name == groupName {
+				rolesMap[role] = struct{}{}
+			}
+		}
+	}
+	return lo.Keys(rolesMap), nil
+}
diff --git a/internal/rootcoord/meta_table_test.go b/internal/rootcoord/meta_table_test.go
index 7bd666f7ca3c4..4192b655c03be 100644
--- a/internal/rootcoord/meta_table_test.go
+++ b/internal/rootcoord/meta_table_test.go
@@ -2071,3 +2071,44 @@ 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().SavePrivilegeGroup(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("")
+	assert.Error(t, err)
+	err = mt.CreatePrivilegeGroup("Insert")
+	assert.Error(t, err)
+	err = mt.CreatePrivilegeGroup("pg2")
+	assert.NoError(t, err)
+	err = mt.DropPrivilegeGroup("")
+	assert.Error(t, err)
+	err = mt.DropPrivilegeGroup("pg1")
+	assert.NoError(t, err)
+	err = mt.OperatePrivilegeGroup("", []*milvuspb.PrivilegeEntity{}, milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup)
+	assert.Error(t, err)
+	err = mt.OperatePrivilegeGroup("pg3", []*milvuspb.PrivilegeEntity{}, milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup)
+	assert.Error(t, err)
+	_, err = mt.GetPrivilegeGroupRoles("")
+	assert.Error(t, err)
+	_, err = mt.ListPrivilegeGroups()
+	assert.NoError(t, err)
+}
diff --git a/internal/rootcoord/mock_test.go b/internal/rootcoord/mock_test.go
index e0eb4db1d8711..a9cf579fe5e85 100644
--- a/internal/rootcoord/mock_test.go
+++ b/internal/rootcoord/mock_test.go
@@ -96,6 +96,11 @@ 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
+	GetPrivilegeGroupRolesFunc       func(groupName string) ([]*milvuspb.RoleEntity, error)
 }
 
 func (m mockMetaTable) GetDatabaseByName(ctx context.Context, dbName string, ts Timestamp) (*model.Database, error) {
@@ -250,6 +255,26 @@ 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 (m mockMetaTable) GetPrivilegeGroupRoles(groupName string) ([]*milvuspb.RoleEntity, error) {
+	return m.GetPrivilegeGroupRolesFunc(groupName)
+}
+
 func newMockMetaTable() *mockMetaTable {
 	return &mockMetaTable{}
 }
@@ -524,6 +549,21 @@ 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")
+	}
+	meta.GetPrivilegeGroupRolesFunc = func(groupName string) ([]*milvuspb.RoleEntity, error) {
+		return nil, errors.New("error mock GetPrivilegeGroupRoles")
+	}
 	return withMeta(meta)
 }
 
diff --git a/internal/rootcoord/mocks/meta_table.go b/internal/rootcoord/mocks/meta_table.go
index 9dc0aee9c9e08..28f679f055991 100644
--- a/internal/rootcoord/mocks/meta_table.go
+++ b/internal/rootcoord/mocks/meta_table.go
@@ -570,6 +570,52 @@ func (_c *IMetaTable_CreateDatabase_Call) RunAndReturn(run func(context.Context,
 	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
+}
+
 // CreateRole provides a mock function with given fields: tenant, entity
 func (_m *IMetaTable) CreateRole(tenant string, entity *milvuspb.RoleEntity) error {
 	ret := _m.Called(tenant, entity)
@@ -842,6 +888,52 @@ func (_c *IMetaTable_DropGrant_Call) RunAndReturn(run func(string, *milvuspb.Rol
 	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
+//   - 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(string) error) *IMetaTable_DropPrivilegeGroup_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // DropRole provides a mock function with given fields: tenant, roleName
 func (_m *IMetaTable) DropRole(tenant string, roleName string) error {
 	ret := _m.Called(tenant, roleName)
@@ -1265,6 +1357,64 @@ func (_c *IMetaTable_GetDatabaseByName_Call) RunAndReturn(run func(context.Conte
 	return _c
 }
 
+// GetPrivilegeGroupRoles provides a mock function with given fields: groupName
+func (_m *IMetaTable) GetPrivilegeGroupRoles(groupName string) ([]*milvuspb.RoleEntity, error) {
+	ret := _m.Called(groupName)
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetPrivilegeGroupRoles")
+	}
+
+	var r0 []*milvuspb.RoleEntity
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string) ([]*milvuspb.RoleEntity, error)); ok {
+		return rf(groupName)
+	}
+	if rf, ok := ret.Get(0).(func(string) []*milvuspb.RoleEntity); ok {
+		r0 = rf(groupName)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*milvuspb.RoleEntity)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(string) error); ok {
+		r1 = rf(groupName)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// IMetaTable_GetPrivilegeGroupRoles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPrivilegeGroupRoles'
+type IMetaTable_GetPrivilegeGroupRoles_Call struct {
+	*mock.Call
+}
+
+// GetPrivilegeGroupRoles is a helper method to define mock.On call
+//   - groupName string
+func (_e *IMetaTable_Expecter) GetPrivilegeGroupRoles(groupName interface{}) *IMetaTable_GetPrivilegeGroupRoles_Call {
+	return &IMetaTable_GetPrivilegeGroupRoles_Call{Call: _e.mock.On("GetPrivilegeGroupRoles", groupName)}
+}
+
+func (_c *IMetaTable_GetPrivilegeGroupRoles_Call) Run(run func(groupName string)) *IMetaTable_GetPrivilegeGroupRoles_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *IMetaTable_GetPrivilegeGroupRoles_Call) Return(_a0 []*milvuspb.RoleEntity, _a1 error) *IMetaTable_GetPrivilegeGroupRoles_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *IMetaTable_GetPrivilegeGroupRoles_Call) RunAndReturn(run func(string) ([]*milvuspb.RoleEntity, error)) *IMetaTable_GetPrivilegeGroupRoles_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // IsAlias provides a mock function with given fields: db, name
 func (_m *IMetaTable) IsAlias(db string, name string) bool {
 	ret := _m.Called(db, name)
@@ -1308,6 +1458,62 @@ func (_c *IMetaTable_IsAlias_Call) RunAndReturn(run func(string, string) bool) *
 	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 {
+		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
+}
+
 // ListAliases provides a mock function with given fields: ctx, dbName, collectionName, ts
 func (_m *IMetaTable) ListAliases(ctx context.Context, dbName string, collectionName string, ts uint64) ([]string, error) {
 	ret := _m.Called(ctx, dbName, collectionName, ts)
@@ -1715,6 +1921,63 @@ func (_c *IMetaTable_ListPolicy_Call) RunAndReturn(run func(string) ([]string, e
 	return _c
 }
 
+// ListPrivilegeGroups provides a mock function with given fields:
+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
+}
+
 // ListUserRole provides a mock function with given fields: tenant
 func (_m *IMetaTable) ListUserRole(tenant string) ([]string, error) {
 	ret := _m.Called(tenant)
@@ -1813,6 +2076,54 @@ func (_c *IMetaTable_OperatePrivilege_Call) RunAndReturn(run func(string, *milvu
 	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
+}
+
 // OperateUserRole provides a mock function with given fields: tenant, userEntity, roleEntity, operateType
 func (_m *IMetaTable) OperateUserRole(tenant string, userEntity *milvuspb.UserEntity, roleEntity *milvuspb.RoleEntity, operateType milvuspb.OperateUserRoleType) error {
 	ret := _m.Called(tenant, userEntity, roleEntity, operateType)
diff --git a/internal/rootcoord/root_coord.go b/internal/rootcoord/root_coord.go
index 374cece06e9e0..27f66f13f56d6 100644
--- a/internal/rootcoord/root_coord.go
+++ b/internal/rootcoord/root_coord.go
@@ -612,7 +612,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},
@@ -2491,11 +2495,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))
@@ -2507,17 +2508,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)
 }
@@ -2562,11 +2571,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
 	}
@@ -2614,6 +2630,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
@@ -2706,14 +2738,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
 }
 
@@ -2914,3 +2955,236 @@ 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
+	}
+
+	if err := c.meta.DropPrivilegeGroup(in.GroupName); err != nil {
+		ctxLog.Warn("fail to drop 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.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", func(ctx context.Context) ([]nestedStep, error) {
+		groups, err := c.meta.ListPrivilegeGroups()
+		if err != nil && !common.IsIgnorableError(err) {
+			log.Warn("fail to list privilege groups", zap.Error(err))
+			return nil, err
+		}
+		currGroups := lo.SliceToMap(groups, func(group *milvuspb.PrivilegeGroupInfo) (string, []*milvuspb.PrivilegeEntity) {
+			return group.GroupName, group.Privileges
+		})
+
+		// get roles granted to the group
+		roles, err := c.meta.GetPrivilegeGroupRoles(in.GroupName)
+		if err != nil {
+			return nil, err
+		}
+
+		newGroups := make(map[string][]*milvuspb.PrivilegeEntity)
+		for k, v := range currGroups {
+			if k != in.GroupName {
+				newGroups[k] = v
+				continue
+			}
+			switch in.Type {
+			case milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup:
+				newPrivs := lo.Union(v, in.Privileges)
+				newGroups[k] = lo.UniqBy(newPrivs, func(p *milvuspb.PrivilegeEntity) string {
+					return p.Name
+				})
+			case milvuspb.OperatePrivilegeGroupType_RemovePrivilegesFromGroup:
+				newPrivs, _ := lo.Difference(v, in.Privileges)
+				newGroups[k] = newPrivs
+			default:
+				return nil, errors.New("invalid operate type")
+			}
+		}
+
+		rolesToRevoke := []*milvuspb.GrantEntity{}
+		rolesToGrant := []*milvuspb.GrantEntity{}
+		compareGrants := func(a, b *milvuspb.GrantEntity) bool {
+			return a.Role.GetName() == b.Role.GetName() &&
+				a.Object.GetName() == b.Object.GetName() &&
+				a.ObjectName == b.ObjectName &&
+				a.Grantor.GetUser().GetName() == b.Grantor.GetUser().GetName() &&
+				a.Grantor.GetPrivilege().GetName() == b.Grantor.GetPrivilege().GetName() &&
+				a.DbName == b.DbName
+		}
+		for _, role := range roles {
+			grants, err := c.meta.SelectGrant(util.DefaultTenant, &milvuspb.GrantEntity{
+				Role:   role,
+				DbName: util.AnyWord,
+			})
+			if err != nil {
+				return nil, err
+			}
+			currGrants := c.expandPrivilegeGroups(grants, currGroups)
+			newGrants := c.expandPrivilegeGroups(grants, newGroups)
+
+			toRevoke := lo.Filter(currGrants, func(item *milvuspb.GrantEntity, _ int) bool {
+				return !lo.ContainsBy(newGrants, func(newItem *milvuspb.GrantEntity) bool {
+					return compareGrants(item, newItem)
+				})
+			})
+
+			toGrant := lo.Filter(newGrants, func(item *milvuspb.GrantEntity, _ int) bool {
+				return !lo.ContainsBy(currGrants, func(currItem *milvuspb.GrantEntity) bool {
+					return compareGrants(item, currItem)
+				})
+			})
+
+			rolesToRevoke = append(rolesToRevoke, toRevoke...)
+			rolesToGrant = append(rolesToGrant, toGrant...)
+		}
+
+		if len(rolesToRevoke) > 0 {
+			opType := int32(typeutil.CacheRevokePrivilege)
+			if err := c.proxyClientManager.RefreshPolicyInfoCache(ctx, &proxypb.RefreshPolicyInfoCacheRequest{
+				OpType: opType,
+				OpKey:  funcutil.PolicyForPrivileges(rolesToRevoke),
+			}); err != nil {
+				log.Warn("fail to refresh policy info cache for revoke privileges in operate privilege group", zap.Any("in", in), zap.Error(err))
+				return nil, err
+			}
+		}
+
+		if len(rolesToGrant) > 0 {
+			opType := int32(typeutil.CacheGrantPrivilege)
+			if err := c.proxyClientManager.RefreshPolicyInfoCache(ctx, &proxypb.RefreshPolicyInfoCacheRequest{
+				OpType: opType,
+				OpKey:  funcutil.PolicyForPrivileges(rolesToGrant),
+			}); err != nil {
+				log.Warn("fail to refresh policy info cache for grants privilege in operate privilege group", zap.Any("in", in), zap.Error(err))
+				return nil, err
+			}
+		}
+		return nil, nil
+	}))
+
+	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
+	}))
+
+	err := redoTask.Execute(ctx)
+	if err != nil {
+		errMsg := "fail to execute task when operate privilege group"
+		ctxLog.Warn(errMsg, zap.Error(err))
+		status := merr.StatusWithErrorCode(errors.New(errMsg), commonpb.ErrorCode_OperatePrivilegeGroupFailure)
+		return status, 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
+}
+
+func (c *Core) expandPrivilegeGroups(grants []*milvuspb.GrantEntity, groups map[string][]*milvuspb.PrivilegeEntity) []*milvuspb.GrantEntity {
+	newGrants := []*milvuspb.GrantEntity{}
+	for _, grant := range grants {
+		if groups[grant.Grantor.Privilege.Name] == nil {
+			newGrants = append(newGrants, grant)
+		} else {
+			for _, priv := range groups[grant.Grantor.Privilege.Name] {
+				newGrants = append(newGrants, &milvuspb.GrantEntity{
+					Role:       grant.Role,
+					Object:     grant.Object,
+					ObjectName: grant.ObjectName,
+					Grantor: &milvuspb.GrantorEntity{
+						User:      grant.Grantor.User,
+						Privilege: priv,
+					},
+					DbName: grant.DbName,
+				})
+			}
+		}
+	}
+	// uniq by role + object + object name + grantor user + privilege name + db name
+	return lo.UniqBy(newGrants, func(g *milvuspb.GrantEntity) string {
+		return fmt.Sprintf("%s-%s-%s-%s-%s-%s", g.Role, g.Object, g.ObjectName, g.Grantor.User, g.Grantor.Privilege.Name, g.DbName)
+	})
+}
diff --git a/internal/rootcoord/root_coord_test.go b/internal/rootcoord/root_coord_test.go
index 689bfee948a5a..bdf93e15a9a41 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"},
@@ -1784,6 +1787,39 @@ func TestRootCoord_RBACError(t *testing.T) {
 		}
 	})
 
+	t.Run("operate privilege group failed", func(t *testing.T) {
+		mockMeta := c.meta.(*mockMetaTable)
+		mockMeta.ListPrivilegeGroupsFunc = func() ([]*milvuspb.PrivilegeGroupInfo, error) {
+			return nil, errors.New("mock error")
+		}
+		mockMeta.CreatePrivilegeGroupFunc = func(groupName string) error {
+			return errors.New("mock error")
+		}
+		mockMeta.GetPrivilegeGroupRolesFunc = func(groupName string) ([]*milvuspb.RoleEntity, error) {
+			return nil, errors.New("mock error")
+		}
+		{
+			resp, err := c.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{})
+			assert.NoError(t, err)
+			assert.NotEqual(t, commonpb.ErrorCode_Success, resp.GetErrorCode())
+		}
+		{
+			resp, err := c.ListPrivilegeGroups(ctx, &milvuspb.ListPrivilegeGroupsRequest{})
+			assert.NoError(t, err)
+			assert.NotEqual(t, commonpb.ErrorCode_Success, resp.GetStatus().GetErrorCode())
+		}
+		{
+			resp, err := c.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{})
+			assert.NoError(t, err)
+			assert.NotEqual(t, commonpb.ErrorCode_Success, resp.GetErrorCode())
+		}
+		{
+			resp, err := c.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{})
+			assert.NoError(t, err)
+			assert.NotEqual(t, commonpb.ErrorCode_Success, resp.GetErrorCode())
+		}
+	})
+
 	t.Run("select grant failed", func(t *testing.T) {
 		{
 			resp, err := c.SelectGrant(ctx, &milvuspb.SelectGrantRequest{})
@@ -1879,6 +1915,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 bc80d954ae250..0c5d431af4bd8 100644
--- a/internal/util/mock/grpc_rootcoord_client.go
+++ b/internal/util/mock/grpc_rootcoord_client.go
@@ -270,6 +270,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 b3fa9deb7ac49..17192fed58738 100644
--- a/pkg/go.mod
+++ b/pkg/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/expr-lang/expr v1.15.7
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
 	github.com/klauspost/compress v1.17.7
-	github.com/milvus-io/milvus-proto/go-api/v2 v2.4.15
+	github.com/milvus-io/milvus-proto/go-api/v2 v2.4.16-0.20241110064419-549e4694a7e7
 	github.com/nats-io/nats-server/v2 v2.10.12
 	github.com/nats-io/nats.go v1.34.1
 	github.com/panjf2000/ants/v2 v2.7.2
diff --git a/pkg/go.sum b/pkg/go.sum
index c41940317fa8b..3daf8119ee54c 100644
--- a/pkg/go.sum
+++ b/pkg/go.sum
@@ -503,8 +503,8 @@ github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119 h1:9VXijWu
 github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119/go.mod h1:DvXTE/K/RtHehxU8/GtDs4vFtfw64jJ3PaCnFri8CRg=
 github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b h1:TfeY0NxYxZzUfIfYe5qYDBzt4ZYRqzUjTR6CvUzjat8=
 github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b/go.mod h1:iwW+9cWfIzzDseEBCCeDSN5SD16Tidvy8cwQ7ZY8Qj4=
-github.com/milvus-io/milvus-proto/go-api/v2 v2.4.15 h1:1y+hkeGh7zaD5ZasWjfKNZYWdH8VlLKcjoeyFiSh/I8=
-github.com/milvus-io/milvus-proto/go-api/v2 v2.4.15/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs=
+github.com/milvus-io/milvus-proto/go-api/v2 v2.4.16-0.20241110064419-549e4694a7e7 h1:gq5xxDS2EIYVk3ujO+sQgDWrhTTpsmV+r6Gm7dfFrt8=
+github.com/milvus-io/milvus-proto/go-api/v2 v2.4.16-0.20241110064419-549e4694a7e7/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs=
 github.com/milvus-io/pulsar-client-go v0.6.10 h1:eqpJjU+/QX0iIhEo3nhOqMNXL+TyInAs1IAHZCrCM/A=
 github.com/milvus-io/pulsar-client-go v0.6.10/go.mod h1:lQqCkgwDF8YFYjKA+zOheTk1tev2B+bKj5j7+nm8M1w=
 github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
diff --git a/pkg/metrics/rootcoord_metrics.go b/pkg/metrics/rootcoord_metrics.go
index 582207c61b0e9..90ed0b6b7abe5 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/funcutil/policy.go b/pkg/util/funcutil/policy.go
index 384d3a436f1b8..bdf2650a51dab 100644
--- a/pkg/util/funcutil/policy.go
+++ b/pkg/util/funcutil/policy.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 
 	"github.com/cockroachdb/errors"
+	"github.com/samber/lo"
 	"go.uber.org/zap"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
@@ -114,6 +115,16 @@ func PolicyForPrivilege(roleName string, objectType string, objectName string, p
 	return fmt.Sprintf(`{"PType":"p","V0":"%s","V1":"%s","V2":"%s"}`, roleName, PolicyForResource(dbName, objectType, objectName), privilege)
 }
 
+func PolicyForPrivileges(grants []*milvuspb.GrantEntity) string {
+	return strings.Join(lo.Map(grants, func(r *milvuspb.GrantEntity, _ int) string {
+		return PolicyForPrivilege(r.Role.Name, r.Object.Name, r.ObjectName, r.Grantor.Privilege.Name, r.DbName)
+	}), "|")
+}
+
+func PrivilegesForPolicy(policy string) []string {
+	return strings.Split(policy, "|")
+}
+
 func PolicyForResource(dbName string, objectType string, objectName string) string {
 	return fmt.Sprintf("%s-%s", objectType, CombineObjectName(dbName, objectName))
 }
diff --git a/tests/integration/rbac/privilege_group_test.go b/tests/integration/rbac/privilege_group_test.go
index da89b603a472e..bea3af0514737 100644
--- a/tests/integration/rbac/privilege_group_test.go
+++ b/tests/integration/rbac/privilege_group_test.go
@@ -17,6 +17,7 @@ package rbac
 
 import (
 	"context"
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/suite"
@@ -41,94 +42,229 @@ 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.operatePrivilege(ctx, roleName, "ReadOnly", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
+	s.operatePrivilege(ctx, roleName, "ReadWrite", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
+	s.operatePrivilege(ctx, roleName, "Admin", commonpb.ObjectType_Global.String(), milvuspb.OperatePrivilegeType_Grant)
+
+	s.validateGrants(ctx, roleName, commonpb.ObjectType_Global.String(), 1)
+	s.validateGrants(ctx, roleName, commonpb.ObjectType_Collection.String(), 2)
+}
+
+/*
+create group1: query, search
+grant insert to role -> role: insert
+grant group1 to role -> role: insert, group1(query, search)
+create group2: query, delete
+grant group2 to role -> role: insert, group1(query, search), group2(query, delete)
+add query, load to group1 -> group1: query, search, load -> role: insert, group1(query, search, load), group2(query, delete)
+remove query from group1 -> group1: search, load -> role: insert, group1(search, load), group2(query, delete), role still have query privilege because of group2 granted.
+*/
+func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() {
+	ctx := GetContext(context.Background(), "root:123456")
+
+	// Helper function to operate on privilege groups
+	operatePrivilegeGroup := func(groupName string, operateType milvuspb.OperatePrivilegeGroupType, privileges []*milvuspb.PrivilegeEntity) {
+		resp, err := s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{
+			GroupName:  groupName,
+			Type:       operateType,
+			Privileges: privileges,
+		})
+		s.NoError(err)
+		s.True(merr.Ok(resp))
+	}
+
+	// Helper function to list privilege groups and return the target group and its privileges
+	validatePrivilegeGroup := func(groupName string, privileges int) []*milvuspb.PrivilegeEntity {
+		resp, err := s.Cluster.Proxy.ListPrivilegeGroups(ctx, &milvuspb.ListPrivilegeGroupsRequest{})
+		s.NoError(err)
+		for _, privGroup := range resp.PrivilegeGroups {
+			if privGroup.GroupName == groupName {
+				s.Equal(privileges, len(privGroup.Privileges))
+				return privGroup.Privileges
+			}
+		}
+		return nil
+	}
+
+	// create group1: query, search
+	createResp, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{
+		GroupName: "group1",
 	})
 	s.NoError(err)
-	s.True(merr.Ok(resp2))
+	s.True(merr.Ok(createResp))
+	validatePrivilegeGroup("group1", 0)
+	operatePrivilegeGroup("group1", milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, []*milvuspb.PrivilegeEntity{
+		{Name: "Query"},
+		{Name: "Search"},
+	})
+	validatePrivilegeGroup("group1", 2)
 
-	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"},
-			},
-		},
+	// grant insert to role -> role: insert
+	role := "role1"
+	createRoleResp, err := s.Cluster.Proxy.CreateRole(ctx, &milvuspb.CreateRoleRequest{
+		Entity: &milvuspb.RoleEntity{Name: role},
 	})
 	s.NoError(err)
-	s.True(merr.Ok(resp3))
+	s.True(merr.Ok(createRoleResp))
+	s.operatePrivilege(ctx, role, "Insert", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
+	s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 1)
 
-	resp4, 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,
-			Grantor: &milvuspb.GrantorEntity{
-				User:      &milvuspb.UserEntity{Name: util.UserRoot},
-				Privilege: &milvuspb.PrivilegeEntity{Name: "Admin"},
-			},
-		},
+	// grant group1 to role -> role: insert, group1(query, search)
+	s.operatePrivilege(ctx, role, "group1", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
+	s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 2)
+
+	// create group2: query, delete
+	createResp2, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{
+		GroupName: "group2",
 	})
 	s.NoError(err)
-	s.True(merr.Ok(resp4))
+	s.True(merr.Ok(createResp2))
+	validatePrivilegeGroup("group2", 0)
+	operatePrivilegeGroup("group2", milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, []*milvuspb.PrivilegeEntity{
+		{Name: "Query"},
+		{Name: "Delete"},
+	})
+	validatePrivilegeGroup("group2", 2)
+
+	// grant group2 to role -> role: insert, group1(query, search), group2(query, delete)
+	s.operatePrivilege(ctx, role, "group2", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
+	s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 3)
 
-	resp5, err := s.Cluster.Proxy.SelectGrant(ctx, &milvuspb.SelectGrantRequest{
+	// add query, load to group1 -> group1: query, search, load -> role: insert, group1(query, search, load), group2(query, delete)
+	operatePrivilegeGroup("group1", milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, []*milvuspb.PrivilegeEntity{
+		{Name: "Query"},
+		{Name: "Load"},
+	})
+	validatePrivilegeGroup("group1", 3)
+	s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 3)
+
+	// remove query from group1 -> group1: search, load -> role: insert, group1(search, load), group2(query, delete), role still have query privilege because of group2 granted.
+	operatePrivilegeGroup("group1", milvuspb.OperatePrivilegeGroupType_RemovePrivilegesFromGroup, []*milvuspb.PrivilegeEntity{
+		{Name: "Query"},
+	})
+	validatePrivilegeGroup("group1", 2)
+	s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 3)
+
+	// Drop the group during any role usage will cause error
+	dropResp, _ := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{
+		GroupName: "group1",
+	})
+	s.Error(merr.Error(dropResp))
+
+	// Revoke privilege group and privileges
+	s.operatePrivilege(ctx, role, "group1", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Revoke)
+	s.operatePrivilege(ctx, role, "group2", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Revoke)
+	s.operatePrivilege(ctx, role, "Insert", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Revoke)
+
+	// Drop the privilege group after revoking the privilege will succeed
+	dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{
+		GroupName: "group1",
+	})
+	s.NoError(err)
+	s.True(merr.Ok(dropResp))
+
+	dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{
+		GroupName: "group2",
+	})
+	s.NoError(err)
+	s.True(merr.Ok(dropResp))
+
+	// Validate the group was dropped
+	resp, err := s.Cluster.Proxy.ListPrivilegeGroups(ctx, &milvuspb.ListPrivilegeGroupsRequest{})
+	s.NoError(err)
+	s.Equal(0, len(resp.PrivilegeGroups))
+
+	// Drop the role
+	dropRoleResp, err := s.Cluster.Proxy.DropRole(ctx, &milvuspb.DropRoleRequest{
+		RoleName: role,
+	})
+	s.NoError(err)
+	s.True(merr.Ok(dropRoleResp))
+}
+
+func (s *PrivilegeGroupTestSuite) TestInvalidPrivilegeGroup() {
+	ctx := GetContext(context.Background(), "root:123456")
+
+	createResp, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{
+		GroupName: "",
+	})
+	s.NoError(err)
+	s.False(merr.Ok(createResp))
+
+	dropResp, err := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{
+		GroupName: "group1",
+	})
+	s.NoError(err)
+	s.True(merr.Ok(dropResp))
+
+	dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{
+		GroupName: "",
+	})
+	s.NoError(err)
+	s.False(merr.Ok(dropResp))
+
+	operateResp, err := s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{
+		GroupName: "",
+	})
+	s.NoError(err)
+	s.False(merr.Ok(operateResp))
+
+	operateResp, err = s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{
+		GroupName:  "group1",
+		Privileges: []*milvuspb.PrivilegeEntity{{Name: "123"}},
+	})
+	s.NoError(err)
+	s.False(merr.Ok(operateResp))
+}
+
+func (s *PrivilegeGroupTestSuite) operatePrivilege(ctx context.Context, role, privilege, objectType string, operateType milvuspb.OperatePrivilegeType) {
+	resp, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{
+		Type: operateType,
 		Entity: &milvuspb.GrantEntity{
-			Role:       &milvuspb.RoleEntity{Name: roleName},
-			Object:     &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()},
+			Role:       &milvuspb.RoleEntity{Name: role},
+			Object:     &milvuspb.ObjectEntity{Name: objectType},
 			ObjectName: util.AnyWord,
 			DbName:     util.AnyWord,
+			Grantor: &milvuspb.GrantorEntity{
+				User:      &milvuspb.UserEntity{Name: util.UserRoot},
+				Privilege: &milvuspb.PrivilegeEntity{Name: privilege},
+			},
 		},
 	})
 	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 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()},
+			Object:     &milvuspb.ObjectEntity{Name: objectType},
 			ObjectName: util.AnyWord,
 			DbName:     util.AnyWord,
 		},
 	})
+	fmt.Println("!!!validateGrants: ", resp)
 	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) {