diff --git a/configs/milvus.yaml b/configs/milvus.yaml index 7023f7f04fc25..7d28d9dd1a9b3 100644 --- a/configs/milvus.yaml +++ b/configs/milvus.yaml @@ -830,6 +830,7 @@ common: # like the old password verification when updating the credential superUsers: defaultRootPassword: "Milvus" # default password for root user. The maximum length is 72 characters, and double quotes are required. + rootShouldBindRole: false # Whether the root user should bind a role when the authorization is enabled. rbac: overrideBuiltInPrivilegeGroups: enabled: false # Whether to override build-in privilege groups diff --git a/internal/distributed/proxy/httpserver/handler_v1_test.go b/internal/distributed/proxy/httpserver/handler_v1_test.go index ef7d206c320d1..00d7ee7a3fef3 100644 --- a/internal/distributed/proxy/httpserver/handler_v1_test.go +++ b/internal/distributed/proxy/httpserver/handler_v1_test.go @@ -143,8 +143,11 @@ func initHTTPServer(proxy types.ProxyComponent, needAuth bool) *gin.Engine { ----|----------------|----------------|---------------- */ func genAuthMiddleWare(needAuth bool) gin.HandlerFunc { + InitMockGlobalMetaCache() + proxy.AddRootUserToAdminRole() if needAuth { return func(c *gin.Context) { + // proxy.RemoveRootUserFromAdminRole() c.Set(ContextUsername, "") username, password, ok := ParseUsernamePassword(c) if !ok { @@ -161,6 +164,10 @@ func genAuthMiddleWare(needAuth bool) gin.HandlerFunc { } } +func InitMockGlobalMetaCache() { + proxy.InitEmptyGlobalCache() +} + func Print(code int32, message string) string { return fmt.Sprintf("{\"%s\":%d,\"%s\":\"%s\"}", HTTPReturnCode, code, HTTPReturnMessage, message) } diff --git a/internal/distributed/proxy/httpserver/handler_v2_test.go b/internal/distributed/proxy/httpserver/handler_v2_test.go index 44eeb6cb5640b..891284f62b734 100644 --- a/internal/distributed/proxy/httpserver/handler_v2_test.go +++ b/internal/distributed/proxy/httpserver/handler_v2_test.go @@ -350,8 +350,8 @@ func TestGrpcWrapper(t *testing.T) { assert.Equal(t, http.StatusForbidden, w.Code) err = json.Unmarshal(w.Body.Bytes(), returnBody) assert.Nil(t, err) - assert.Equal(t, int32(2), returnBody.Code) - assert.Equal(t, "service unavailable: internal: Milvus Proxy is not ready yet. please wait", returnBody.Message) + assert.Equal(t, int32(65535), returnBody.Code) + assert.Equal(t, "rpc error: code = PermissionDenied desc = PrivilegeLoad: permission deny to test in the `default` database", returnBody.Message) fmt.Println(w.Body.String()) }) } diff --git a/internal/proxy/meta_cache_testonly.go b/internal/proxy/meta_cache_testonly.go new file mode 100644 index 0000000000000..3ef06ad3fa485 --- /dev/null +++ b/internal/proxy/meta_cache_testonly.go @@ -0,0 +1,63 @@ +//go:build test +// +build test + +/* + * 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 proxy + +import ( + "github.com/cockroachdb/errors" + "github.com/stretchr/testify/mock" + + "github.com/milvus-io/milvus/internal/mocks" + "github.com/milvus-io/milvus/pkg/common" + "github.com/milvus-io/milvus/pkg/util/funcutil" + "github.com/milvus-io/milvus/pkg/util/typeutil" +) + +func AddRootUserToAdminRole() { + err := globalMetaCache.RefreshPolicyInfo(typeutil.CacheOp{OpType: typeutil.CacheAddUserToRole, OpKey: funcutil.EncodeUserRoleCache("root", "admin")}) + if err != nil { + panic(err) + } +} + +func RemoveRootUserFromAdminRole() { + err := globalMetaCache.RefreshPolicyInfo(typeutil.CacheOp{OpType: typeutil.CacheRemoveUserFromRole, OpKey: funcutil.EncodeUserRoleCache("root", "admin")}) + if err != nil { + panic(err) + } +} + +func InitEmptyGlobalCache() { + var err error + emptyMock := common.NewEmptyMockT() + rootcoord := mocks.NewMockRootCoordClient(emptyMock) + rootcoord.EXPECT().DescribeCollection(mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("collection not found")) + querycoord := mocks.NewMockQueryCoordClient(emptyMock) + mgr := newShardClientMgr() + globalMetaCache, err = NewMetaCache(rootcoord, querycoord, mgr) + if err != nil { + panic(err) + } +} + +func SetGlobalMetaCache(metaCache *MetaCache) { + globalMetaCache = metaCache +} diff --git a/internal/proxy/privilege_interceptor.go b/internal/proxy/privilege_interceptor.go index 9656cf244c9b6..fc358d3baf8ac 100644 --- a/internal/proxy/privilege_interceptor.go +++ b/internal/proxy/privilege_interceptor.go @@ -129,7 +129,7 @@ func PrivilegeInterceptor(ctx context.Context, req interface{}) (context.Context log.Warn("GetCurUserFromContext fail", zap.Error(err)) return ctx, err } - if username == util.UserRoot { + if !Params.CommonCfg.RootShouldBindRole.GetAsBool() && username == util.UserRoot { return ctx, nil } roleNames, err := GetRole(username) diff --git a/internal/proxy/privilege_interceptor_test.go b/internal/proxy/privilege_interceptor_test.go index 272864e195e7d..322a661d8cfd4 100644 --- a/internal/proxy/privilege_interceptor_test.go +++ b/internal/proxy/privilege_interceptor_test.go @@ -174,6 +174,40 @@ func TestPrivilegeInterceptor(t *testing.T) { }) } +func TestRootShouldBindRole(t *testing.T) { + paramtable.Init() + Params.Save(Params.CommonCfg.AuthorizationEnabled.Key, "true") + defer Params.Reset(Params.CommonCfg.AuthorizationEnabled.Key) + rootCtx := GetContext(context.Background(), "root:Milvus") + t.Run("not bind role", func(t *testing.T) { + Params.Save(Params.CommonCfg.RootShouldBindRole.Key, "false") + defer Params.Reset(Params.CommonCfg.RootShouldBindRole.Key) + + InitEmptyGlobalCache() + _, err := PrivilegeInterceptor(rootCtx, &milvuspb.LoadCollectionRequest{ + CollectionName: "col1", + }) + assert.NoError(t, err) + }) + + t.Run("bind role", func(t *testing.T) { + Params.Save(Params.CommonCfg.RootShouldBindRole.Key, "true") + defer Params.Reset(Params.CommonCfg.RootShouldBindRole.Key) + + InitEmptyGlobalCache() + _, err := PrivilegeInterceptor(rootCtx, &milvuspb.LoadCollectionRequest{ + CollectionName: "col1", + }) + assert.Error(t, err) + + AddRootUserToAdminRole() + _, err = PrivilegeInterceptor(rootCtx, &milvuspb.LoadCollectionRequest{ + CollectionName: "col1", + }) + assert.NoError(t, err) + }) +} + func TestResourceGroupPrivilege(t *testing.T) { ctx := context.Background() diff --git a/internal/rootcoord/root_coord.go b/internal/rootcoord/root_coord.go index ec8fd93ac8628..e00909b8a8c65 100644 --- a/internal/rootcoord/root_coord.go +++ b/internal/rootcoord/root_coord.go @@ -578,7 +578,11 @@ func (c *Core) initCredentials(initCtx context.Context) error { } log.Ctx(initCtx).Info("RootCoord init user root") err = c.meta.AddCredential(initCtx, &internalpb.CredentialInfo{Username: util.UserRoot, EncryptedPassword: encryptedRootPassword}) - return err + if err != nil { + log.Ctx(initCtx).Warn("RootCoord init user root failed", zap.Error(err)) + return err + } + return nil } return nil } diff --git a/pkg/common/mock_testonly.go b/pkg/common/mock_testonly.go new file mode 100644 index 0000000000000..f039578b0dba3 --- /dev/null +++ b/pkg/common/mock_testonly.go @@ -0,0 +1,52 @@ +//go:build test + +/* + * 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 common + +import ( + "context" + "fmt" + + "github.com/milvus-io/milvus/pkg/log" +) + +type MockTestingT struct { + ctx context.Context +} + +func NewEmptyMockT() *MockTestingT { + return &MockTestingT{ + ctx: context.Background(), + } +} + +func (m *MockTestingT) Logf(format string, args ...interface{}) { + log.Ctx(m.ctx).Info(fmt.Sprintf(format, args...)) +} + +func (m *MockTestingT) Errorf(format string, args ...interface{}) { + log.Ctx(m.ctx).Error(fmt.Sprintf(format, args...)) +} + +func (m *MockTestingT) FailNow() { + log.Ctx(m.ctx).Panic("FailNow called") +} + +func (m *MockTestingT) Cleanup(func()) {} diff --git a/pkg/util/paramtable/component_param.go b/pkg/util/paramtable/component_param.go index 712668a9ca6c8..dea773b45b71f 100644 --- a/pkg/util/paramtable/component_param.go +++ b/pkg/util/paramtable/component_param.go @@ -239,6 +239,7 @@ type commonConfig struct { AuthorizationEnabled ParamItem `refreshable:"false"` SuperUsers ParamItem `refreshable:"true"` DefaultRootPassword ParamItem `refreshable:"false"` + RootShouldBindRole ParamItem `refreshable:"true"` ClusterName ParamItem `refreshable:"false"` @@ -669,6 +670,15 @@ like the old password verification when updating the credential`, } p.DefaultRootPassword.Init(base.mgr) + p.RootShouldBindRole = ParamItem{ + Key: "common.security.rootShouldBindRole", + Version: "2.5.4", + Doc: "Whether the root user should bind a role when the authorization is enabled.", + DefaultValue: "false", + Export: true, + } + p.RootShouldBindRole.Init(base.mgr) + p.ClusterName = ParamItem{ Key: "common.cluster.name", Version: "2.0.0",