enhance: [2.4] RBAC built in privilege groups and grant v2 (#37787)

cherry-pick from master: https://github.com/milvus-io/milvus/pull/37720,
https://github.com/milvus-io/milvus/pull/37785
issue: https://github.com/milvus-io/milvus/issues/37031

Signed-off-by: shaoting-huang <shaoting.huang@zilliz.com>
This commit is contained in:
sthuang 2024-11-25 11:24:54 +08:00 committed by GitHub
parent ff6e8e2f2b
commit d8f1af68e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1148 additions and 170 deletions

View File

@ -797,6 +797,30 @@ common:
# like the old password verification when updating the credential
superUsers:
defaultRootPassword: Milvus # default password for root user
rbac:
overrideBuiltInPrivilgeGroups:
enabled: false # Whether to override build-in privilege groups
cluster:
readonly:
privileges: ListDatabases,SelectOwnership,SelectUser,DescribeResourceGroup,ListResourceGroups # Cluster level readonly privileges
readwrite:
privileges: ListDatabases,SelectOwnership,SelectUser,DescribeResourceGroup,ListResourceGroups,FlushAll,TransferNode,TransferReplica,UpdateResourceGroups # Cluster level readwrite privileges
admin:
privileges: ListDatabases,SelectOwnership,SelectUser,DescribeResourceGroup,ListResourceGroups,FlushAll,TransferNode,TransferReplica,UpdateResourceGroups,BackupRBAC,RestoreRBAC,CreateDatabase,DropDatabase,CreateOwnership,DropOwnership,ManageOwnership,CreateResourceGroup,DropResourceGroup,UpdateUser # Cluster level admin privileges
database:
readonly:
privileges: ShowCollections,DescribeDatabase # Database level readonly privileges
readwrite:
privileges: ShowCollections,DescribeDatabase,AlterDatabase # Database level readwrite privileges
admin:
privileges: ShowCollections,DescribeDatabase,AlterDatabase,CreateCollection,DropCollection # Database level admin privileges
collection:
readonly:
privileges: Query,Search,IndexDetail,GetFlushState,GetLoadState,GetLoadingProgress,HasPartition,ShowPartitions,DescribeCollection,DescribeAlias,GetStatistics,ListAliases # Collection level readonly privileges
readwrite:
privileges: Query,Search,IndexDetail,GetFlushState,GetLoadState,GetLoadingProgress,HasPartition,ShowPartitions,DescribeCollection,DescribeAlias,GetStatistics,ListAliases,Load,Release,Insert,Delete,Upsert,Import,Flush,Compaction,LoadBalance,RenameCollection,CreateIndex,DropIndex,CreatePartition,DropPartition # Collection level readwrite privileges
admin:
privileges: Query,Search,IndexDetail,GetFlushState,GetLoadState,GetLoadingProgress,HasPartition,ShowPartitions,DescribeCollection,DescribeAlias,GetStatistics,ListAliases,Load,Release,Insert,Delete,Upsert,Import,Flush,Compaction,LoadBalance,RenameCollection,CreateIndex,DropIndex,CreatePartition,DropPartition,CreateAlias,DropAlias # Collection level admin privileges
tlsMode: 0
session:
ttl: 30 # ttl value when session granting a lease to register service

2
go.mod
View File

@ -26,7 +26,7 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/klauspost/compress v1.17.9
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1
github.com/minio/minio-go/v7 v7.0.73
github.com/pingcap/log v1.1.1-0.20221015072633-39906604fb81
github.com/prometheus/client_golang v1.14.0

4
go.sum
View File

@ -608,8 +608,8 @@ github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119 h1:9VXijWu
github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119/go.mod h1:DvXTE/K/RtHehxU8/GtDs4vFtfw64jJ3PaCnFri8CRg=
github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b h1:TfeY0NxYxZzUfIfYe5qYDBzt4ZYRqzUjTR6CvUzjat8=
github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b/go.mod h1:iwW+9cWfIzzDseEBCCeDSN5SD16Tidvy8cwQ7ZY8Qj4=
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17 h1:ANkXdUKKpIPPQkw9pkV9ku9AEtSaPyua9XzdMTUxjCs=
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs=
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1 h1:Xp4zOR85XFFtM7Eif945BeSmDf30hbdijbeNSuy92Bg=
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs=
github.com/milvus-io/milvus-storage/go v0.0.0-20231227072638-ebd0b8e56d70 h1:Z+sp64fmAOxAG7mU0dfVOXvAXlwRB0c8a96rIM5HevI=
github.com/milvus-io/milvus-storage/go v0.0.0-20231227072638-ebd0b8e56d70/go.mod h1:GPETMcTZq1gLY1WA6Na5kiNAKnq8SEMMiVKUZrM3sho=
github.com/milvus-io/pulsar-client-go v0.6.10 h1:eqpJjU+/QX0iIhEo3nhOqMNXL+TyInAs1IAHZCrCM/A=

View File

@ -43,6 +43,8 @@ const (
RevokeRoleAction = "revoke_role"
GrantPrivilegeAction = "grant_privilege"
RevokePrivilegeAction = "revoke_privilege"
GrantPrivilegeActionV2 = "grant_privilege_v2"
RevokePrivilegeActionV2 = "revoke_privilege_v2"
AlterAction = "alter"
GetProgressAction = "get_progress" // deprecated, keep it for compatibility, use `/v2/vectordb/jobs/import/describe` instead
AddPrivilegesToGroupAction = "add_privileges_to_group"

View File

@ -131,6 +131,8 @@ func (h *HandlersV2) RegisterRoutesToV2(router gin.IRouter) {
router.POST(RoleCategory+DropAction, timeoutMiddleware(wrapperPost(func() any { return &RoleReq{} }, wrapperTraceLog(h.dropRole))))
router.POST(RoleCategory+GrantPrivilegeAction, timeoutMiddleware(wrapperPost(func() any { return &GrantReq{} }, wrapperTraceLog(h.addPrivilegeToRole))))
router.POST(RoleCategory+RevokePrivilegeAction, timeoutMiddleware(wrapperPost(func() any { return &GrantReq{} }, wrapperTraceLog(h.removePrivilegeFromRole))))
router.POST(RoleCategory+GrantPrivilegeActionV2, timeoutMiddleware(wrapperPost(func() any { return &GrantV2Req{} }, wrapperTraceLog(h.grantV2))))
router.POST(RoleCategory+RevokePrivilegeActionV2, timeoutMiddleware(wrapperPost(func() any { return &GrantV2Req{} }, wrapperTraceLog(h.revokeV2))))
// privilege group
router.POST(PrivilegeGroupCategory+CreateAction, timeoutMiddleware(wrapperPost(func() any { return &PrivilegeGroupReq{} }, wrapperTraceLog(h.createPrivilegeGroup))))
@ -1702,6 +1704,37 @@ func (h *HandlersV2) operatePrivilegeToRole(ctx context.Context, c *gin.Context,
return resp, err
}
func (h *HandlersV2) operatePrivilegeToRoleV2(ctx context.Context, c *gin.Context, httpReq *GrantV2Req, operateType milvuspb.OperatePrivilegeType) (interface{}, error) {
req := &milvuspb.OperatePrivilegeRequest{
Entity: &milvuspb.GrantEntity{
Role: &milvuspb.RoleEntity{Name: httpReq.RoleName},
Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()},
ObjectName: httpReq.CollectionName,
DbName: httpReq.DbName,
Grantor: &milvuspb.GrantorEntity{
Privilege: &milvuspb.PrivilegeEntity{Name: httpReq.Privilege},
},
},
Type: operateType,
Version: "v2",
}
resp, err := wrapperProxy(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/OperatePrivilege", func(reqCtx context.Context, req any) (interface{}, error) {
return h.proxy.OperatePrivilege(reqCtx, req.(*milvuspb.OperatePrivilegeRequest))
})
if err == nil {
HTTPReturn(c, http.StatusOK, wrapperReturnDefault())
}
return resp, err
}
func (h *HandlersV2) grantV2(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) {
return h.operatePrivilegeToRoleV2(ctx, c, anyReq.(*GrantV2Req), milvuspb.OperatePrivilegeType_Grant)
}
func (h *HandlersV2) revokeV2(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) {
return h.operatePrivilegeToRoleV2(ctx, c, anyReq.(*GrantV2Req), milvuspb.OperatePrivilegeType_Revoke)
}
func (h *HandlersV2) addPrivilegeToRole(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) {
return h.operatePrivilegeToRole(ctx, c, anyReq.(*GrantReq), milvuspb.OperatePrivilegeType_Grant, dbName)
}

View File

@ -1106,7 +1106,7 @@ func TestMethodPost(t *testing.T) {
mp.EXPECT().UpdateCredential(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once()
mp.EXPECT().OperateUserRole(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Twice()
mp.EXPECT().CreateRole(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once()
mp.EXPECT().OperatePrivilege(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Twice()
mp.EXPECT().OperatePrivilege(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Times(4)
mp.EXPECT().CreateIndex(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Twice()
mp.EXPECT().CreateIndex(mock.Anything, mock.Anything).Return(commonErrorStatus, nil).Once()
mp.EXPECT().CreateAlias(mock.Anything, mock.Anything).Return(commonSuccessStatus, nil).Once()
@ -1179,6 +1179,12 @@ func TestMethodPost(t *testing.T) {
queryTestCases = append(queryTestCases, rawTestCase{
path: versionalV2(RoleCategory, RevokePrivilegeAction),
})
queryTestCases = append(queryTestCases, rawTestCase{
path: versionalV2(RoleCategory, GrantPrivilegeActionV2),
})
queryTestCases = append(queryTestCases, rawTestCase{
path: versionalV2(RoleCategory, RevokePrivilegeActionV2),
})
queryTestCases = append(queryTestCases, rawTestCase{
path: versionalV2(IndexCategory, CreateAction),
})

View File

@ -273,6 +273,13 @@ type PrivilegeGroupReq struct {
Privileges []string `json:"privileges"`
}
type GrantV2Req struct {
RoleName string `json:"roleName" binding:"required"`
DbName string `json:"dbName"`
CollectionName string `json:"collectionName"`
Privilege string `json:"privilege" binding:"required"`
}
type GrantReq struct {
RoleName string `json:"roleName" binding:"required"`
ObjectType string `json:"objectType" binding:"required"`

View File

@ -4734,6 +4734,61 @@ func (_c *MockProxy_OperatePrivilegeGroup_Call) RunAndReturn(run func(context.Co
return _c
}
// OperatePrivilegeV2 provides a mock function with given fields: _a0, _a1
func (_m *MockProxy) OperatePrivilegeV2(_a0 context.Context, _a1 *milvuspb.OperatePrivilegeV2Request) (*commonpb.Status, error) {
ret := _m.Called(_a0, _a1)
var r0 *commonpb.Status
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.OperatePrivilegeV2Request) (*commonpb.Status, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.OperatePrivilegeV2Request) *commonpb.Status); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*commonpb.Status)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.OperatePrivilegeV2Request) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockProxy_OperatePrivilegeV2_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OperatePrivilegeV2'
type MockProxy_OperatePrivilegeV2_Call struct {
*mock.Call
}
// OperatePrivilegeV2 is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 *milvuspb.OperatePrivilegeV2Request
func (_e *MockProxy_Expecter) OperatePrivilegeV2(_a0 interface{}, _a1 interface{}) *MockProxy_OperatePrivilegeV2_Call {
return &MockProxy_OperatePrivilegeV2_Call{Call: _e.mock.On("OperatePrivilegeV2", _a0, _a1)}
}
func (_c *MockProxy_OperatePrivilegeV2_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.OperatePrivilegeV2Request)) *MockProxy_OperatePrivilegeV2_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*milvuspb.OperatePrivilegeV2Request))
})
return _c
}
func (_c *MockProxy_OperatePrivilegeV2_Call) Return(_a0 *commonpb.Status, _a1 error) *MockProxy_OperatePrivilegeV2_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockProxy_OperatePrivilegeV2_Call) RunAndReturn(run func(context.Context, *milvuspb.OperatePrivilegeV2Request) (*commonpb.Status, error)) *MockProxy_OperatePrivilegeV2_Call {
_c.Call.Return(run)
return _c
}
// OperateUserRole provides a mock function with given fields: _a0, _a1
func (_m *MockProxy) OperateUserRole(_a0 context.Context, _a1 *milvuspb.OperateUserRoleRequest) (*commonpb.Status, error) {
ret := _m.Called(_a0, _a1)

View File

@ -5210,6 +5210,87 @@ func (node *Proxy) validPrivilegeParams(req *milvuspb.OperatePrivilegeRequest) e
return nil
}
func (node *Proxy) validOperatePrivilegeV2Params(req *milvuspb.OperatePrivilegeV2Request) error {
if req.Role == nil {
return fmt.Errorf("the role in the request is nil")
}
if err := ValidateRoleName(req.Role.Name); err != nil {
return err
}
if err := ValidatePrivilege(req.Grantor.Privilege.Name); err != nil {
return err
}
if req.Type != milvuspb.OperatePrivilegeType_Grant && req.Type != milvuspb.OperatePrivilegeType_Revoke {
return fmt.Errorf("the type in the request not grant or revoke")
}
if err := ValidateObjectName(req.DbName); err != nil {
return err
}
if err := ValidateObjectName(req.CollectionName); err != nil {
return err
}
return nil
}
func (node *Proxy) OperatePrivilegeV2(ctx context.Context, req *milvuspb.OperatePrivilegeV2Request) (*commonpb.Status, error) {
ctx, sp := otel.Tracer(typeutil.ProxyRole).Start(ctx, "Proxy-OperatePrivilegeV2")
defer sp.End()
log := log.Ctx(ctx)
log.Info("OperatePrivilegeV2",
zap.Any("req", req))
if err := merr.CheckHealthy(node.GetStateCode()); err != nil {
return merr.Status(err), nil
}
if err := node.validOperatePrivilegeV2Params(req); err != nil {
return merr.Status(err), nil
}
curUser, err := GetCurUserFromContext(ctx)
if err != nil {
return merr.Status(err), nil
}
req.Grantor.User = &milvuspb.UserEntity{Name: curUser}
request := &milvuspb.OperatePrivilegeRequest{
Base: &commonpb.MsgBase{MsgType: commonpb.MsgType_OperatePrivilege},
Entity: &milvuspb.GrantEntity{
Role: req.Role,
Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()},
ObjectName: req.CollectionName,
DbName: req.DbName,
Grantor: req.Grantor,
},
Type: req.Type,
Version: "v2",
}
req.Grantor.User = &milvuspb.UserEntity{Name: curUser}
result, err := node.rootCoord.OperatePrivilege(ctx, request)
if err != nil {
log.Warn("fail to operate privilege", zap.Error(err))
return merr.Status(err), nil
}
relatedPrivileges := util.RelatedPrivileges[util.PrivilegeNameForMetastore(req.Grantor.Privilege.Name)]
if len(relatedPrivileges) != 0 {
for _, relatedPrivilege := range relatedPrivileges {
relatedReq := proto.Clone(request).(*milvuspb.OperatePrivilegeRequest)
relatedReq.Entity.Grantor.Privilege.Name = util.PrivilegeNameForAPI(relatedPrivilege)
result, err = node.rootCoord.OperatePrivilege(ctx, relatedReq)
if err != nil {
log.Warn("fail to operate related privilege", zap.String("related_privilege", relatedPrivilege), zap.Error(err))
return merr.Status(err), nil
}
if !merr.Ok(result) {
log.Warn("fail to operate related privilege", zap.String("related_privilege", relatedPrivilege), zap.Any("result", result))
return result, nil
}
}
}
if merr.Ok(result) {
SendReplicateMessagePack(ctx, node.replicateMsgStream, req)
}
return result, nil
}
func (node *Proxy) OperatePrivilege(ctx context.Context, req *milvuspb.OperatePrivilegeRequest) (*commonpb.Status, error) {
ctx, sp := otel.Tracer(typeutil.ProxyRole).Start(ctx, "Proxy-OperatePrivilege")
defer sp.End()
@ -6399,8 +6480,11 @@ func (node *Proxy) CreatePrivilegeGroup(ctx context.Context, req *milvuspb.Creat
if err := merr.CheckHealthy(node.GetStateCode()); err != nil {
return merr.Status(err), nil
}
if req.GroupName == "" {
return merr.Status(fmt.Errorf("the group name in the drop privilege group request is nil")), nil
if err := ValidatePrivilegeGroupName(req.GroupName); err != nil {
log.Warn("CreatePrivilegeGroup failed",
zap.Error(err),
)
return getErrResponse(err, "CreatePrivilegeGroup", "", ""), nil
}
if req.Base == nil {
req.Base = &commonpb.MsgBase{}
@ -6428,8 +6512,11 @@ func (node *Proxy) DropPrivilegeGroup(ctx context.Context, req *milvuspb.DropPri
if err := merr.CheckHealthy(node.GetStateCode()); err != nil {
return merr.Status(err), nil
}
if req.GroupName == "" {
return merr.Status(fmt.Errorf("the group name in the drop privilege group request is nil")), nil
if err := ValidatePrivilegeGroupName(req.GroupName); err != nil {
log.Warn("DropPrivilegeGroup failed",
zap.Error(err),
)
return getErrResponse(err, "DropPrivilegeGroup", "", ""), nil
}
if req.Base == nil {
req.Base = &commonpb.MsgBase{}
@ -6486,8 +6573,11 @@ func (node *Proxy) OperatePrivilegeGroup(ctx context.Context, req *milvuspb.Oper
if err := merr.CheckHealthy(node.GetStateCode()); err != nil {
return merr.Status(err), nil
}
if req.GroupName == "" {
return merr.Status(fmt.Errorf("the group name in the drop privilege group request is nil")), nil
if err := ValidatePrivilegeGroupName(req.GroupName); err != nil {
log.Warn("OperatePrivilegeGroup failed",
zap.Error(err),
)
return getErrResponse(err, "OperatePrivilegeGroup", "", ""), nil
}
for _, priv := range req.GetPrivileges() {
if err := ValidatePrivilege(priv.Name); err != nil {

View File

@ -162,8 +162,8 @@ func PrivilegeInterceptor(ctx context.Context, req interface{}) (context.Context
e := getEnforcer()
for _, roleName := range roleNames {
permitFunc := func(resName string) (bool, error) {
object := funcutil.PolicyForResource(dbName, objectType, resName)
permitFunc := func(objectName string) (bool, error) {
object := funcutil.PolicyForResource(dbName, objectType, objectName)
isPermit, cached, version := GetPrivilegeCache(roleName, object, objectPrivilege)
if cached {
return isPermit, nil

View File

@ -563,3 +563,45 @@ func TestPrivilegeGroup(t *testing.T) {
assert.NoError(t, err)
})
}
func TestBuiltinPrivilegeGroup(t *testing.T) {
t.Run("ClusterAdmin", func(t *testing.T) {
paramtable.Get().Save(Params.CommonCfg.AuthorizationEnabled.Key, "true")
initPrivilegeGroups()
var err error
ctx := GetContext(context.Background(), "fooo:123456")
client := &MockRootCoordClientInterface{}
queryCoord := &mocks.MockQueryCoordClient{}
mgr := newShardClientMgr()
policies := []string{}
for _, priv := range util.BuiltinPrivilegeGroups["ClusterReadOnly"] {
objectType := util.GetObjectType(priv)
policies = append(policies, funcutil.PolicyForPrivilege("role1", objectType, "*", util.PrivilegeNameForMetastore(priv), "default"))
}
client.listPolicy = func(ctx context.Context, in *internalpb.ListPolicyRequest) (*internalpb.ListPolicyResponse, error) {
return &internalpb.ListPolicyResponse{
Status: merr.Success(),
PolicyInfos: policies,
UserRoles: []string{
funcutil.EncodeUserRoleCache("fooo", "role1"),
},
}, nil
}
InitMetaCache(ctx, client, queryCoord, mgr)
defer CleanPrivilegeCache()
_, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.SelectUserRequest{})
assert.NoError(t, err)
_, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.DescribeResourceGroupRequest{})
assert.NoError(t, err)
_, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.ListResourceGroupsRequest{})
assert.NoError(t, err)
_, err = PrivilegeInterceptor(GetContext(context.Background(), "fooo:123456"), &milvuspb.CreateResourceGroupRequest{})
assert.Error(t, err)
})
}

View File

@ -2700,6 +2700,8 @@ func TestProxy(t *testing.T) {
testProxyRole(ctx, t, proxy)
testProxyPrivilege(ctx, t, proxy)
testProxyOperatePrivilegeV2(ctx, t, proxy)
assert.False(t, false, true)
testProxyRefreshPolicyInfoCache(ctx, t, proxy)
// proxy unhealthy
@ -4368,6 +4370,106 @@ func testProxyPrivilege(ctx context.Context, t *testing.T, proxy *Proxy) {
wg.Wait()
}
func testProxyOperatePrivilegeV2(ctx context.Context, t *testing.T, proxy *Proxy) {
var wg sync.WaitGroup
wg.Add(1)
t.Run("Operate Privilege V2, Select Grant", func(t *testing.T) {
defer wg.Done()
// GrantPrivilege
req := &milvuspb.OperatePrivilegeV2Request{}
resp, _ := proxy.OperatePrivilegeV2(ctx, req)
assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode)
req.Role = &milvuspb.RoleEntity{}
resp, _ = proxy.OperatePrivilegeV2(ctx, req)
assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode)
req.Grantor = &milvuspb.GrantorEntity{}
resp, _ = proxy.OperatePrivilegeV2(ctx, req)
assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode)
req.Grantor.Privilege = &milvuspb.PrivilegeEntity{}
resp, _ = proxy.OperatePrivilegeV2(ctx, req)
assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode)
req.Grantor.Privilege.Name = util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeAll.String())
resp, _ = proxy.OperatePrivilegeV2(ctx, req)
assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode)
req.DbName = ""
resp, _ = proxy.OperatePrivilegeV2(ctx, req)
assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode)
req.CollectionName = ""
resp, _ = proxy.OperatePrivilegeV2(ctx, req)
assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode)
roleReq := &milvuspb.OperatePrivilegeV2Request{
Role: &milvuspb.RoleEntity{Name: "public"},
Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeLoad.String())}},
DbName: "default",
CollectionName: "col1",
Type: milvuspb.OperatePrivilegeType_Grant,
}
resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq)
assert.Equal(t, commonpb.ErrorCode_Success, resp.ErrorCode)
roleReq = &milvuspb.OperatePrivilegeV2Request{
Role: &milvuspb.RoleEntity{Name: "public"},
Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadOnly.String())}},
DbName: util.AnyWord,
CollectionName: util.AnyWord,
Type: milvuspb.OperatePrivilegeType_Grant,
}
resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq)
assert.Equal(t, commonpb.ErrorCode_Success, resp.ErrorCode)
roleReq = &milvuspb.OperatePrivilegeV2Request{
Role: &milvuspb.RoleEntity{Name: "public"},
Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadOnly.String())}},
DbName: "db1",
CollectionName: util.AnyWord,
Type: milvuspb.OperatePrivilegeType_Grant,
}
resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq)
assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode)
roleReq = &milvuspb.OperatePrivilegeV2Request{
Role: &milvuspb.RoleEntity{Name: "public"},
Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionReadOnly.String())}},
DbName: "db1",
CollectionName: util.AnyWord,
Type: milvuspb.OperatePrivilegeType_Grant,
}
resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq)
assert.Equal(t, commonpb.ErrorCode_Success, resp.ErrorCode)
roleReq = &milvuspb.OperatePrivilegeV2Request{
Role: &milvuspb.RoleEntity{Name: "public"},
Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadOnly.String())}},
DbName: "db1",
CollectionName: "col1",
Type: milvuspb.OperatePrivilegeType_Grant,
}
resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq)
assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode)
roleReq = &milvuspb.OperatePrivilegeV2Request{
Role: &milvuspb.RoleEntity{Name: "public"},
Grantor: &milvuspb.GrantorEntity{Privilege: &milvuspb.PrivilegeEntity{Name: util.MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseReadOnly.String())}},
DbName: "db1",
CollectionName: util.AnyWord,
Type: milvuspb.OperatePrivilegeType_Grant,
}
resp, _ = proxy.OperatePrivilegeV2(ctx, roleReq)
assert.Equal(t, commonpb.ErrorCode_Success, resp.ErrorCode)
})
wg.Wait()
}
func testProxyPrivilegeUnhealthy(ctx context.Context, t *testing.T, proxy *Proxy) {
testProxyPrivilegeFail(ctx, t, proxy, "unhealthy")
}

View File

@ -155,6 +155,32 @@ func validateCollectionNameOrAlias(entity, entityType string) error {
return nil
}
func ValidatePrivilegeGroupName(groupName string) error {
if groupName == "" {
return merr.WrapErrDatabaseNameInvalid(groupName, "privilege group name couldn't be empty")
}
if len(groupName) > Params.ProxyCfg.MaxNameLength.GetAsInt() {
return merr.WrapErrDatabaseNameInvalid(groupName,
fmt.Sprintf("the length of a privilege group name must be less than %d characters", Params.ProxyCfg.MaxNameLength.GetAsInt()))
}
firstChar := groupName[0]
if firstChar != '_' && !isAlpha(firstChar) {
return merr.WrapErrDatabaseNameInvalid(groupName,
"the first character of a privilege group name must be an underscore or letter")
}
for i := 1; i < len(groupName); i++ {
c := groupName[i]
if c != '_' && !isAlpha(c) && !isNumber(c) {
return merr.WrapErrDatabaseNameInvalid(groupName,
"privilege group name can only contain numbers, letters and underscores")
}
}
return nil
}
func ValidateResourceGroupName(entity string) error {
if entity == "" {
return errors.New("resource group name couldn't be empty")

View File

@ -98,6 +98,7 @@ type mockMetaTable struct {
DescribeDatabaseFunc func(ctx context.Context, dbName string) (*model.Database, error)
CreatePrivilegeGroupFunc func(groupName string) error
DropPrivilegeGroupFunc func(groupName string) error
IsCustomPrivilegeGroupFunc func(groupName string) (bool, error)
ListPrivilegeGroupsFunc func() ([]*milvuspb.PrivilegeGroupInfo, error)
OperatePrivilegeGroupFunc func(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error
GetPrivilegeGroupRolesFunc func(groupName string) ([]*milvuspb.RoleEntity, error)
@ -267,6 +268,10 @@ func (m mockMetaTable) ListPrivilegeGroups() ([]*milvuspb.PrivilegeGroupInfo, er
return m.ListPrivilegeGroupsFunc()
}
func (m mockMetaTable) IsCustomPrivilegeGroup(groupName string) (bool, error) {
return m.IsCustomPrivilegeGroupFunc(groupName)
}
func (m mockMetaTable) OperatePrivilegeGroup(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error {
return m.OperatePrivilegeGroupFunc(groupName, privileges, operateType)
}
@ -558,6 +563,9 @@ func withInvalidMeta() Opt {
meta.ListPrivilegeGroupsFunc = func() ([]*milvuspb.PrivilegeGroupInfo, error) {
return nil, errors.New("error mock ListPrivilegeGroups")
}
meta.IsCustomPrivilegeGroupFunc = func(groupName string) (bool, error) {
return false, errors.New("error mock IsCustomPrivilegeGroup")
}
meta.OperatePrivilegeGroupFunc = func(groupName string, privileges []*milvuspb.PrivilegeEntity, operateType milvuspb.OperatePrivilegeGroupType) error {
return errors.New("error mock OperatePrivilegeGroup")
}

View File

@ -21,6 +21,7 @@ import (
"fmt"
"math/rand"
"os"
"strings"
"sync"
"time"
@ -601,6 +602,50 @@ func (c *Core) initPublicRolePrivilege() error {
return nil
}
func (c *Core) initBuiltinPrivilegeGroups() []*milvuspb.PrivilegeGroupInfo {
// init built in privilege groups, override by config if rbac config enabled
builtinGroups := make([]*milvuspb.PrivilegeGroupInfo, 0)
for groupName, privileges := range util.BuiltinPrivilegeGroups {
if Params.RbacConfig.Enabled.GetAsBool() {
var confPrivs []string
switch groupName {
case "ClusterReadOnly":
confPrivs = Params.RbacConfig.ClusterReadOnlyPrivileges.GetAsStrings()
case "ClusterReadWrite":
confPrivs = Params.RbacConfig.ClusterReadWritePrivileges.GetAsStrings()
case "ClusterAdmin":
confPrivs = Params.RbacConfig.ClusterAdminPrivileges.GetAsStrings()
case "DatabaseReadOnly":
confPrivs = Params.RbacConfig.DBReadOnlyPrivileges.GetAsStrings()
case "DatabaseReadWrite":
confPrivs = Params.RbacConfig.DBReadWritePrivileges.GetAsStrings()
case "DatabaseAdmin":
confPrivs = Params.RbacConfig.DBAdminPrivileges.GetAsStrings()
case "CollectionReadOnly":
confPrivs = Params.RbacConfig.CollectionReadOnlyPrivileges.GetAsStrings()
case "CollectionReadWrite":
confPrivs = Params.RbacConfig.CollectionReadWritePrivileges.GetAsStrings()
case "CollectionAdmin":
confPrivs = Params.RbacConfig.CollectionAdminPrivileges.GetAsStrings()
default:
return nil
}
if len(confPrivs) > 0 {
privileges = confPrivs
}
}
privs := lo.Map(privileges, func(name string, _ int) *milvuspb.PrivilegeEntity {
return &milvuspb.PrivilegeEntity{Name: name}
})
builtinGroups = append(builtinGroups, &milvuspb.PrivilegeGroupInfo{
GroupName: groupName,
Privileges: privs,
})
}
return builtinGroups
}
func (c *Core) initBuiltinRoles() error {
rolePrivilegesMap := Params.RoleCfg.Roles.GetAsRoleDetails()
for role, privilegesJSON := range rolePrivilegesMap {
@ -612,7 +657,7 @@ func (c *Core) initBuiltinRoles() error {
for _, privilege := range privilegesJSON[util.RoleConfigPrivileges] {
privilegeName := privilege[util.RoleConfigPrivilege]
if !util.IsAnyWord(privilege[util.RoleConfigPrivilege]) {
dbPrivName, err := c.getMetastorePrivilegeName(privilege[util.RoleConfigPrivilege])
dbPrivName, err := c.getMetastorePrivilegeName(c.ctx, privilege[util.RoleConfigPrivilege])
if err != nil {
return errors.Wrapf(err, "failed to get metastore privilege name for: %s", privilege[util.RoleConfigPrivilege])
}
@ -2491,44 +2536,72 @@ func (c *Core) isValidObject(entity *milvuspb.ObjectEntity) error {
return nil
}
func (c *Core) isValidGrantor(entity *milvuspb.GrantorEntity, object string) error {
if entity == nil {
return errors.New("the grantor entity is nil")
}
if entity.User == nil || entity.User.Name == "" {
return errors.New("the user entity in the grantor entity is nil or empty")
}
if _, err := c.meta.SelectUser(util.DefaultTenant, &milvuspb.UserEntity{Name: entity.User.Name}, false); err != nil {
log.Warn("fail to select the user", zap.String("username", entity.User.Name), zap.Error(err))
return errors.New("not found the user, maybe the user isn't existed or internal system error")
}
if entity.Privilege == nil {
return errors.New("the privilege entity in the grantor entity is nil")
}
if util.IsAnyWord(entity.Privilege.Name) {
func (c *Core) isValidPrivilege(ctx context.Context, privilegeName string, object string) error {
if util.IsAnyWord(privilegeName) {
return nil
}
customPrivGroup, err := c.meta.IsCustomPrivilegeGroup(privilegeName)
if err != nil {
return err
}
if customPrivGroup {
return fmt.Errorf("can not operate the custom privilege group [%s]", privilegeName)
}
if lo.Contains(lo.Keys(util.BuiltinPrivilegeGroups), privilegeName) {
return fmt.Errorf("can not operate the built-in privilege group [%s]", privilegeName)
}
// check object privileges for built-in privileges
if util.IsPrivilegeNameDefined(entity.Privilege.Name) {
if util.IsPrivilegeNameDefined(privilegeName) {
privileges, ok := util.ObjectPrivileges[object]
if !ok {
return fmt.Errorf("not found the object type[name: %s], supported the object types: %v", object, lo.Keys(commonpb.ObjectType_value))
}
for _, privilege := range privileges {
if privilege == entity.Privilege.Name {
if privilege == privilegeName {
return nil
}
}
}
// check if it is a custom privilege group
customPrivGroup, err := c.meta.IsCustomPrivilegeGroup(entity.Privilege.Name)
if err != nil {
return err
}
if customPrivGroup {
return fmt.Errorf("not found the privilege name[%s] in object[%s]", privilegeName, object)
}
func (c *Core) isValidPrivilegeV2(ctx context.Context, privilegeName, dbName, collectionName string) error {
if util.IsAnyWord(privilegeName) {
return nil
}
var privilegeLevel string
for group, privileges := range util.BuiltinPrivilegeGroups {
if privilegeName == group || lo.Contains(privileges, privilegeName) {
privilegeLevel = group
break
}
}
if privilegeLevel == "" {
customPrivGroup, err := c.meta.IsCustomPrivilegeGroup(privilegeName)
if err != nil {
return err
}
if customPrivGroup {
return nil
}
return fmt.Errorf("not found the privilege name[%s] in the custom privilege groups", privilegeName)
}
switch {
case strings.HasPrefix(privilegeLevel, milvuspb.PrivilegeLevel_Cluster.String()):
if !util.IsAnyWord(dbName) || !util.IsAnyWord(collectionName) {
return fmt.Errorf("dbName and collectionName should be * for the cluster level privilege: %s", privilegeName)
}
return nil
case strings.HasPrefix(privilegeLevel, milvuspb.PrivilegeLevel_Database.String()):
if collectionName != "" && collectionName != util.AnyWord {
return fmt.Errorf("collectionName should be empty or * for the database level privilege: %s", privilegeName)
}
return nil
case strings.HasPrefix(privilegeLevel, milvuspb.PrivilegeLevel_Collection.String()):
return nil
default:
return nil
}
return fmt.Errorf("not found the privilege name[%s] in object[%s]", entity.Privilege.Name, object)
}
// OperatePrivilege operate the privilege, including grant and revoke
@ -2545,50 +2618,42 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile
ctxLog := log.Ctx(ctx).With(zap.String("role", typeutil.RootCoordRole), zap.Any("in", in))
ctxLog.Debug(method)
if err := merr.CheckHealthy(c.GetStateCode()); err != nil {
return merr.Status(err), nil
}
if in.Type != milvuspb.OperatePrivilegeType_Grant && in.Type != milvuspb.OperatePrivilegeType_Revoke {
errMsg := fmt.Sprintf("invalid operate privilege type, current type: %s, valid value: [%s, %s]", in.Type, milvuspb.OperatePrivilegeType_Grant, milvuspb.OperatePrivilegeType_Revoke)
ctxLog.Warn(errMsg)
return merr.StatusWithErrorCode(errors.New(errMsg), commonpb.ErrorCode_OperatePrivilegeFailure), nil
}
if in.Entity == nil {
errMsg := "the grant entity in the request is nil"
ctxLog.Error(errMsg)
return merr.StatusWithErrorCode(errors.New(errMsg), commonpb.ErrorCode_OperatePrivilegeFailure), nil
}
if err := c.isValidObject(in.Entity.Object); err != nil {
ctxLog.Warn("", zap.Error(err))
if err := c.operatePrivilegeCommonCheck(ctx, in); err != nil {
return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil
}
if err := c.isValidRole(in.Entity.Role); err != nil {
ctxLog.Warn("", zap.Error(err))
return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil
}
if err := c.isValidGrantor(in.Entity.Grantor, in.Entity.Object.Name); err != nil {
ctxLog.Error("", zap.Error(err))
return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil
switch in.Version {
case "v2":
if err := c.isValidPrivilegeV2(ctx, in.Entity.Grantor.Privilege.Name,
in.Entity.DbName, in.Entity.ObjectName); err != nil {
ctxLog.Error("", zap.Error(err))
return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil
}
default:
if err := c.isValidPrivilege(ctx, in.Entity.Grantor.Privilege.Name, in.Entity.Object.Name); err != nil {
ctxLog.Error("", zap.Error(err))
return merr.StatusWithErrorCode(err, commonpb.ErrorCode_OperatePrivilegeFailure), nil
}
// set up object name if it is global object type and not built in privilege group
if in.Entity.Object.Name == commonpb.ObjectType_Global.String() && !lo.Contains(lo.Keys(util.BuiltinPrivilegeGroups), in.Entity.Grantor.Privilege.Name) {
in.Entity.ObjectName = util.AnyWord
}
}
// 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", privName))
if in.Entity.Object.Name == commonpb.ObjectType_Global.String() {
in.Entity.ObjectName = util.AnyWord
}
redoTask := newBaseRedoTask(c.stepExecutor)
redoTask.AddSyncStep(NewSimpleStep("operate privilege meta data", func(ctx context.Context) ([]nestedStep, error) {
if !util.IsAnyWord(privName) {
// set up privilege name for metastore
dbPrivName, err := c.getMetastorePrivilegeName(ctx, privName)
if err != nil {
return nil, err
}
in.Entity.Grantor.Privilege.Name = dbPrivName
}
err := c.meta.OperatePrivilege(util.DefaultTenant, in.Entity, in.Type)
if err != nil && !common.IsIgnorableError(err) {
log.Warn("fail to operate the privilege", zap.Any("in", in), zap.Error(err))
@ -2597,6 +2662,8 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile
return nil, nil
}))
redoTask.AddAsyncStep(NewSimpleStep("operate privilege cache", func(ctx context.Context) ([]nestedStep, error) {
// set back to expand privilege group
in.Entity.Grantor.Privilege.Name = privName
var opType int32
switch in.Type {
case milvuspb.OperatePrivilegeType_Grant:
@ -2607,9 +2674,23 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile
log.Warn("invalid operate type for the OperatePrivilege api", zap.Any("in", in))
return nil, nil
}
grants := []*milvuspb.GrantEntity{in.Entity}
allGroups, err := c.meta.ListPrivilegeGroups()
allGroups = append(allGroups, c.initBuiltinPrivilegeGroups()...)
if err != nil {
return nil, err
}
groups := lo.SliceToMap(allGroups, func(group *milvuspb.PrivilegeGroupInfo) (string, []*milvuspb.PrivilegeEntity) {
return group.GroupName, group.Privileges
})
expandGrants, err := c.expandPrivilegeGroups(grants, groups)
if err != nil {
return nil, err
}
if err := c.proxyClientManager.RefreshPolicyInfoCache(ctx, &proxypb.RefreshPolicyInfoCacheRequest{
OpType: opType,
OpKey: funcutil.PolicyForPrivilege(in.Entity.Role.Name, in.Entity.Object.Name, in.Entity.ObjectName, in.Entity.Grantor.Privilege.Name, in.Entity.DbName),
OpKey: funcutil.PolicyForPrivileges(expandGrants),
}); err != nil {
log.Warn("fail to refresh policy info cache", zap.Any("in", in), zap.Error(err))
return nil, err
@ -2630,7 +2711,42 @@ func (c *Core) OperatePrivilege(ctx context.Context, in *milvuspb.OperatePrivile
return merr.Success(), nil
}
func (c *Core) getMetastorePrivilegeName(privName string) (string, error) {
func (c *Core) operatePrivilegeCommonCheck(ctx context.Context, in *milvuspb.OperatePrivilegeRequest) error {
if err := merr.CheckHealthy(c.GetStateCode()); err != nil {
return err
}
if in.Type != milvuspb.OperatePrivilegeType_Grant && in.Type != milvuspb.OperatePrivilegeType_Revoke {
errMsg := fmt.Sprintf("invalid operate privilege type, current type: %s, valid value: [%s, %s]", in.Type, milvuspb.OperatePrivilegeType_Grant, milvuspb.OperatePrivilegeType_Revoke)
return errors.New(errMsg)
}
if in.Entity == nil {
errMsg := "the grant entity in the request is nil"
return errors.New(errMsg)
}
if err := c.isValidObject(in.Entity.Object); err != nil {
return errors.New("the object entity in the request is nil or invalid")
}
if err := c.isValidRole(in.Entity.Role); err != nil {
return err
}
entity := in.Entity.Grantor
if entity == nil {
return errors.New("the grantor entity is nil")
}
if entity.User == nil || entity.User.Name == "" {
return errors.New("the user entity in the grantor entity is nil or empty")
}
if _, err := c.meta.SelectUser(util.DefaultTenant, &milvuspb.UserEntity{Name: entity.User.Name}, false); err != nil {
log.Warn("fail to select the user", zap.String("username", entity.User.Name), zap.Error(err))
return errors.New("not found the user, maybe the user isn't existed or internal system error")
}
if entity.Privilege == nil {
return errors.New("the privilege entity in the grantor entity is nil")
}
return nil
}
func (c *Core) getMetastorePrivilegeName(ctx context.Context, privName string) (string, error) {
// if it is built-in privilege, return the privilege name directly
if util.IsPrivilegeNameDefined(privName) {
return util.PrivilegeNameForMetastore(privName), nil
@ -3026,6 +3142,18 @@ func (c *Core) ListPrivilegeGroups(ctx context.Context, in *milvuspb.ListPrivile
ctxLog.Debug(method + " success")
metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.SuccessLabel).Inc()
metrics.RootCoordDDLReqLatency.WithLabelValues(method).Observe(float64(tr.ElapseSpan().Milliseconds()))
// append built in privilege groups
for groupName, privileges := range util.BuiltinPrivilegeGroups {
privGroups = append(privGroups, &milvuspb.PrivilegeGroupInfo{
GroupName: groupName,
Privileges: lo.Map(privileges, func(p string, _ int) *milvuspb.PrivilegeEntity {
return &milvuspb.PrivilegeEntity{
Name: p,
}
}),
})
}
return &milvuspb.ListPrivilegeGroupsResponse{
Status: merr.Success(),
PrivilegeGroups: privGroups,
@ -3072,6 +3200,14 @@ func (c *Core) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePr
newGroups[k] = lo.UniqBy(newPrivs, func(p *milvuspb.PrivilegeEntity) string {
return p.Name
})
// check if privileges are the same object type
objectTypes := lo.SliceToMap(newPrivs, func(p *milvuspb.PrivilegeEntity) (string, struct{}) {
return util.GetObjectType(p.Name), struct{}{}
})
if len(objectTypes) > 1 {
return nil, errors.New("privileges are not the same object type")
}
case milvuspb.OperatePrivilegeGroupType_RemovePrivilegesFromGroup:
newPrivs, _ := lo.Difference(v, in.Privileges)
newGroups[k] = newPrivs
@ -3083,11 +3219,11 @@ func (c *Core) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePr
rolesToRevoke := []*milvuspb.GrantEntity{}
rolesToGrant := []*milvuspb.GrantEntity{}
compareGrants := func(a, b *milvuspb.GrantEntity) bool {
return a.Role.GetName() == b.Role.GetName() &&
a.Object.GetName() == b.Object.GetName() &&
return a.Role.Name == b.Role.Name &&
a.Object.Name == b.Object.Name &&
a.ObjectName == b.ObjectName &&
a.Grantor.GetUser().GetName() == b.Grantor.GetUser().GetName() &&
a.Grantor.GetPrivilege().GetName() == b.Grantor.GetPrivilege().GetName() &&
a.Grantor.User.Name == b.Grantor.User.Name &&
a.Grantor.Privilege.Name == b.Grantor.Privilege.Name &&
a.DbName == b.DbName
}
for _, role := range roles {
@ -3098,8 +3234,14 @@ func (c *Core) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePr
if err != nil {
return nil, err
}
currGrants := c.expandPrivilegeGroups(grants, currGroups)
newGrants := c.expandPrivilegeGroups(grants, newGroups)
currGrants, err := c.expandPrivilegeGroups(grants, currGroups)
if err != nil {
return nil, err
}
newGrants, err := c.expandPrivilegeGroups(grants, newGroups)
if err != nil {
return nil, err
}
toRevoke := lo.Filter(currGrants, func(item *milvuspb.GrantEntity, _ int) bool {
return !lo.ContainsBy(newGrants, func(newItem *milvuspb.GrantEntity) bool {
@ -3163,28 +3305,50 @@ func (c *Core) OperatePrivilegeGroup(ctx context.Context, in *milvuspb.OperatePr
return merr.Success(), nil
}
func (c *Core) expandPrivilegeGroups(grants []*milvuspb.GrantEntity, groups map[string][]*milvuspb.PrivilegeEntity) []*milvuspb.GrantEntity {
func (c *Core) expandPrivilegeGroups(grants []*milvuspb.GrantEntity, groups map[string][]*milvuspb.PrivilegeEntity) ([]*milvuspb.GrantEntity, error) {
newGrants := []*milvuspb.GrantEntity{}
createGrantEntity := func(grant *milvuspb.GrantEntity, privilegeName string) (*milvuspb.GrantEntity, error) {
metaName, err := c.getMetastorePrivilegeName(c.ctx, privilegeName)
if err != nil {
return nil, err
}
if objectType := util.GetObjectType(privilegeName); objectType != "" {
grant.Object.Name = objectType
}
return &milvuspb.GrantEntity{
Role: grant.Role,
Object: grant.Object,
ObjectName: grant.ObjectName,
Grantor: &milvuspb.GrantorEntity{
User: grant.Grantor.User,
Privilege: &milvuspb.PrivilegeEntity{
Name: metaName,
},
},
DbName: grant.DbName,
}, nil
}
for _, grant := range grants {
if groups[grant.Grantor.Privilege.Name] == nil {
newGrants = append(newGrants, grant)
privName := grant.Grantor.Privilege.Name
if privGroup, exists := groups[privName]; !exists {
newGrant, err := createGrantEntity(grant, privName)
if err != nil {
return nil, err
}
newGrants = append(newGrants, newGrant)
} else {
for _, priv := range groups[grant.Grantor.Privilege.Name] {
newGrants = append(newGrants, &milvuspb.GrantEntity{
Role: grant.Role,
Object: grant.Object,
ObjectName: grant.ObjectName,
Grantor: &milvuspb.GrantorEntity{
User: grant.Grantor.User,
Privilege: priv,
},
DbName: grant.DbName,
})
for _, priv := range privGroup {
newGrant, err := createGrantEntity(grant, priv.Name)
if err != nil {
return nil, err
}
newGrants = append(newGrants, newGrant)
}
}
}
// uniq by role + object + object name + grantor user + privilege name + db name
return lo.UniqBy(newGrants, func(g *milvuspb.GrantEntity) string {
return fmt.Sprintf("%s-%s-%s-%s-%s-%s", g.Role, g.Object, g.ObjectName, g.Grantor.User, g.Grantor.Privilege.Name, g.DbName)
})
}), nil
}

View File

@ -1764,7 +1764,9 @@ func TestRootCoord_RBACError(t *testing.T) {
assert.NoError(t, err)
assert.NotEqual(t, commonpb.ErrorCode_Success, resp.ErrorCode)
}
mockMeta.IsCustomPrivilegeGroupFunc = func(groupName string) (bool, error) {
return false, nil
}
mockMeta.SelectUserFunc = func(tenant string, entity *milvuspb.UserEntity, includeRoleInfo bool) ([]*milvuspb.UserResult, error) {
return nil, nil
}
@ -2009,6 +2011,29 @@ func TestCore_InitRBAC(t *testing.T) {
err := c.initRbac()
assert.NoError(t, err)
})
t.Run("init default privilege groups", func(t *testing.T) {
clusterReadWrite := `SelectOwnership,SelectUser,DescribeResourceGroup`
meta := mockrootcoord.NewIMetaTable(t)
c := newTestCore(withHealthyCode(), withMeta(meta))
Params.Save(Params.RbacConfig.Enabled.Key, "true")
Params.Save(Params.RbacConfig.ClusterReadWritePrivileges.Key, clusterReadWrite)
defer func() {
Params.Reset(Params.RbacConfig.Enabled.Key)
Params.Reset(Params.RbacConfig.ClusterReadWritePrivileges.Key)
}()
builtinGroups := c.initBuiltinPrivilegeGroups()
fmt.Println(builtinGroups)
assert.Equal(t, len(util.BuiltinPrivilegeGroups), len(builtinGroups))
for _, group := range builtinGroups {
if group.GroupName == "ClusterReadWrite" {
assert.Equal(t, len(group.Privileges), 3)
}
}
})
}
func TestCore_BackupRBAC(t *testing.T) {

View File

@ -12,7 +12,7 @@ require (
github.com/expr-lang/expr v1.15.7
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/klauspost/compress v1.17.7
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1
github.com/nats-io/nats-server/v2 v2.10.12
github.com/nats-io/nats.go v1.34.1
github.com/panjf2000/ants/v2 v2.7.2

View File

@ -503,8 +503,8 @@ github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119 h1:9VXijWu
github.com/milvus-io/cgosymbolizer v0.0.0-20240722103217-b7dee0e50119/go.mod h1:DvXTE/K/RtHehxU8/GtDs4vFtfw64jJ3PaCnFri8CRg=
github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b h1:TfeY0NxYxZzUfIfYe5qYDBzt4ZYRqzUjTR6CvUzjat8=
github.com/milvus-io/gorocksdb v0.0.0-20220624081344-8c5f4212846b/go.mod h1:iwW+9cWfIzzDseEBCCeDSN5SD16Tidvy8cwQ7ZY8Qj4=
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17 h1:ANkXdUKKpIPPQkw9pkV9ku9AEtSaPyua9XzdMTUxjCs=
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.17/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs=
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1 h1:Xp4zOR85XFFtM7Eif945BeSmDf30hbdijbeNSuy92Bg=
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.18-0.20241120092224-a1c2ac2fd2c1/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs=
github.com/milvus-io/pulsar-client-go v0.6.10 h1:eqpJjU+/QX0iIhEo3nhOqMNXL+TyInAs1IAHZCrCM/A=
github.com/milvus-io/pulsar-client-go v0.6.10/go.mod h1:lQqCkgwDF8YFYjKA+zOheTk1tev2B+bKj5j7+nm8M1w=
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=

View File

@ -19,6 +19,8 @@ package util
import (
"strings"
"github.com/samber/lo"
"github.com/milvus-io/milvus-proto/go-api/v2/commonpb"
"github.com/milvus-io/milvus/pkg/common"
"github.com/milvus-io/milvus/pkg/util/typeutil"
@ -160,6 +162,15 @@ var (
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropPrivilegeGroup.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListPrivilegeGroups.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeOperatePrivilegeGroup.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadOnly.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadWrite.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterAdmin.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseReadOnly.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseReadWrite.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseAdmin.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionReadOnly.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionReadWrite.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionAdmin.String()),
},
commonpb.ObjectType_User.String(): {
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeUpdateUser.String()),
@ -283,6 +294,97 @@ var (
commonpb.ObjectPrivilege_PrivilegeAlterDatabase.String(),
commonpb.ObjectPrivilege_PrivilegeFlush.String(),
}
BuiltinPrivilegeGroups = map[string][]string{
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionReadOnly.String()): CollectionReadOnlyPrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionReadWrite.String()): CollectionReadWritePrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupCollectionAdmin.String()): CollectionAdminPrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseReadOnly.String()): DatabaseReadOnlyPrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseReadWrite.String()): DatabaseReadWritePrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupDatabaseAdmin.String()): DatabaseAdminPrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadOnly.String()): ClusterReadOnlyPrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterReadWrite.String()): ClusterReadWritePrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGroupClusterAdmin.String()): ClusterAdminPrivilegeGroup,
}
CollectionReadOnlyPrivilegeGroup = []string{
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeQuery.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeSearch.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeIndexDetail.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGetFlushState.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGetLoadState.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGetLoadingProgress.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeHasPartition.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeShowPartitions.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDescribeCollection.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDescribeAlias.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeGetStatistics.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListAliases.String()),
}
CollectionReadWritePrivilegeGroup = append(CollectionReadOnlyPrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeLoad.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeRelease.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeInsert.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDelete.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeUpsert.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeImport.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeFlush.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCompaction.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeLoadBalance.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeRenameCollection.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateIndex.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropIndex.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreatePartition.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropPartition.String()),
)
CollectionAdminPrivilegeGroup = append(CollectionReadWritePrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateAlias.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropAlias.String()),
)
DatabaseReadOnlyPrivilegeGroup = []string{
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeShowCollections.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDescribeDatabase.String()),
}
DatabaseReadWritePrivilegeGroup = append(DatabaseReadOnlyPrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeAlterDatabase.String()),
)
DatabaseAdminPrivilegeGroup = append(DatabaseReadWritePrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateCollection.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropCollection.String()),
)
ClusterReadOnlyPrivilegeGroup = []string{
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListDatabases.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeSelectOwnership.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeSelectUser.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDescribeResourceGroup.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeListResourceGroups.String()),
}
ClusterReadWritePrivilegeGroup = append(ClusterReadOnlyPrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeFlushAll.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeTransferNode.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeTransferReplica.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeUpdateResourceGroups.String()),
)
ClusterAdminPrivilegeGroup = append(ClusterReadWritePrivilegeGroup,
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeBackupRBAC.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeRestoreRBAC.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateDatabase.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropDatabase.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateOwnership.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropOwnership.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeManageOwnership.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeCreateResourceGroup.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeDropResourceGroup.String()),
MetaStore2API(commonpb.ObjectPrivilege_PrivilegeUpdateUser.String()),
)
)
// StringSet convert array to map for conveniently check if the array contains an element
@ -344,6 +446,12 @@ func IsPrivilegeNameDefined(name string) bool {
return PrivilegeNameForMetastore(name) != ""
}
func IsBuiltinPrivilegeGroup(name string) bool {
dbPrivilege := PrivilegeGroupWord + name
_, ok := commonpb.ObjectPrivilege_value[dbPrivilege]
return ok
}
func PrivilegeGroupNameForMetastore(name string) string {
return PrivilegeGroupWord + name
}
@ -360,3 +468,12 @@ func IsBuiltinRole(roleName string) bool {
}
return false
}
func GetObjectType(privName string) string {
for objectType, privs := range ObjectPrivileges {
if lo.Contains(privs, privName) {
return objectType
}
}
return ""
}

View File

@ -74,6 +74,7 @@ type ComponentParam struct {
HTTPCfg httpConfig
LogCfg logConfig
RoleCfg roleConfig
RbacConfig rbacConfig
RootCoordGrpcServerCfg GrpcServerConfig
ProxyGrpcServerCfg GrpcServerConfig
@ -124,6 +125,7 @@ func (p *ComponentParam) init(bt *BaseTable) {
p.HTTPCfg.init(bt)
p.LogCfg.init(bt)
p.RoleCfg.init(bt)
p.RbacConfig.init(bt)
p.GpuConfig.init(bt)
p.RootCoordGrpcServerCfg.Init("rootCoord", bt)

View File

@ -0,0 +1,41 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package paramtable
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/milvus-io/milvus/pkg/util"
)
func TestRbacConfig_Init(t *testing.T) {
params := ComponentParam{}
params.Init(NewBaseTable(SkipRemote(true)))
cfg := &params.RbacConfig
assert.Equal(t, cfg.Enabled.GetAsBool(), false)
assert.Equal(t, cfg.ClusterReadOnlyPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["ClusterReadOnly"])
assert.Equal(t, cfg.ClusterReadWritePrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["ClusterReadWrite"])
assert.Equal(t, cfg.ClusterAdminPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["ClusterAdmin"])
assert.Equal(t, cfg.DBReadOnlyPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["DatabaseReadOnly"])
assert.Equal(t, cfg.DBReadWritePrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["DatabaseReadWrite"])
assert.Equal(t, cfg.DBAdminPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["DatabaseAdmin"])
assert.Equal(t, cfg.CollectionReadOnlyPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["CollectionReadOnly"])
assert.Equal(t, cfg.CollectionReadWritePrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["CollectionReadWrite"])
assert.Equal(t, cfg.CollectionAdminPrivileges.GetAsStrings(), util.BuiltinPrivilegeGroups["CollectionAdmin"])
}

View File

@ -0,0 +1,114 @@
package paramtable
import (
"strings"
"github.com/milvus-io/milvus/pkg/util"
)
type rbacConfig struct {
Enabled ParamItem `refreshable:"false"`
ClusterReadOnlyPrivileges ParamItem `refreshable:"false"`
ClusterReadWritePrivileges ParamItem `refreshable:"false"`
ClusterAdminPrivileges ParamItem `refreshable:"false"`
DBReadOnlyPrivileges ParamItem `refreshable:"false"`
DBReadWritePrivileges ParamItem `refreshable:"false"`
DBAdminPrivileges ParamItem `refreshable:"false"`
CollectionReadOnlyPrivileges ParamItem `refreshable:"false"`
CollectionReadWritePrivileges ParamItem `refreshable:"false"`
CollectionAdminPrivileges ParamItem `refreshable:"false"`
}
func (p *rbacConfig) init(base *BaseTable) {
p.Enabled = ParamItem{
Key: "common.security.rbac.overrideBuiltInPrivilgeGroups.enabled",
DefaultValue: "false",
Version: "2.4.16",
Doc: "Whether to override build-in privilege groups",
Export: true,
}
p.Enabled.Init(base.mgr)
p.ClusterReadOnlyPrivileges = ParamItem{
Key: "common.security.rbac.cluster.readonly.privileges",
DefaultValue: strings.Join(util.ClusterReadOnlyPrivilegeGroup, ","),
Version: "2.4.16",
Doc: "Cluster level readonly privileges",
Export: true,
}
p.ClusterReadOnlyPrivileges.Init(base.mgr)
p.ClusterReadWritePrivileges = ParamItem{
Key: "common.security.rbac.cluster.readwrite.privileges",
DefaultValue: strings.Join(util.ClusterReadWritePrivilegeGroup, ","),
Version: "2.4.16",
Doc: "Cluster level readwrite privileges",
Export: true,
}
p.ClusterReadWritePrivileges.Init(base.mgr)
p.ClusterAdminPrivileges = ParamItem{
Key: "common.security.rbac.cluster.admin.privileges",
DefaultValue: strings.Join(util.ClusterAdminPrivilegeGroup, ","),
Version: "2.4.16",
Doc: "Cluster level admin privileges",
Export: true,
}
p.ClusterAdminPrivileges.Init(base.mgr)
p.DBReadOnlyPrivileges = ParamItem{
Key: "common.security.rbac.database.readonly.privileges",
DefaultValue: strings.Join(util.DatabaseReadOnlyPrivilegeGroup, ","),
Version: "2.4.16",
Doc: "Database level readonly privileges",
Export: true,
}
p.DBReadOnlyPrivileges.Init(base.mgr)
p.DBReadWritePrivileges = ParamItem{
Key: "common.security.rbac.database.readwrite.privileges",
DefaultValue: strings.Join(util.DatabaseReadWritePrivilegeGroup, ","),
Version: "2.4.16",
Doc: "Database level readwrite privileges",
Export: true,
}
p.DBReadWritePrivileges.Init(base.mgr)
p.DBAdminPrivileges = ParamItem{
Key: "common.security.rbac.database.admin.privileges",
DefaultValue: strings.Join(util.DatabaseAdminPrivilegeGroup, ","),
Version: "2.4.16",
Doc: "Database level admin privileges",
Export: true,
}
p.DBAdminPrivileges.Init(base.mgr)
p.CollectionReadOnlyPrivileges = ParamItem{
Key: "common.security.rbac.collection.readonly.privileges",
DefaultValue: strings.Join(util.CollectionReadOnlyPrivilegeGroup, ","),
Version: "2.4.16",
Doc: "Collection level readonly privileges",
Export: true,
}
p.CollectionReadOnlyPrivileges.Init(base.mgr)
p.CollectionReadWritePrivileges = ParamItem{
Key: "common.security.rbac.collection.readwrite.privileges",
DefaultValue: strings.Join(util.CollectionReadWritePrivilegeGroup, ","),
Version: "2.4.16",
Doc: "Collection level readwrite privileges",
Export: true,
}
p.CollectionReadWritePrivileges.Init(base.mgr)
p.CollectionAdminPrivileges = ParamItem{
Key: "common.security.rbac.collection.admin.privileges",
DefaultValue: strings.Join(util.CollectionAdminPrivilegeGroup, ","),
Version: "2.4.16",
Doc: "Collection level admin privileges",
Export: true,
}
p.CollectionAdminPrivileges.Init(base.mgr)
}

View File

@ -18,8 +18,10 @@ package rbac
import (
"context"
"fmt"
"strings"
"testing"
"github.com/samber/lo"
"github.com/stretchr/testify/suite"
"github.com/milvus-io/milvus-proto/go-api/v2/commonpb"
@ -46,10 +48,10 @@ func (s *PrivilegeGroupTestSuite) TestBuiltinPrivilegeGroup() {
ctx := GetContext(context.Background(), "root:123456")
// Test empty RBAC content
resp, err := s.Cluster.Proxy.BackupRBAC(ctx, &milvuspb.BackupRBACMetaRequest{})
backupResp, err := s.Cluster.Proxy.BackupRBAC(ctx, &milvuspb.BackupRBACMetaRequest{})
s.NoError(err)
s.True(merr.Ok(resp.GetStatus()))
s.Equal("", resp.GetRBACMeta().String())
s.True(merr.Ok(backupResp.GetStatus()))
s.Equal("", backupResp.GetRBACMeta().String())
// Generate some RBAC content
roleName := "test_role"
@ -59,24 +61,130 @@ func (s *PrivilegeGroupTestSuite) TestBuiltinPrivilegeGroup() {
s.NoError(err)
s.True(merr.Ok(createRoleResp))
s.operatePrivilege(ctx, roleName, "ReadOnly", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
s.operatePrivilege(ctx, roleName, "ReadWrite", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
s.operatePrivilege(ctx, roleName, "Admin", commonpb.ObjectType_Global.String(), milvuspb.OperatePrivilegeType_Grant)
resp, _ := s.operatePrivilege(ctx, roleName, "ReadOnly", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilege(ctx, roleName, "ReadWrite", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilege(ctx, roleName, "Admin", commonpb.ObjectType_Global.String(), milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
for _, builtinGroup := range lo.Keys(util.BuiltinPrivilegeGroups) {
fmt.Println("!!! builtinGroup: ", builtinGroup)
resp, _ = s.operatePrivilege(ctx, roleName, builtinGroup, commonpb.ObjectType_Global.String(), milvuspb.OperatePrivilegeType_Grant)
s.False(merr.Ok(resp))
}
s.validateGrants(ctx, roleName, commonpb.ObjectType_Global.String(), 1)
s.validateGrants(ctx, roleName, commonpb.ObjectType_Collection.String(), 2)
}
/*
create group1: query, search
grant insert to role -> role: insert
grant group1 to role -> role: insert, group1(query, search)
create group2: query, delete
grant group2 to role -> role: insert, group1(query, search), group2(query, delete)
add query, load to group1 -> group1: query, search, load -> role: insert, group1(query, search, load), group2(query, delete)
remove query from group1 -> group1: search, load -> role: insert, group1(search, load), group2(query, delete), role still have query privilege because of group2 granted.
*/
func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() {
func (s *PrivilegeGroupTestSuite) TestInvalidPrivilegeGroup() {
ctx := GetContext(context.Background(), "root:123456")
createResp, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{
GroupName: "",
})
s.NoError(err)
s.False(merr.Ok(createResp))
// create group %$ will fail
createResp, _ = s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{
GroupName: "%$",
})
s.False(merr.Ok(createResp))
createResp, _ = s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{
GroupName: "a&",
})
s.False(merr.Ok(createResp))
createResp, _ = s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{
GroupName: strings.Repeat("a", 300),
})
s.False(merr.Ok(createResp))
// drop group %$ will fail
dropResp, _ := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{
GroupName: "%$",
})
s.False(merr.Ok(dropResp))
dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{
GroupName: "group1",
})
s.NoError(err)
s.True(merr.Ok(dropResp))
dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{
GroupName: "",
})
s.NoError(err)
s.False(merr.Ok(dropResp))
operateResp, err := s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{
GroupName: "",
})
s.NoError(err)
s.False(merr.Ok(operateResp))
operateResp, err = s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{
GroupName: "group1",
Privileges: []*milvuspb.PrivilegeEntity{{Name: "123"}},
})
s.NoError(err)
s.False(merr.Ok(operateResp))
}
func (s *PrivilegeGroupTestSuite) TestGrantV2BuiltinPrivilegeGroup() {
ctx := GetContext(context.Background(), "root:123456")
roleName := "test_role"
createRoleResp, err := s.Cluster.Proxy.CreateRole(ctx, &milvuspb.CreateRoleRequest{
Entity: &milvuspb.RoleEntity{Name: roleName},
})
s.NoError(err)
s.True(merr.Ok(createRoleResp))
resp, _ := s.operatePrivilegeV2(ctx, roleName, "ClusterReadOnly", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "ClusterReadWrite", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "ClusterAdmin", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseReadOnly", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseReadWrite", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseAdmin", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionReadOnly", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionReadWrite", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionAdmin", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "ClusterAdmin", "db1", util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.False(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "ClusterAdmin", "db1", "col1", milvuspb.OperatePrivilegeType_Grant)
s.False(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "ClusterAdmin", util.AnyWord, "col1", milvuspb.OperatePrivilegeType_Grant)
s.False(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseAdmin", "db1", util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseAdmin", "db1", "col1", milvuspb.OperatePrivilegeType_Grant)
s.False(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "DatabaseAdmin", util.AnyWord, "col1", milvuspb.OperatePrivilegeType_Grant)
s.False(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionAdmin", "db1", util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionAdmin", "db1", "col1", milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, roleName, "CollectionAdmin", util.AnyWord, "col1", milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
}
func (s *PrivilegeGroupTestSuite) TestGrantV2CustomPrivilegeGroup() {
ctx := GetContext(context.Background(), "root:123456")
// Helper function to operate on privilege groups
@ -123,12 +231,14 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() {
})
s.NoError(err)
s.True(merr.Ok(createRoleResp))
s.operatePrivilege(ctx, role, "Insert", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 1)
resp, _ := s.operatePrivilegeV2(ctx, role, "Insert", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
s.validateGrants(ctx, role, commonpb.ObjectType_Global.String(), 1)
// grant group1 to role -> role: insert, group1(query, search)
s.operatePrivilege(ctx, role, "group1", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 2)
resp, _ = s.operatePrivilegeV2(ctx, role, "group1", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
s.validateGrants(ctx, role, commonpb.ObjectType_Global.String(), 2)
// create group2: query, delete
createResp2, err := s.Cluster.Proxy.CreatePrivilegeGroup(ctx, &milvuspb.CreatePrivilegeGroupRequest{
@ -144,8 +254,9 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() {
validatePrivilegeGroup("group2", 2)
// grant group2 to role -> role: insert, group1(query, search), group2(query, delete)
s.operatePrivilege(ctx, role, "group2", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Grant)
s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 3)
resp, _ = s.operatePrivilegeV2(ctx, role, "group2", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
s.validateGrants(ctx, role, commonpb.ObjectType_Global.String(), 3)
// add query, load to group1 -> group1: query, search, load -> role: insert, group1(query, search, load), group2(query, delete)
operatePrivilegeGroup("group1", milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup, []*milvuspb.PrivilegeEntity{
@ -153,14 +264,22 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() {
{Name: "Load"},
})
validatePrivilegeGroup("group1", 3)
s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 3)
s.validateGrants(ctx, role, commonpb.ObjectType_Global.String(), 3)
// add different object type privileges to group1 is not allowed
resp, _ = s.Cluster.Proxy.OperatePrivilegeGroup(ctx, &milvuspb.OperatePrivilegeGroupRequest{
GroupName: "group1",
Type: milvuspb.OperatePrivilegeGroupType_AddPrivilegesToGroup,
Privileges: []*milvuspb.PrivilegeEntity{{Name: "CreateCollection"}},
})
s.Error(merr.Error(resp))
// remove query from group1 -> group1: search, load -> role: insert, group1(search, load), group2(query, delete), role still have query privilege because of group2 granted.
operatePrivilegeGroup("group1", milvuspb.OperatePrivilegeGroupType_RemovePrivilegesFromGroup, []*milvuspb.PrivilegeEntity{
{Name: "Query"},
})
validatePrivilegeGroup("group1", 2)
s.validateGrants(ctx, role, commonpb.ObjectType_Collection.String(), 3)
s.validateGrants(ctx, role, commonpb.ObjectType_Global.String(), 3)
// Drop the group during any role usage will cause error
dropResp, _ := s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{
@ -169,9 +288,12 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() {
s.Error(merr.Error(dropResp))
// Revoke privilege group and privileges
s.operatePrivilege(ctx, role, "group1", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Revoke)
s.operatePrivilege(ctx, role, "group2", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Revoke)
s.operatePrivilege(ctx, role, "Insert", commonpb.ObjectType_Collection.String(), milvuspb.OperatePrivilegeType_Revoke)
resp, _ = s.operatePrivilegeV2(ctx, role, "group1", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, role, "group2", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, role, "Insert", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke)
s.True(merr.Ok(resp))
// Drop the privilege group after revoking the privilege will succeed
dropResp, err = s.Cluster.Proxy.DropPrivilegeGroup(ctx, &milvuspb.DropPrivilegeGroupRequest{
@ -187,9 +309,22 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() {
s.True(merr.Ok(dropResp))
// Validate the group was dropped
resp, err := s.Cluster.Proxy.ListPrivilegeGroups(ctx, &milvuspb.ListPrivilegeGroupsRequest{})
listResp, err := s.Cluster.Proxy.ListPrivilegeGroups(ctx, &milvuspb.ListPrivilegeGroupsRequest{})
s.NoError(err)
s.Equal(0, len(resp.PrivilegeGroups))
s.Equal(len(util.BuiltinPrivilegeGroups), len(listResp.PrivilegeGroups))
// validate edge cases
resp, _ = s.operatePrivilegeV2(ctx, role, util.AnyWord, util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, role, util.AnyWord, util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke)
s.True(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, role, "group3", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Revoke)
s.False(merr.Ok(resp))
resp, _ = s.operatePrivilegeV2(ctx, role, "%$", util.AnyWord, util.AnyWord, milvuspb.OperatePrivilegeType_Grant)
s.False(merr.Ok(resp))
// Drop the role
dropRoleResp, err := s.Cluster.Proxy.DropRole(ctx, &milvuspb.DropRoleRequest{
@ -199,42 +334,7 @@ func (s *PrivilegeGroupTestSuite) TestCustomPrivilegeGroup() {
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) {
func (s *PrivilegeGroupTestSuite) operatePrivilege(ctx context.Context, role, privilege, objectType string, operateType milvuspb.OperatePrivilegeType) (*commonpb.Status, error) {
resp, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{
Type: operateType,
Entity: &milvuspb.GrantEntity{
@ -248,8 +348,25 @@ func (s *PrivilegeGroupTestSuite) operatePrivilege(ctx context.Context, role, pr
},
},
})
s.NoError(err)
s.True(merr.Ok(resp))
return resp, err
}
func (s *PrivilegeGroupTestSuite) operatePrivilegeV2(ctx context.Context, role, privilege, dbName, collectionName string, operateType milvuspb.OperatePrivilegeType) (*commonpb.Status, error) {
resp, err := s.Cluster.Proxy.OperatePrivilege(ctx, &milvuspb.OperatePrivilegeRequest{
Type: operateType,
Entity: &milvuspb.GrantEntity{
Role: &milvuspb.RoleEntity{Name: role},
Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()},
ObjectName: collectionName,
DbName: dbName,
Grantor: &milvuspb.GrantorEntity{
User: &milvuspb.UserEntity{Name: util.UserRoot},
Privilege: &milvuspb.PrivilegeEntity{Name: privilege},
},
},
Version: "v2",
})
return resp, err
}
func (s *PrivilegeGroupTestSuite) validateGrants(ctx context.Context, roleName, objectType string, expectedCount int) {
@ -261,7 +378,6 @@ func (s *PrivilegeGroupTestSuite) validateGrants(ctx context.Context, roleName,
DbName: util.AnyWord,
},
})
fmt.Println("!!!validateGrants: ", resp)
s.NoError(err)
s.True(merr.Ok(resp.GetStatus()))
s.Len(resp.GetEntities(), expectedCount)

View File

@ -78,11 +78,15 @@ func (s *RBACBackupTestSuite) TestBackup() {
Type: operateType,
Entity: &milvuspb.GrantEntity{
Role: &milvuspb.RoleEntity{Name: role},
Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Collection.String()},
ObjectName: objectName,
Object: &milvuspb.ObjectEntity{Name: commonpb.ObjectType_Global.String()},
ObjectName: collectionName,
DbName: dbName,
Grantor: &milvuspb.GrantorEntity{User: &milvuspb.UserEntity{Name: util.UserRoot}, Privilege: &milvuspb.PrivilegeEntity{Name: privilege}},
Grantor: &milvuspb.GrantorEntity{
User: &milvuspb.UserEntity{Name: util.UserRoot},
Privilege: &milvuspb.PrivilegeEntity{Name: privilege},
},
},
Version: "v2",
})
s.NoError(err)
s.True(merr.Ok(resp))