diff --git a/client/Makefile b/client/Makefile index 71af63269c..c43227c809 100644 --- a/client/Makefile +++ b/client/Makefile @@ -30,4 +30,4 @@ unittest: generate-mockery: @echo "Generating mockery Milvus service server" - @../bin/mockery --srcpkg=github.com/milvus-io/milvus-proto/go-api/v2/milvuspb --name=MilvusServiceServer --filename=mock_milvus_server_test.go --output=milvusclient --outpkg=milvusclient --with-expecter + @../bin/mockery --srcpkg=github.com/milvus-io/milvus-proto/go-api/v3/milvuspb --name=MilvusServiceServer --filename=mock_milvus_server_test.go --output=milvusclient --outpkg=milvusclient --with-expecter diff --git a/client/entity/rbac.go b/client/entity/rbac.go index 5118a7542c..ae3128bc8d 100644 --- a/client/entity/rbac.go +++ b/client/entity/rbac.go @@ -17,8 +17,9 @@ package entity type User struct { - UserName string - Roles []string + UserName string + Roles []string + Description string } type Role struct { diff --git a/client/go.mod b/client/go.mod index 23bea7f090..075b3c0f9b 100644 --- a/client/go.mod +++ b/client/go.mod @@ -7,7 +7,7 @@ require ( github.com/cockroachdb/errors v1.9.1 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 - github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260526093827-489331a5a41f + github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e github.com/milvus-io/milvus/pkg/v3 v3.0.0-beta github.com/quasilyte/go-ruleguard/dsl v0.3.23 github.com/samber/lo v1.52.0 diff --git a/client/go.sum b/client/go.sum index 05c25b43a1..66c3894107 100644 --- a/client/go.sum +++ b/client/go.sum @@ -222,8 +222,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= -github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260526093827-489331a5a41f h1:W+paUzc+AA381Rydx2DJffoQuM4l+efVqO+2i9GdfMw= -github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260526093827-489331a5a41f/go.mod h1:rbKpv5JToISTKTTLl0duL5r6wbYnjJ9SsD0QgXMzKy0= +github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e h1:axPnOhjsWqqxk0dOQRSIU2mdg52Kyl/8E6iDFZy/IpE= +github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e/go.mod h1:rbKpv5JToISTKTTLl0duL5r6wbYnjJ9SsD0QgXMzKy0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/client/milvusclient/mock_milvus_server_test.go b/client/milvusclient/mock_milvus_server_test.go index cb8757fe00..0cc48af27d 100644 --- a/client/milvusclient/mock_milvus_server_test.go +++ b/client/milvusclient/mock_milvus_server_test.go @@ -6,11 +6,8 @@ import ( context "context" commonpb "github.com/milvus-io/milvus-proto/go-api/v3/commonpb" - federpb "github.com/milvus-io/milvus-proto/go-api/v3/federpb" - milvuspb "github.com/milvus-io/milvus-proto/go-api/v3/milvuspb" - mock "github.com/stretchr/testify/mock" ) @@ -794,6 +791,65 @@ func (_c *MilvusServiceServer_AlterIndex_Call) RunAndReturn(run func(context.Con return _c } +// AlterRole provides a mock function with given fields: _a0, _a1 +func (_m *MilvusServiceServer) AlterRole(_a0 context.Context, _a1 *milvuspb.AlterRoleRequest) (*commonpb.Status, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for AlterRole") + } + + var r0 *commonpb.Status + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.AlterRoleRequest) (*commonpb.Status, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.AlterRoleRequest) *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.AlterRoleRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MilvusServiceServer_AlterRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AlterRole' +type MilvusServiceServer_AlterRole_Call struct { + *mock.Call +} + +// AlterRole is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.AlterRoleRequest +func (_e *MilvusServiceServer_Expecter) AlterRole(_a0 interface{}, _a1 interface{}) *MilvusServiceServer_AlterRole_Call { + return &MilvusServiceServer_AlterRole_Call{Call: _e.mock.On("AlterRole", _a0, _a1)} +} + +func (_c *MilvusServiceServer_AlterRole_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.AlterRoleRequest)) *MilvusServiceServer_AlterRole_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.AlterRoleRequest)) + }) + return _c +} + +func (_c *MilvusServiceServer_AlterRole_Call) Return(_a0 *commonpb.Status, _a1 error) *MilvusServiceServer_AlterRole_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MilvusServiceServer_AlterRole_Call) RunAndReturn(run func(context.Context, *milvuspb.AlterRoleRequest) (*commonpb.Status, error)) *MilvusServiceServer_AlterRole_Call { + _c.Call.Return(run) + return _c +} + // BackupRBAC provides a mock function with given fields: _a0, _a1 func (_m *MilvusServiceServer) BackupRBAC(_a0 context.Context, _a1 *milvuspb.BackupRBACMetaRequest) (*milvuspb.BackupRBACMetaResponse, error) { ret := _m.Called(_a0, _a1) @@ -3247,6 +3303,65 @@ func (_c *MilvusServiceServer_DumpMessages_Call) RunAndReturn(run func(*milvuspb return _c } +// ExportSnapshot provides a mock function with given fields: _a0, _a1 +func (_m *MilvusServiceServer) ExportSnapshot(_a0 context.Context, _a1 *milvuspb.ExportSnapshotRequest) (*milvuspb.ExportSnapshotResponse, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ExportSnapshot") + } + + var r0 *milvuspb.ExportSnapshotResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.ExportSnapshotRequest) (*milvuspb.ExportSnapshotResponse, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.ExportSnapshotRequest) *milvuspb.ExportSnapshotResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*milvuspb.ExportSnapshotResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.ExportSnapshotRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MilvusServiceServer_ExportSnapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExportSnapshot' +type MilvusServiceServer_ExportSnapshot_Call struct { + *mock.Call +} + +// ExportSnapshot is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.ExportSnapshotRequest +func (_e *MilvusServiceServer_Expecter) ExportSnapshot(_a0 interface{}, _a1 interface{}) *MilvusServiceServer_ExportSnapshot_Call { + return &MilvusServiceServer_ExportSnapshot_Call{Call: _e.mock.On("ExportSnapshot", _a0, _a1)} +} + +func (_c *MilvusServiceServer_ExportSnapshot_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.ExportSnapshotRequest)) *MilvusServiceServer_ExportSnapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.ExportSnapshotRequest)) + }) + return _c +} + +func (_c *MilvusServiceServer_ExportSnapshot_Call) Return(_a0 *milvuspb.ExportSnapshotResponse, _a1 error) *MilvusServiceServer_ExportSnapshot_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MilvusServiceServer_ExportSnapshot_Call) RunAndReturn(run func(context.Context, *milvuspb.ExportSnapshotRequest) (*milvuspb.ExportSnapshotResponse, error)) *MilvusServiceServer_ExportSnapshot_Call { + _c.Call.Return(run) + return _c +} + // Flush provides a mock function with given fields: _a0, _a1 func (_m *MilvusServiceServer) Flush(_a0 context.Context, _a1 *milvuspb.FlushRequest) (*milvuspb.FlushResponse, error) { ret := _m.Called(_a0, _a1) @@ -6787,6 +6902,65 @@ func (_c *MilvusServiceServer_ReplicateMessage_Call) RunAndReturn(run func(conte return _c } +// RestoreExternalSnapshot provides a mock function with given fields: _a0, _a1 +func (_m *MilvusServiceServer) RestoreExternalSnapshot(_a0 context.Context, _a1 *milvuspb.RestoreExternalSnapshotRequest) (*milvuspb.RestoreExternalSnapshotResponse, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for RestoreExternalSnapshot") + } + + var r0 *milvuspb.RestoreExternalSnapshotResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.RestoreExternalSnapshotRequest) (*milvuspb.RestoreExternalSnapshotResponse, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.RestoreExternalSnapshotRequest) *milvuspb.RestoreExternalSnapshotResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*milvuspb.RestoreExternalSnapshotResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.RestoreExternalSnapshotRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MilvusServiceServer_RestoreExternalSnapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RestoreExternalSnapshot' +type MilvusServiceServer_RestoreExternalSnapshot_Call struct { + *mock.Call +} + +// RestoreExternalSnapshot is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.RestoreExternalSnapshotRequest +func (_e *MilvusServiceServer_Expecter) RestoreExternalSnapshot(_a0 interface{}, _a1 interface{}) *MilvusServiceServer_RestoreExternalSnapshot_Call { + return &MilvusServiceServer_RestoreExternalSnapshot_Call{Call: _e.mock.On("RestoreExternalSnapshot", _a0, _a1)} +} + +func (_c *MilvusServiceServer_RestoreExternalSnapshot_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.RestoreExternalSnapshotRequest)) *MilvusServiceServer_RestoreExternalSnapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.RestoreExternalSnapshotRequest)) + }) + return _c +} + +func (_c *MilvusServiceServer_RestoreExternalSnapshot_Call) Return(_a0 *milvuspb.RestoreExternalSnapshotResponse, _a1 error) *MilvusServiceServer_RestoreExternalSnapshot_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MilvusServiceServer_RestoreExternalSnapshot_Call) RunAndReturn(run func(context.Context, *milvuspb.RestoreExternalSnapshotRequest) (*milvuspb.RestoreExternalSnapshotResponse, error)) *MilvusServiceServer_RestoreExternalSnapshot_Call { + _c.Call.Return(run) + return _c +} + // RestoreRBAC provides a mock function with given fields: _a0, _a1 func (_m *MilvusServiceServer) RestoreRBAC(_a0 context.Context, _a1 *milvuspb.RestoreRBACMetaRequest) (*commonpb.Status, error) { ret := _m.Called(_a0, _a1) diff --git a/client/milvusclient/rbac.go b/client/milvusclient/rbac.go index 14d4f0de4b..6a1dd6c487 100644 --- a/client/milvusclient/rbac.go +++ b/client/milvusclient/rbac.go @@ -53,8 +53,9 @@ func (c *Client) DescribeUser(ctx context.Context, opt DescribeUserOption, callO } result := resp.GetResults()[0] user = &entity.User{ - UserName: result.GetUser().GetName(), - Roles: lo.Map(result.GetRoles(), func(r *milvuspb.RoleEntity, _ int) string { return r.GetName() }), + UserName: result.GetUser().GetName(), + Roles: lo.Map(result.GetRoles(), func(r *milvuspb.RoleEntity, _ int) string { return r.GetName() }), + Description: result.GetDescription(), } return nil diff --git a/client/milvusclient/rbac_options.go b/client/milvusclient/rbac_options.go index 4d877110fe..5f2d8ceb80 100644 --- a/client/milvusclient/rbac_options.go +++ b/client/milvusclient/rbac_options.go @@ -67,14 +67,16 @@ type CreateUserOption interface { } type createUserOption struct { - userName string - password string + userName string + password string + description *string } func (opt *createUserOption) Request() *milvuspb.CreateCredentialRequest { return &milvuspb.CreateCredentialRequest{ - Username: opt.userName, - Password: crypto.Base64Encode(opt.password), + Username: opt.userName, + Password: crypto.Base64Encode(opt.password), + Description: opt.description, } } @@ -85,6 +87,11 @@ func NewCreateUserOption(userName, password string) *createUserOption { } } +func (opt *createUserOption) WithDescription(description string) *createUserOption { + opt.description = &description + return opt +} + type UpdatePasswordOption interface { Request() *milvuspb.UpdateCredentialRequest } @@ -93,6 +100,7 @@ type updatePasswordOption struct { userName string oldPassword string newPassword string + description *string } func (opt *updatePasswordOption) Request() *milvuspb.UpdateCredentialRequest { @@ -100,6 +108,7 @@ func (opt *updatePasswordOption) Request() *milvuspb.UpdateCredentialRequest { Username: opt.userName, OldPassword: crypto.Base64Encode(opt.oldPassword), NewPassword: crypto.Base64Encode(opt.newPassword), + Description: opt.description, } } @@ -111,6 +120,11 @@ func NewUpdatePasswordOption(userName, oldPassword, newPassword string) *updateP } } +func (opt *updatePasswordOption) WithDescription(description string) *updatePasswordOption { + opt.description = &description + return opt +} + type DropUserOption interface { Request() *milvuspb.DeleteCredentialRequest } diff --git a/client/milvusclient/rbac_test.go b/client/milvusclient/rbac_test.go index 6a77aac853..ed7ca03250 100644 --- a/client/milvusclient/rbac_test.go +++ b/client/milvusclient/rbac_test.go @@ -65,12 +65,14 @@ func (s *UserSuite) TestDescribeUser() { userName := fmt.Sprintf("user_%s", s.randString(5)) s.Run("success", func() { + description := "go sdk user description" s.mock.EXPECT().SelectUser(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, r *milvuspb.SelectUserRequest) (*milvuspb.SelectUserResponse, error) { s.Equal(userName, r.GetUser().GetName()) return &milvuspb.SelectUserResponse{ Results: []*milvuspb.UserResult{ { - User: &milvuspb.UserEntity{Name: userName}, + User: &milvuspb.UserEntity{Name: userName}, + Description: description, Roles: []*milvuspb.RoleEntity{ {Name: "role1"}, {Name: "role2"}, @@ -84,6 +86,7 @@ func (s *UserSuite) TestDescribeUser() { s.NoError(err) s.Equal(userName, user.UserName) s.Equal([]string{"role1", "role2"}, user.Roles) + s.Equal(description, user.Description) }) s.Run("failure", func() { @@ -101,13 +104,15 @@ func (s *UserSuite) TestCreateUser() { s.Run("success", func() { userName := fmt.Sprintf("user_%s", s.randString(5)) password := s.randString(12) + description := "go sdk create description" s.mock.EXPECT().CreateCredential(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, ccr *milvuspb.CreateCredentialRequest) (*commonpb.Status, error) { s.Equal(userName, ccr.GetUsername()) s.Equal(crypto.Base64Encode(password), ccr.GetPassword()) + s.Equal(description, ccr.GetDescription()) return merr.Success(), nil }).Once() - err := s.client.CreateUser(ctx, NewCreateUserOption(userName, password)) + err := s.client.CreateUser(ctx, NewCreateUserOption(userName, password).WithDescription(description)) s.NoError(err) }) } @@ -120,14 +125,31 @@ func (s *UserSuite) TestUpdatePassword() { userName := fmt.Sprintf("user_%s", s.randString(5)) oldPassword := s.randString(12) newPassword := s.randString(12) + description := "go sdk update description" s.mock.EXPECT().UpdateCredential(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, ucr *milvuspb.UpdateCredentialRequest) (*commonpb.Status, error) { s.Equal(userName, ucr.GetUsername()) s.Equal(crypto.Base64Encode(oldPassword), ucr.GetOldPassword()) s.Equal(crypto.Base64Encode(newPassword), ucr.GetNewPassword()) + s.Equal(description, ucr.GetDescription()) return merr.Success(), nil }).Once() - err := s.client.UpdatePassword(ctx, NewUpdatePasswordOption(userName, oldPassword, newPassword)) + err := s.client.UpdatePassword(ctx, NewUpdatePasswordOption(userName, oldPassword, newPassword).WithDescription(description)) + s.NoError(err) + }) + + s.Run("description only", func() { + userName := fmt.Sprintf("user_%s", s.randString(5)) + description := "go sdk description only update" + s.mock.EXPECT().UpdateCredential(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, ucr *milvuspb.UpdateCredentialRequest) (*commonpb.Status, error) { + s.Equal(userName, ucr.GetUsername()) + s.Empty(ucr.GetOldPassword()) + s.Empty(ucr.GetNewPassword()) + s.Equal(description, ucr.GetDescription()) + return merr.Success(), nil + }).Once() + + err := s.client.UpdatePassword(ctx, NewUpdatePasswordOption(userName, "", "").WithDescription(description)) s.NoError(err) }) diff --git a/docs/design-docs/design_docs/20260601-rbac-user-description.md b/docs/design-docs/design_docs/20260601-rbac-user-description.md new file mode 100644 index 0000000000..02a11127c6 --- /dev/null +++ b/docs/design-docs/design_docs/20260601-rbac-user-description.md @@ -0,0 +1,173 @@ +# MEP: RBAC User Description + +- **Created:** 2026-06-01 +- **Author(s):** @shaoting-huang +- **Status:** Under Review +- **Component:** Proxy | Coordinator +- **Related Issues:** #50179 +- **Released:** Milvus release version, if applicable + +## Summary + +Milvus RBAC users can carry an optional human-readable description. The field is +accepted on user creation, returned by user read APIs, and can be edited through +the existing credential update API without requiring a password change. + +**GitHub Issue**: https://github.com/milvus-io/milvus/issues/50179 + +## Motivation + +RBAC users are currently identified only by username and role bindings. Operators +need a lightweight place to record who owns a user, what integration it belongs +to, or why it exists. The description must be editable without rotating the +password, and password rotation must not erase the description. + +## Goals + +- Persist a user description together with credential metadata. +- Return the description from user describe and select flows, including when + role information is not requested. +- Allow description-only updates through `UpdateCredential`. +- Preserve the existing password when only the description changes. +- Preserve the existing description when only the password changes. +- Avoid invalidating or blanking proxy authentication cache entries during + description-only updates. + +## Non-Goals + +- Add a new RPC for user description edits. +- Change RBAC authorization semantics. A caller with `PrivilegeUpdateUser` can + update a description without knowing the target user's password. +- Add user descriptions to RBAC backup and restore. The current proto dependency + does not add description to `UserInfo`. + +## Public Interfaces + +The milvus-proto dependency adds optional description fields to the existing +credential requests and user read result: + +```protobuf +message CreateCredentialRequest { + optional string description = 6; +} + +message UpdateCredentialRequest { + optional string description = 7; +} + +message UserResult { + UserEntity user = 1; + repeated RoleEntity roles = 2; + string description = 3; +} +``` + +Milvus internal credential messages add the same optional field so the WAL body +can distinguish "field not provided" from "set description to empty string": + +```protobuf +message CredentialInfo { + string username = 1; + string encrypted_password = 2; + string sha256_password = 5; + uint64 time_tick = 6; +optional string description = 7; +} +``` + +`proxy.maxUserDescriptionLength` limits the byte length of the description. The +default is 1024 bytes. + +## Design Details + +### Data Flow + +#### Create User + +1. Proxy validates username, password, and description length. +2. Proxy encrypts the password, computes the SHA256 cache value, and sends + `CredentialInfo` with `description` to RootCoord. +3. RootCoord broadcasts an alter-user WAL message. +4. The WAL ack callback writes the credential metadata and updates proxy auth + caches because a password is present. + +#### Update Password + +1. Proxy enters the password update path only when `new_password` is provided. +2. Proxy decodes and validates the old and new passwords, verifies the old + password unless the caller is a configured super user, then sends the new + encrypted password and SHA256 cache value. +3. RootCoord performs a read-modify-write merge. If the incoming message does + not carry `description`, the existing description is preserved. +4. The WAL ack callback updates proxy auth caches because the body carries a + non-empty SHA256 password. + +#### Update Description Only + +1. Proxy validates description length and skips the password block because + `new_password` is absent. +2. Proxy sends `CredentialInfo` with `description` and no password fields. +3. RootCoord merges the incoming metadata with the existing credential. Because + the incoming encrypted password is empty, the existing encrypted password is + preserved. +4. The WAL ack callback skips proxy auth cache updates because no SHA256 + password is present. + +#### Read User + +`Catalog.getUserResult` loads the credential before the role-info early return +and copies `Credential.Description` into `UserResult.Description`. This makes +both `include_role_info=true` and `include_role_info=false` return the field. + +## Storage Model + +`model.Credential` stores `Description` in the same JSON payload as the encrypted +password and timetick. `Sha256Password` remains cache-only and is not persisted +by the RootCoord merge path. + +Proxy enforces the description length before the request reaches RootCoord, so an +oversized description is rejected before it can increase the etcd credential +value. RootCoord keeps the existing credential validation shape and does not +duplicate proxy-side username, password, or description length checks. + +HTTP v2 user create and update requests pass the optional description through to +the same gRPC credential APIs. HTTP v2 user describe preserves the existing role +list response and includes the description in the response object. + +## Compatibility, Deprecation, and Migration Plan + +The description field is optional. Existing credential records unmarshal with an +empty description. Existing clients that do not send descriptions continue to +create and update credentials as before. + +This Milvus PR temporarily uses a git-based `replace` for the milvus-proto +branch that defines the API fields. During coordinated landing, the replace is +removed and the dependency is switched to the upstream milvus-proto version that +contains those fields. + +No deprecation or data migration is required. + +## Test Plan + +- Proxy unit tests cover create with description, description length rejection, + description-only update, password-plus-description update, empty update + rejection, and empty-string description clearing. +- RootCoord unit tests cover read-modify-write preservation and proxy auth cache + behavior, including malformed password updates. +- Metastore tests cover credential marshal/unmarshal and user result readback. +- HTTP v2 tests cover create, description-only update, and describe response + propagation. +- CI runs package tests, static check, and Go-only builds against the coordinated + milvus-proto dependency. + +## Rejected Alternatives + +- Add a new update-description RPC. Rejected because the existing + `UpdateCredential` API already owns credential metadata updates and can model + password and description as independently optional fields. +- Store descriptions in a separate metadata key. Rejected because credentials + already have a versioned WAL update path and catalog record. A separate key + would add another consistency edge without reducing update complexity. +- Update proxy auth caches on every credential metadata edit. Rejected because a + description-only update carries no SHA256 password and would blank cached + authentication state. diff --git a/go.mod b/go.mod index a5dcb4256c..8c44ac90aa 100644 --- a/go.mod +++ b/go.mod @@ -69,8 +69,7 @@ require ( github.com/hamba/avro/v2 v2.29.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/magiconair/properties v1.8.7 - github.com/milvus-io/milvus-proto/go-api/v2 v2.6.18 - github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260526093827-489331a5a41f + github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e github.com/milvus-io/milvus/client/v2 v2.6.2 github.com/milvus-io/milvus/pkg/v3 v3.0.0-beta github.com/shirou/gopsutil/v4 v4.25.10 diff --git a/go.sum b/go.sum index 40fcd307fa..b996d92d8f 100644 --- a/go.sum +++ b/go.sum @@ -795,10 +795,8 @@ github.com/milvus-io/cgosymbolizer v0.0.0-20250318084424-114f4050c3a6 h1:YHMFI6L github.com/milvus-io/cgosymbolizer v0.0.0-20250318084424-114f4050c3a6/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.6.18 h1:4EaGTuDn0yv9BtQ23l2Wube7d8XcLA4+z2sQsgRkYK0= -github.com/milvus-io/milvus-proto/go-api/v2 v2.6.18/go.mod h1:/6UT4zZl6awVeXLeE7UGDWZvXj3IWkRsh3mqsn0DiAs= -github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260526093827-489331a5a41f h1:W+paUzc+AA381Rydx2DJffoQuM4l+efVqO+2i9GdfMw= -github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260526093827-489331a5a41f/go.mod h1:rbKpv5JToISTKTTLl0duL5r6wbYnjJ9SsD0QgXMzKy0= +github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e h1:axPnOhjsWqqxk0dOQRSIU2mdg52Kyl/8E6iDFZy/IpE= +github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e/go.mod h1:rbKpv5JToISTKTTLl0duL5r6wbYnjJ9SsD0QgXMzKy0= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= diff --git a/internal/distributed/proxy/httpserver/handler_v2.go b/internal/distributed/proxy/httpserver/handler_v2.go index 95d116c737..ec510ca038 100644 --- a/internal/distributed/proxy/httpserver/handler_v2.go +++ b/internal/distributed/proxy/httpserver/handler_v2.go @@ -2663,14 +2663,18 @@ func (h *HandlersV2) describeUser(ctx context.Context, c *gin.Context, anyReq an }) if err == nil { roleNames := []string{} + description := "" for _, userRole := range resp.(*milvuspb.SelectUserResponse).Results { if userRole.User.Name == userName { + description = userRole.GetDescription() for _, role := range userRole.Roles { roleNames = append(roleNames, role.Name) } } } - HTTPReturn(c, http.StatusOK, wrapperReturnList(roleNames)) + result := wrapperReturnList(roleNames) + result[HTTPReturnDescription] = description + HTTPReturn(c, http.StatusOK, result) } return resp, err } @@ -2678,8 +2682,9 @@ func (h *HandlersV2) describeUser(ctx context.Context, c *gin.Context, anyReq an func (h *HandlersV2) createUser(ctx context.Context, c *gin.Context, anyReq any, dbName string) (interface{}, error) { httpReq := anyReq.(*PasswordReq) req := &milvuspb.CreateCredentialRequest{ - Username: httpReq.UserName, - Password: crypto.Base64Encode(httpReq.Password), + Username: httpReq.UserName, + Password: crypto.Base64Encode(httpReq.Password), + Description: httpReq.Description, } resp, err := wrapperProxy(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/CreateCredential", func(reqCtx context.Context, req any) (interface{}, error) { return h.proxy.CreateCredential(reqCtx, req.(*milvuspb.CreateCredentialRequest)) @@ -2694,8 +2699,11 @@ func (h *HandlersV2) updateUser(ctx context.Context, c *gin.Context, anyReq any, httpReq := anyReq.(*NewPasswordReq) req := &milvuspb.UpdateCredentialRequest{ Username: httpReq.UserName, - OldPassword: crypto.Base64Encode(httpReq.Password), - NewPassword: crypto.Base64Encode(httpReq.NewPassword), + Description: httpReq.Description, + } + if httpReq.NewPassword != "" { + req.OldPassword = crypto.Base64Encode(httpReq.Password) + req.NewPassword = crypto.Base64Encode(httpReq.NewPassword) } resp, err := wrapperProxy(ctx, c, req, h.checkAuth, false, "/milvus.proto.milvus.MilvusService/UpdateCredential", func(reqCtx context.Context, req any) (interface{}, error) { return h.proxy.UpdateCredential(reqCtx, req.(*milvuspb.UpdateCredentialRequest)) diff --git a/internal/distributed/proxy/httpserver/handler_v2_test.go b/internal/distributed/proxy/httpserver/handler_v2_test.go index 1fc444f6c1..b41ac8f4f8 100644 --- a/internal/distributed/proxy/httpserver/handler_v2_test.go +++ b/internal/distributed/proxy/httpserver/handler_v2_test.go @@ -29,6 +29,7 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "google.golang.org/protobuf/proto" @@ -44,6 +45,7 @@ import ( "github.com/milvus-io/milvus/pkg/v3/common" "github.com/milvus-io/milvus/pkg/v3/proto/internalpb" "github.com/milvus-io/milvus/pkg/v3/util" + "github.com/milvus-io/milvus/pkg/v3/util/crypto" "github.com/milvus-io/milvus/pkg/v3/util/merr" "github.com/milvus-io/milvus/pkg/v3/util/paramtable" ) @@ -2879,6 +2881,79 @@ func TestMethodPost(t *testing.T) { } } +func TestUserDescriptionV2(t *testing.T) { + paramtable.Init() + + description := "用户描述" + mp := mocks.NewMockProxy(t) + mp.EXPECT(). + CreateCredential(mock.Anything, mock.MatchedBy(func(req *milvuspb.CreateCredentialRequest) bool { + return req.GetUsername() == util.UserRoot && + req.GetPassword() == crypto.Base64Encode("Milvus") && + req.Description != nil && + req.GetDescription() == description + })). + Return(commonSuccessStatus, nil). + Once() + mp.EXPECT(). + UpdateCredential(mock.Anything, mock.MatchedBy(func(req *milvuspb.UpdateCredentialRequest) bool { + return req.GetUsername() == util.UserRoot && + req.GetOldPassword() == "" && + req.GetNewPassword() == "" && + req.Description != nil && + req.GetDescription() == description + })). + Return(commonSuccessStatus, nil). + Once() + mp.EXPECT(). + SelectUser(mock.Anything, mock.Anything). + Return(&milvuspb.SelectUserResponse{ + Status: &StatusSuccess, + Results: []*milvuspb.UserResult{ + { + User: &milvuspb.UserEntity{Name: util.UserRoot}, + Roles: []*milvuspb.RoleEntity{{Name: util.RoleAdmin}}, + Description: description, + }, + }, + }, nil). + Once() + + testEngine := initHTTPServerV2(mp, false) + for _, testcase := range []struct { + path string + body string + }{ + { + path: versionalV2(UserCategory, CreateAction), + body: `{"userName":"` + util.UserRoot + `","password":"Milvus","description":"` + description + `"}`, + }, + { + path: versionalV2(UserCategory, UpdatePasswordAction), + body: `{"userName":"` + util.UserRoot + `","description":"` + description + `"}`, + }, + { + path: versionalV2(UserCategory, DescribeAction), + body: `{"userName":"` + util.UserRoot + `"}`, + }, + } { + t.Run(testcase.path, func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, testcase.path, bytes.NewReader([]byte(testcase.body))) + w := httptest.NewRecorder() + testEngine.ServeHTTP(w, req) + assert.Equal(t, http.StatusOK, w.Code) + + resp := map[string]interface{}{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Equal(t, float64(0), resp[HTTPReturnCode]) + if testcase.path == versionalV2(UserCategory, DescribeAction) { + assert.Equal(t, description, resp[HTTPReturnDescription]) + assert.Equal(t, []interface{}{util.RoleAdmin}, resp[HTTPReturnData]) + } + }) + } +} + func validateTestCases(t *testing.T, testEngine *gin.Engine, queryTestCases []requestBodyTestCase, allowInt64 bool) { for i, testcase := range queryTestCases { t.Run(testcase.path, func(t *testing.T) { diff --git a/internal/distributed/proxy/httpserver/request_v2.go b/internal/distributed/proxy/httpserver/request_v2.go index 22dc5e58e1..f47e18a5bd 100644 --- a/internal/distributed/proxy/httpserver/request_v2.go +++ b/internal/distributed/proxy/httpserver/request_v2.go @@ -519,14 +519,16 @@ type TimestampGetter interface { GetTimestamp() uint64 } type PasswordReq struct { - UserName string `json:"userName" binding:"required"` - Password string `json:"password" binding:"required"` + UserName string `json:"userName" binding:"required"` + Password string `json:"password" binding:"required"` + Description *string `json:"description"` } type NewPasswordReq struct { - UserName string `json:"userName" binding:"required"` - Password string `json:"password" binding:"required"` - NewPassword string `json:"newPassword" binding:"required"` + UserName string `json:"userName" binding:"required"` + Password string `json:"password"` + NewPassword string `json:"newPassword"` + Description *string `json:"description"` } type UserRoleReq struct { diff --git a/internal/metastore/catalog.go b/internal/metastore/catalog.go index d4e3d9b84b..1d5b173ae4 100644 --- a/internal/metastore/catalog.go +++ b/internal/metastore/catalog.go @@ -90,7 +90,6 @@ type RootCoordCatalog interface { // MigrateGrantCollectionName migrates all grants from oldName to newName when a collection is renamed. MigrateGrantCollectionName(ctx context.Context, tenant string, oldDBName string, oldName string, newDBName string, newName string) error - ListCredentialsWithPasswd(ctx context.Context) (map[string]string, error) BackupRBAC(ctx context.Context, tenant string) (*milvuspb.RBACMeta, error) RestoreRBAC(ctx context.Context, tenant string, meta *milvuspb.RBACMeta) error diff --git a/internal/metastore/kv/rootcoord/kv_catalog.go b/internal/metastore/kv/rootcoord/kv_catalog.go index cb83b730e6..18c504c462 100644 --- a/internal/metastore/kv/rootcoord/kv_catalog.go +++ b/internal/metastore/kv/rootcoord/kv_catalog.go @@ -360,7 +360,8 @@ func (kc *Catalog) CreateAlias(ctx context.Context, alias *model.Alias, ts typeu func (kc *Catalog) AlterCredential(ctx context.Context, credential *model.Credential) error { k := fmt.Sprintf("%s/%s", CredentialPrefix, credential.Username) credentialInfo := model.MarshalCredentialModel(credential) - credentialInfo.Username = "" // Username is already save in the key, remove it from the value. + credentialInfo.Username = "" // Username is already save in the key, remove it from the value. + credentialInfo.Sha256Password = "" // Sha256Password is cache-only, do not persist it. v, err := json.Marshal(credentialInfo) if err != nil { log.Ctx(ctx).Error("create credential marshal fail", zap.String("key", k), zap.Error(err)) @@ -1129,37 +1130,45 @@ func (kc *Catalog) ListAliases(ctx context.Context, dbID int64, ts typeutil.Time } func (kc *Catalog) ListCredentials(ctx context.Context) ([]string, error) { - users, err := kc.ListCredentialsWithPasswd(ctx) + credentials, err := kc.listCredentials(ctx) if err != nil { return nil, err } - return lo.Keys(users), nil + return lo.Map(credentials, func(credential *model.Credential, _ int) string { + return credential.Username + }), nil } -func (kc *Catalog) ListCredentialsWithPasswd(ctx context.Context) (map[string]string, error) { +func (kc *Catalog) listCredentials(ctx context.Context) ([]*model.Credential, error) { keys, values, err := kc.Txn.LoadWithPrefix(ctx, CredentialPrefix+"/") if err != nil { - log.Ctx(ctx).Error("list all credential usernames fail", zap.String("prefix", CredentialPrefix), zap.Error(err)) + log.Ctx(ctx).Error("list all credentials fail", zap.String("prefix", CredentialPrefix), zap.Error(err)) return nil, err } - users := make(map[string]string) + credentials := make([]*model.Credential, 0, len(keys)) + prefix := CredentialPrefix + "/" for i := range keys { - username := typeutil.After(keys[i], UserSubPrefix+"/") - if len(username) == 0 { - log.Ctx(ctx).Warn("no username extract from path:", zap.String("path", keys[i])) + prefixPos := strings.Index(keys[i], prefix) + if prefixPos < 0 { + log.Ctx(ctx).Warn("invalid credential key", zap.String("path", keys[i]), zap.String("prefix", prefix)) continue } - credential := &internalpb.CredentialInfo{} - err := json.Unmarshal([]byte(values[i]), credential) - if err != nil { + username := keys[i][prefixPos+len(prefix):] + if len(username) == 0 || strings.Contains(username, "/") { + log.Ctx(ctx).Warn("invalid credential key", zap.String("path", keys[i]), zap.String("prefix", prefix)) + continue + } + credentialInfo := &internalpb.CredentialInfo{} + if err := json.Unmarshal([]byte(values[i]), credentialInfo); err != nil { log.Ctx(ctx).Error("credential unmarshal fail", zap.String("key", keys[i]), zap.Error(err)) return nil, err } - users[username] = credential.EncryptedPassword + credentialInfo.Username = username + credentials = append(credentials, model.UnmarshalCredentialModel(credentialInfo)) } - return users, nil + return credentials, nil } func (kc *Catalog) remove(ctx context.Context, k string) error { @@ -1302,13 +1311,17 @@ func (kc *Catalog) getRolesByUsername(ctx context.Context, tenant string, userna return roles, nil } -// getUserResult get the user result by the username. And never return the error because the error means the user isn't added to a role. -func (kc *Catalog) getUserResult(ctx context.Context, tenant string, username string, includeRoleInfo bool) (*milvuspb.UserResult, error) { - result := &milvuspb.UserResult{User: &milvuspb.UserEntity{Name: username}} +// getUserResult gets the user result from a loaded credential. +func (kc *Catalog) getUserResult(ctx context.Context, tenant string, credential *model.Credential, includeRoleInfo bool) (*milvuspb.UserResult, error) { + result := &milvuspb.UserResult{ + User: &milvuspb.UserEntity{Name: credential.Username}, + Description: credential.Description, + } + if !includeRoleInfo { return result, nil } - roleNames, err := kc.getRolesByUsername(ctx, tenant, username) + roleNames, err := kc.getRolesByUsername(ctx, tenant, credential.Username) if err != nil { log.Ctx(ctx).Warn("fail to get roles by the username", zap.Error(err)) return result, err @@ -1323,40 +1336,33 @@ func (kc *Catalog) getUserResult(ctx context.Context, tenant string, username st func (kc *Catalog) ListUser(ctx context.Context, tenant string, entity *milvuspb.UserEntity, includeRoleInfo bool) ([]*milvuspb.UserResult, error) { var ( - usernames []string - err error - results []*milvuspb.UserResult + credentials []*model.Credential + err error ) - appendUserResult := func(username string) error { - result, err := kc.getUserResult(ctx, tenant, username, includeRoleInfo) - if err != nil { - return err - } - results = append(results, result) - return nil - } - if entity == nil { - usernames, err = kc.ListCredentials(ctx) - if err != nil { - return results, err - } - } else { - if funcutil.IsEmptyString(entity.Name) { - return results, merr.WrapErrParameterInvalidMsg("username in the user entity is empty") - } - _, err = kc.GetCredential(ctx, entity.Name) - if err != nil { - return results, err - } - usernames = append(usernames, entity.Name) - } - for _, username := range usernames { - err = appendUserResult(username) + credentials, err = kc.listCredentials(ctx) if err != nil { return nil, err } + } else { + if funcutil.IsEmptyString(entity.Name) { + return nil, merr.WrapErrParameterInvalidMsg("username in the user entity is empty") + } + credential, err := kc.GetCredential(ctx, entity.Name) + if err != nil { + return nil, err + } + credentials = []*model.Credential{credential} + } + + results := make([]*milvuspb.UserResult, 0, len(credentials)) + for _, credential := range credentials { + result, err := kc.getUserResult(ctx, tenant, credential, includeRoleInfo) + if err != nil { + return nil, err + } + results = append(results, result) } return results, nil } @@ -1987,14 +1993,20 @@ func (kc *Catalog) ListUserRole(ctx context.Context, tenant string) ([]string, e } func (kc *Catalog) BackupRBAC(ctx context.Context, tenant string) (*milvuspb.RBACMeta, error) { - users, err := kc.ListUser(ctx, tenant, nil, true) + credentials, err := kc.listCredentials(ctx) if err != nil { return nil, err } - credentials, err := kc.ListCredentialsWithPasswd(ctx) - if err != nil { - return nil, err + users := make([]*milvuspb.UserResult, 0, len(credentials)) + credentialPasswords := make(map[string]string, len(credentials)) + for _, credential := range credentials { + credentialPasswords[credential.Username] = credential.EncryptedPassword + user, err := kc.getUserResult(ctx, tenant, credential, true) + if err != nil { + return nil, err + } + users = append(users, user) } userInfos := lo.FilterMap(users, func(entity *milvuspb.UserResult, _ int) (*milvuspb.UserInfo, bool) { @@ -2004,7 +2016,7 @@ func (kc *Catalog) BackupRBAC(ctx context.Context, tenant string) (*milvuspb.RBA } return &milvuspb.UserInfo{ User: userName, - Password: credentials[userName], + Password: credentialPasswords[userName], Roles: entity.GetRoles(), }, true }) diff --git a/internal/metastore/kv/rootcoord/kv_catalog_test.go b/internal/metastore/kv/rootcoord/kv_catalog_test.go index 41f0aa3760..ac82ccd062 100644 --- a/internal/metastore/kv/rootcoord/kv_catalog_test.go +++ b/internal/metastore/kv/rootcoord/kv_catalog_test.go @@ -1607,6 +1607,133 @@ func getUserInfoMetaString(username string) string { return string(validBytes) } +func TestGetUserResultIncludesDescriptionWithoutRoleInfo(t *testing.T) { + ctx := context.TODO() + tenant := util.DefaultTenant + username := "user-with-description" + description := "stored description" + + kvmock := mocks.NewTxnKV(t) + c := NewCatalog(kvmock).(*Catalog) + credential := &model.Credential{ + Username: username, + Description: description, + } + + result, err := c.getUserResult(ctx, tenant, credential, false) + + require.NoError(t, err) + assert.Equal(t, username, result.GetUser().GetName()) + assert.Equal(t, description, result.GetDescription()) + assert.Empty(t, result.GetRoles()) +} + +func TestListUserReturnsCredentialLoadError(t *testing.T) { + ctx := context.TODO() + tenant := util.DefaultTenant + username := "user-with-credential-load-error" + credentialKey := fmt.Sprintf("%s/%s", CredentialPrefix, username) + expectedErr := errors.New("credential load failed") + + kvmock := mocks.NewTxnKV(t) + c := NewCatalog(kvmock).(*Catalog) + kvmock.EXPECT().Load(mock.Anything, credentialKey).Return("", expectedErr) + + results, err := c.ListUser(ctx, tenant, &milvuspb.UserEntity{Name: username}, false) + + require.ErrorIs(t, err, expectedErr) + assert.Empty(t, results) +} + +func TestListUserSingleUserReusesLoadedCredentialDescription(t *testing.T) { + ctx := context.TODO() + tenant := util.DefaultTenant + username := "user-with-description" + description := "single user description" + credentialKey := fmt.Sprintf("%s/%s", CredentialPrefix, username) + credentialValue, err := json.Marshal(&internalpb.CredentialInfo{Description: &description}) + require.NoError(t, err) + + kvmock := mocks.NewTxnKV(t) + c := NewCatalog(kvmock).(*Catalog) + kvmock.EXPECT().Load(mock.Anything, credentialKey).Return(string(credentialValue), nil).Once() + + results, err := c.ListUser(ctx, tenant, &milvuspb.UserEntity{Name: username}, false) + + require.NoError(t, err) + require.Len(t, results, 1) + assert.Equal(t, username, results[0].GetUser().GetName()) + assert.Equal(t, description, results[0].GetDescription()) + assert.Empty(t, results[0].GetRoles()) +} + +func TestListUserAllUsersReusesLoadedCredentialDescriptions(t *testing.T) { + ctx := context.TODO() + tenant := util.DefaultTenant + user1Description := "first user" + user2Description := "second user" + ghostDescription := "ghost user" + user1Credential, err := json.Marshal(&internalpb.CredentialInfo{Description: &user1Description}) + require.NoError(t, err) + user2Credential, err := json.Marshal(&internalpb.CredentialInfo{Description: &user2Description}) + require.NoError(t, err) + ghostCredential, err := json.Marshal(&internalpb.CredentialInfo{Description: &ghostDescription}) + require.NoError(t, err) + + kvmock := mocks.NewTxnKV(t) + c := NewCatalog(kvmock).(*Catalog) + kvmock.EXPECT().LoadWithPrefix(mock.Anything, CredentialPrefix+"/").Return( + []string{ + fmt.Sprintf("%s/%s", CredentialPrefix, "user1"), + fmt.Sprintf("%s/%s", CredentialPrefix, "user2"), + fmt.Sprintf("%s/%s/%s", CredentialPrefix, UserSubPrefix, "ghost"), + }, + []string{string(user1Credential), string(user2Credential), string(ghostCredential)}, + nil, + ) + + results, err := c.ListUser(ctx, tenant, nil, false) + + require.NoError(t, err) + require.Len(t, results, 2) + assert.Equal(t, "user1", results[0].GetUser().GetName()) + assert.Equal(t, user1Description, results[0].GetDescription()) + assert.Empty(t, results[0].GetRoles()) + assert.Equal(t, "user2", results[1].GetUser().GetName()) + assert.Equal(t, user2Description, results[1].GetDescription()) + assert.Empty(t, results[1].GetRoles()) +} + +func TestBackupRBACReusesLoadedCredentials(t *testing.T) { + ctx := context.TODO() + tenant := util.DefaultTenant + username := "user-with-password" + password := "encrypted-password" + credentialValue, err := json.Marshal(&internalpb.CredentialInfo{EncryptedPassword: password}) + require.NoError(t, err) + + var credentialPrefixLoads int + kvmock := mocks.NewTxnKV(t) + c := NewCatalog(kvmock).(*Catalog) + kvmock.EXPECT().LoadWithPrefix(mock.Anything, mock.Anything).RunAndReturn( + func(ctx context.Context, key string) ([]string, []string, error) { + if key == CredentialPrefix+"/" { + credentialPrefixLoads++ + return []string{fmt.Sprintf("%s/%s", CredentialPrefix, username)}, []string{string(credentialValue)}, nil + } + return nil, nil, nil + }) + + backup, err := c.BackupRBAC(ctx, tenant) + + require.NoError(t, err) + assert.Equal(t, 1, credentialPrefixLoads) + require.Len(t, backup.GetUsers(), 1) + assert.Equal(t, username, backup.GetUsers()[0].GetUser()) + assert.Equal(t, password, backup.GetUsers()[0].GetPassword()) + assert.Empty(t, backup.GetUsers()[0].GetRoles()) +} + func TestRBAC_Credential(t *testing.T) { ctx := context.TODO() @@ -1711,6 +1838,32 @@ func TestRBAC_Credential(t *testing.T) { } }) + t.Run("test AlterCredential does not persist sha256 password", func(t *testing.T) { + var ( + kvmock = mocks.NewTxnKV(t) + c = NewCatalog(kvmock) + ) + + kvmock.EXPECT(). + Save(mock.Anything, fmt.Sprintf("%s/%s", CredentialPrefix, "user1"), mock.MatchedBy(func(value string) bool { + credential := &internalpb.CredentialInfo{} + require.NoError(t, json.Unmarshal([]byte(value), credential)) + return credential.GetEncryptedPassword() == "password" && + credential.GetSha256Password() == "" && + credential.GetDescription() == "description" + })). + Return(nil) + + description := "description" + err := c.AlterCredential(ctx, &model.Credential{ + Username: "user1", + EncryptedPassword: "password", + Sha256Password: "sha256-password", + Description: description, + }) + assert.NoError(t, err) + }) + t.Run("test DropCredential", func(t *testing.T) { var ( kvmock = mocks.NewTxnKV(t) @@ -1780,6 +1933,8 @@ func TestRBAC_Credential(t *testing.T) { // Return valid keys if count==0 // return error if count!=0 + credentialValue, err := json.Marshal(&internalpb.CredentialInfo{EncryptedPassword: crypto.Base64Encode("passwd")}) + require.NoError(t, err) kvmock.EXPECT().LoadWithPrefix(mock.Anything, mock.Anything).Call.Return( func(ctx context.Context, key string) []string { cmu.RLock() @@ -1789,6 +1944,7 @@ func TestRBAC_Credential(t *testing.T) { fmt.Sprintf("%s/%s", CredentialPrefix, "user1"), fmt.Sprintf("%s/%s", CredentialPrefix, "user2"), fmt.Sprintf("%s/%s", CredentialPrefix, "user3"), + fmt.Sprintf("%s/%s/%s", CredentialPrefix, UserSubPrefix, "ghost"), "random", } } @@ -1797,13 +1953,13 @@ func TestRBAC_Credential(t *testing.T) { func(ctx context.Context, key string) []string { cmu.RLock() defer cmu.RUnlock() - passwd, _ := json.Marshal(&model.Credential{EncryptedPassword: crypto.Base64Encode("passwd")}) if count == 0 { return []string{ - string(passwd), - string(passwd), - string(passwd), - string(passwd), + string(credentialValue), + string(credentialValue), + string(credentialValue), + string(credentialValue), + string(credentialValue), } } return nil @@ -2119,11 +2275,12 @@ func TestRBAC_Role(t *testing.T) { kvmock = mocks.NewTxnKV(t) c = NewCatalog(kvmock).(*Catalog) - invalidUser = "invalid-user" - invalidUserKey = funcutil.HandleTenantForEtcdPrefix(RoleMappingPrefix, tenant, invalidUser) + invalidUser = "invalid-user" + invalidRoleUser = "invalid-role-user" + invalidRoleUserKey = funcutil.HandleTenantForEtcdPrefix(RoleMappingPrefix, tenant, invalidRoleUser) ) - // returns error for invalidUserKey - kvmock.EXPECT().LoadWithPrefix(mock.Anything, invalidUserKey).Call.Return( + // returns error for invalidRoleUserKey + kvmock.EXPECT().LoadWithPrefix(mock.Anything, invalidRoleUserKey).Call.Return( nil, nil, errors.New("Mock load with prefix wrong")) // Returns keys for RoleMappingPrefix/tenant/user1/ (with trailing slash after the fix) @@ -2136,6 +2293,9 @@ func TestRBAC_Role(t *testing.T) { user1Key + "role3/error", } }, nil, nil) + kvmock.EXPECT().Load(mock.Anything, fmt.Sprintf("%s/%s", CredentialPrefix, "user1")).Return(getUserInfoMetaString("user1"), nil).Maybe() + kvmock.EXPECT().Load(mock.Anything, fmt.Sprintf("%s/%s", CredentialPrefix, invalidUser)).Return("", errors.New("mock load credential error")).Maybe() + kvmock.EXPECT().Load(mock.Anything, fmt.Sprintf("%s/%s", CredentialPrefix, invalidRoleUser)).Return(getUserInfoMetaString(invalidRoleUser), nil).Maybe() // Returns keys for CredentialPrefix var loadCredentialPrefixReturn atomic.Bool @@ -2143,7 +2303,7 @@ func TestRBAC_Role(t *testing.T) { func(ctx context.Context, key string) []string { if loadCredentialPrefixReturn.Load() { return []string{ - fmt.Sprintf("%s/%s/%s", CredentialPrefix, UserSubPrefix, "user1"), + fmt.Sprintf("%s/%s", CredentialPrefix, "user1"), } } return nil @@ -2168,20 +2328,19 @@ func TestRBAC_Role(t *testing.T) { description string isValid bool - user string + credential *model.Credential includeRoleInfo bool }{ - {"valid user1 not include RoleInfo", true, "user1", false}, - {"valid user1 include RoleInfo", true, "user1", true}, - {"invalid user not include RoleInfo", true, invalidUser, false}, - {"invalid user include RoleInfo", false, invalidUser, true}, + {"valid user1 not include RoleInfo", true, &model.Credential{Username: "user1"}, false}, + {"valid user1 include RoleInfo", true, &model.Credential{Username: "user1"}, true}, + {"invalid role user include RoleInfo", false, &model.Credential{Username: invalidRoleUser}, true}, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - res, err := c.getUserResult(ctx, tenant, test.user, test.includeRoleInfo) + res, err := c.getUserResult(ctx, tenant, test.credential, test.includeRoleInfo) - assert.Equal(t, test.user, res.GetUser().GetName()) + assert.Equal(t, test.credential.Username, res.GetUser().GetName()) if test.isValid { assert.NoError(t, err) @@ -2231,6 +2390,7 @@ func TestRBAC_Role(t *testing.T) { {true, true, &milvuspb.UserEntity{Name: "user1"}, false, "valid entity user1 not include RoleInfo"}, {true, true, &milvuspb.UserEntity{Name: "user1"}, true, "valid entity user1 include RoleInfo"}, {false, true, &milvuspb.UserEntity{Name: invalidUser}, true, "invalid entity invalidUser include RoleInfo"}, + {false, true, &milvuspb.UserEntity{Name: invalidRoleUser}, true, "invalid entity invalidRoleUser include RoleInfo"}, } for _, test := range tests { @@ -3282,10 +3442,11 @@ func TestRBAC_Restore(t *testing.T) { assert.NoError(t, err) // check user - users, err := c.ListCredentialsWithPasswd(ctx) + credentials, err := c.(*Catalog).listCredentials(ctx) assert.NoError(t, err) - assert.Len(t, users, 1) - assert.Equal(t, users["user1"], "passwd") + assert.Len(t, credentials, 1) + assert.Equal(t, "user1", credentials[0].Username) + assert.Equal(t, "passwd", credentials[0].EncryptedPassword) // check role roles, err := c.ListRole(ctx, util.DefaultTenant, nil, false) assert.NoError(t, err) @@ -3353,9 +3514,9 @@ func TestRBAC_Restore(t *testing.T) { assert.NoError(t, err) // check user - users, err = c.ListCredentialsWithPasswd(ctx) + credentials, err = c.(*Catalog).listCredentials(ctx) assert.NoError(t, err) - assert.Len(t, users, 2) + assert.Len(t, credentials, 2) // check role roles, err = c.ListRole(ctx, util.DefaultTenant, nil, false) assert.NoError(t, err) diff --git a/internal/metastore/mocks/mock_datacoord_catalog.go b/internal/metastore/mocks/mock_datacoord_catalog.go index 55438a0103..b8a49c5864 100644 --- a/internal/metastore/mocks/mock_datacoord_catalog.go +++ b/internal/metastore/mocks/mock_datacoord_catalog.go @@ -5,18 +5,13 @@ package mocks import ( context "context" + msgpb "github.com/milvus-io/milvus-proto/go-api/v3/msgpb" + metastore "github.com/milvus-io/milvus/internal/metastore" + model "github.com/milvus-io/milvus/internal/metastore/model" datapb "github.com/milvus-io/milvus/pkg/v3/proto/datapb" indexpb "github.com/milvus-io/milvus/pkg/v3/proto/indexpb" - internalpb "github.com/milvus-io/milvus/pkg/v3/proto/internalpb" - - metastore "github.com/milvus-io/milvus/internal/metastore" - mock "github.com/stretchr/testify/mock" - - model "github.com/milvus-io/milvus/internal/metastore/model" - - msgpb "github.com/milvus-io/milvus-proto/go-api/v3/msgpb" ) // DataCoordCatalog is an autogenerated mock type for the DataCoordCatalog type diff --git a/internal/metastore/mocks/mock_querycoord_catalog.go b/internal/metastore/mocks/mock_querycoord_catalog.go index da97e1f00e..3b55fce45e 100644 --- a/internal/metastore/mocks/mock_querycoord_catalog.go +++ b/internal/metastore/mocks/mock_querycoord_catalog.go @@ -5,9 +5,8 @@ package mocks import ( context "context" - mock "github.com/stretchr/testify/mock" - querypb "github.com/milvus-io/milvus/pkg/v3/proto/querypb" + mock "github.com/stretchr/testify/mock" ) // QueryCoordCatalog is an autogenerated mock type for the QueryCoordCatalog type diff --git a/internal/metastore/mocks/mock_rootcoord_catalog.go b/internal/metastore/mocks/mock_rootcoord_catalog.go index fab13d1964..4ec8e335b4 100644 --- a/internal/metastore/mocks/mock_rootcoord_catalog.go +++ b/internal/metastore/mocks/mock_rootcoord_catalog.go @@ -5,14 +5,11 @@ package mocks import ( context "context" - metastore "github.com/milvus-io/milvus/internal/metastore" - internalpb "github.com/milvus-io/milvus/pkg/v3/proto/internalpb" - milvuspb "github.com/milvus-io/milvus-proto/go-api/v3/milvuspb" - - mock "github.com/stretchr/testify/mock" - + metastore "github.com/milvus-io/milvus/internal/metastore" model "github.com/milvus-io/milvus/internal/metastore/model" + internalpb "github.com/milvus-io/milvus/pkg/v3/proto/internalpb" + mock "github.com/stretchr/testify/mock" ) // RootCoordCatalog is an autogenerated mock type for the RootCoordCatalog type @@ -1655,64 +1652,6 @@ func (_c *RootCoordCatalog_ListCredentials_Call) RunAndReturn(run func(context.C return _c } -// ListCredentialsWithPasswd provides a mock function with given fields: ctx -func (_m *RootCoordCatalog) ListCredentialsWithPasswd(ctx context.Context) (map[string]string, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for ListCredentialsWithPasswd") - } - - var r0 map[string]string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (map[string]string, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) map[string]string); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]string) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RootCoordCatalog_ListCredentialsWithPasswd_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListCredentialsWithPasswd' -type RootCoordCatalog_ListCredentialsWithPasswd_Call struct { - *mock.Call -} - -// ListCredentialsWithPasswd is a helper method to define mock.On call -// - ctx context.Context -func (_e *RootCoordCatalog_Expecter) ListCredentialsWithPasswd(ctx interface{}) *RootCoordCatalog_ListCredentialsWithPasswd_Call { - return &RootCoordCatalog_ListCredentialsWithPasswd_Call{Call: _e.mock.On("ListCredentialsWithPasswd", ctx)} -} - -func (_c *RootCoordCatalog_ListCredentialsWithPasswd_Call) Run(run func(ctx context.Context)) *RootCoordCatalog_ListCredentialsWithPasswd_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *RootCoordCatalog_ListCredentialsWithPasswd_Call) Return(_a0 map[string]string, _a1 error) *RootCoordCatalog_ListCredentialsWithPasswd_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *RootCoordCatalog_ListCredentialsWithPasswd_Call) RunAndReturn(run func(context.Context) (map[string]string, error)) *RootCoordCatalog_ListCredentialsWithPasswd_Call { - _c.Call.Return(run) - return _c -} - // ListDatabases provides a mock function with given fields: ctx, ts func (_m *RootCoordCatalog) ListDatabases(ctx context.Context, ts uint64) ([]*model.Database, error) { ret := _m.Called(ctx, ts) diff --git a/internal/metastore/model/credential.go b/internal/metastore/model/credential.go index 256c5acace..1a5a9c5a3b 100644 --- a/internal/metastore/model/credential.go +++ b/internal/metastore/model/credential.go @@ -8,6 +8,7 @@ type Credential struct { Tenant string IsSuper bool Sha256Password string + Description string TimeTick uint64 // the timetick in wal which the credential updates } @@ -21,6 +22,7 @@ func MarshalCredentialModel(cred *Credential) *internalpb.CredentialInfo { EncryptedPassword: cred.EncryptedPassword, IsSuper: cred.IsSuper, Sha256Password: cred.Sha256Password, + Description: &cred.Description, TimeTick: cred.TimeTick, } } @@ -35,6 +37,7 @@ func UnmarshalCredentialModel(cred *internalpb.CredentialInfo) *Credential { Tenant: cred.Tenant, IsSuper: cred.IsSuper, Sha256Password: cred.Sha256Password, + Description: cred.GetDescription(), TimeTick: cred.TimeTick, } } diff --git a/internal/metastore/model/credential_test.go b/internal/metastore/model/credential_test.go index b3f115a6ab..fd1ce4c6ba 100644 --- a/internal/metastore/model/credential_test.go +++ b/internal/metastore/model/credential_test.go @@ -15,14 +15,17 @@ var ( Tenant: "tenant-1", IsSuper: true, Sha256Password: "xxxx", + Description: "description", } + description = "description" credentialPb = &internalpb.CredentialInfo{ Username: "user", EncryptedPassword: "password", Tenant: "tenant-1", IsSuper: true, Sha256Password: "xxxx", + Description: &description, } ) diff --git a/internal/mocks/mock_proxy.go b/internal/mocks/mock_proxy.go index 1b2b9e65f6..3318f3d5ea 100644 --- a/internal/mocks/mock_proxy.go +++ b/internal/mocks/mock_proxy.go @@ -6,19 +6,13 @@ import ( context "context" commonpb "github.com/milvus-io/milvus-proto/go-api/v3/commonpb" - federpb "github.com/milvus-io/milvus-proto/go-api/v3/federpb" - - internalpb "github.com/milvus-io/milvus/pkg/v3/proto/internalpb" - milvuspb "github.com/milvus-io/milvus-proto/go-api/v3/milvuspb" - - mock "github.com/stretchr/testify/mock" - - datapb "github.com/milvus-io/milvus/pkg/v3/proto/datapb" - proxypb "github.com/milvus-io/milvus/pkg/v3/proto/proxypb" - types "github.com/milvus-io/milvus/internal/types" + datapb "github.com/milvus-io/milvus/pkg/v3/proto/datapb" + internalpb "github.com/milvus-io/milvus/pkg/v3/proto/internalpb" + proxypb "github.com/milvus-io/milvus/pkg/v3/proto/proxypb" + mock "github.com/stretchr/testify/mock" ) // MockProxy is an autogenerated mock type for the ProxyComponent type @@ -860,6 +854,65 @@ func (_c *MockProxy_AlterIndex_Call) RunAndReturn(run func(context.Context, *mil return _c } +// AlterRole provides a mock function with given fields: _a0, _a1 +func (_m *MockProxy) AlterRole(_a0 context.Context, _a1 *milvuspb.AlterRoleRequest) (*commonpb.Status, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for AlterRole") + } + + var r0 *commonpb.Status + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.AlterRoleRequest) (*commonpb.Status, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.AlterRoleRequest) *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.AlterRoleRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockProxy_AlterRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AlterRole' +type MockProxy_AlterRole_Call struct { + *mock.Call +} + +// AlterRole is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.AlterRoleRequest +func (_e *MockProxy_Expecter) AlterRole(_a0 interface{}, _a1 interface{}) *MockProxy_AlterRole_Call { + return &MockProxy_AlterRole_Call{Call: _e.mock.On("AlterRole", _a0, _a1)} +} + +func (_c *MockProxy_AlterRole_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.AlterRoleRequest)) *MockProxy_AlterRole_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.AlterRoleRequest)) + }) + return _c +} + +func (_c *MockProxy_AlterRole_Call) Return(_a0 *commonpb.Status, _a1 error) *MockProxy_AlterRole_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockProxy_AlterRole_Call) RunAndReturn(run func(context.Context, *milvuspb.AlterRoleRequest) (*commonpb.Status, error)) *MockProxy_AlterRole_Call { + _c.Call.Return(run) + return _c +} + // BackupRBAC provides a mock function with given fields: _a0, _a1 func (_m *MockProxy) BackupRBAC(_a0 context.Context, _a1 *milvuspb.BackupRBACMetaRequest) (*milvuspb.BackupRBACMetaResponse, error) { ret := _m.Called(_a0, _a1) @@ -3549,6 +3602,65 @@ func (_c *MockProxy_DumpMessages_Call) RunAndReturn(run func(*milvuspb.DumpMessa return _c } +// ExportSnapshot provides a mock function with given fields: _a0, _a1 +func (_m *MockProxy) ExportSnapshot(_a0 context.Context, _a1 *milvuspb.ExportSnapshotRequest) (*milvuspb.ExportSnapshotResponse, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for ExportSnapshot") + } + + var r0 *milvuspb.ExportSnapshotResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.ExportSnapshotRequest) (*milvuspb.ExportSnapshotResponse, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.ExportSnapshotRequest) *milvuspb.ExportSnapshotResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*milvuspb.ExportSnapshotResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.ExportSnapshotRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockProxy_ExportSnapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExportSnapshot' +type MockProxy_ExportSnapshot_Call struct { + *mock.Call +} + +// ExportSnapshot is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.ExportSnapshotRequest +func (_e *MockProxy_Expecter) ExportSnapshot(_a0 interface{}, _a1 interface{}) *MockProxy_ExportSnapshot_Call { + return &MockProxy_ExportSnapshot_Call{Call: _e.mock.On("ExportSnapshot", _a0, _a1)} +} + +func (_c *MockProxy_ExportSnapshot_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.ExportSnapshotRequest)) *MockProxy_ExportSnapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.ExportSnapshotRequest)) + }) + return _c +} + +func (_c *MockProxy_ExportSnapshot_Call) Return(_a0 *milvuspb.ExportSnapshotResponse, _a1 error) *MockProxy_ExportSnapshot_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockProxy_ExportSnapshot_Call) RunAndReturn(run func(context.Context, *milvuspb.ExportSnapshotRequest) (*milvuspb.ExportSnapshotResponse, error)) *MockProxy_ExportSnapshot_Call { + _c.Call.Return(run) + return _c +} + // Flush provides a mock function with given fields: _a0, _a1 func (_m *MockProxy) Flush(_a0 context.Context, _a1 *milvuspb.FlushRequest) (*milvuspb.FlushResponse, error) { ret := _m.Called(_a0, _a1) @@ -8166,6 +8278,65 @@ func (_c *MockProxy_ReplicateMessage_Call) RunAndReturn(run func(context.Context return _c } +// RestoreExternalSnapshot provides a mock function with given fields: _a0, _a1 +func (_m *MockProxy) RestoreExternalSnapshot(_a0 context.Context, _a1 *milvuspb.RestoreExternalSnapshotRequest) (*milvuspb.RestoreExternalSnapshotResponse, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for RestoreExternalSnapshot") + } + + var r0 *milvuspb.RestoreExternalSnapshotResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.RestoreExternalSnapshotRequest) (*milvuspb.RestoreExternalSnapshotResponse, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *milvuspb.RestoreExternalSnapshotRequest) *milvuspb.RestoreExternalSnapshotResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*milvuspb.RestoreExternalSnapshotResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *milvuspb.RestoreExternalSnapshotRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockProxy_RestoreExternalSnapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RestoreExternalSnapshot' +type MockProxy_RestoreExternalSnapshot_Call struct { + *mock.Call +} + +// RestoreExternalSnapshot is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *milvuspb.RestoreExternalSnapshotRequest +func (_e *MockProxy_Expecter) RestoreExternalSnapshot(_a0 interface{}, _a1 interface{}) *MockProxy_RestoreExternalSnapshot_Call { + return &MockProxy_RestoreExternalSnapshot_Call{Call: _e.mock.On("RestoreExternalSnapshot", _a0, _a1)} +} + +func (_c *MockProxy_RestoreExternalSnapshot_Call) Run(run func(_a0 context.Context, _a1 *milvuspb.RestoreExternalSnapshotRequest)) *MockProxy_RestoreExternalSnapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*milvuspb.RestoreExternalSnapshotRequest)) + }) + return _c +} + +func (_c *MockProxy_RestoreExternalSnapshot_Call) Return(_a0 *milvuspb.RestoreExternalSnapshotResponse, _a1 error) *MockProxy_RestoreExternalSnapshot_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockProxy_RestoreExternalSnapshot_Call) RunAndReturn(run func(context.Context, *milvuspb.RestoreExternalSnapshotRequest) (*milvuspb.RestoreExternalSnapshotResponse, error)) *MockProxy_RestoreExternalSnapshot_Call { + _c.Call.Return(run) + return _c +} + // RestoreRBAC provides a mock function with given fields: _a0, _a1 func (_m *MockProxy) RestoreRBAC(_a0 context.Context, _a1 *milvuspb.RestoreRBACMetaRequest) (*commonpb.Status, error) { ret := _m.Called(_a0, _a1) diff --git a/internal/proxy/credential_description_test.go b/internal/proxy/credential_description_test.go new file mode 100644 index 0000000000..ccc8d489db --- /dev/null +++ b/internal/proxy/credential_description_test.go @@ -0,0 +1,265 @@ +// 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 ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/milvus-io/milvus-proto/go-api/v3/commonpb" + "github.com/milvus-io/milvus-proto/go-api/v3/milvuspb" + "github.com/milvus-io/milvus/internal/mocks" + "github.com/milvus-io/milvus/pkg/v3/proto/internalpb" + "github.com/milvus-io/milvus/pkg/v3/util/crypto" + "github.com/milvus-io/milvus/pkg/v3/util/merr" + "github.com/milvus-io/milvus/pkg/v3/util/paramtable" +) + +func TestCreateCredentialPassesDescription(t *testing.T) { + paramtable.Init() + + description := "created user description" + mixCoord := mocks.NewMockMixCoordClient(t) + mixCoord.EXPECT(). + CreateCredential(mock.Anything, mock.MatchedBy(func(cred *internalpb.CredentialInfo) bool { + return cred.GetUsername() == "desc_user" && + cred.Description != nil && + cred.GetDescription() == description && + cred.GetEncryptedPassword() != "" && + cred.GetSha256Password() != "" + })). + Return(merr.Success(), nil) + + node := &Proxy{mixCoord: mixCoord} + node.UpdateStateCode(commonpb.StateCode_Healthy) + + status, err := node.CreateCredential(context.Background(), &milvuspb.CreateCredentialRequest{ + Username: "desc_user", + Password: crypto.Base64Encode("password"), + Description: &description, + }) + + require.NoError(t, err) + assert.Equal(t, commonpb.ErrorCode_Success, status.GetErrorCode()) +} + +func TestCreateCredentialRejectsOverLimitDescription(t *testing.T) { + paramtable.Init() + + mixCoord := mocks.NewMockMixCoordClient(t) + mixCoord.On("CreateCredential", mock.Anything, mock.Anything).Return(merr.Success(), nil).Maybe() + node := &Proxy{mixCoord: mixCoord} + node.UpdateStateCode(commonpb.StateCode_Healthy) + + description := strings.Repeat("x", 1025) + status, err := node.CreateCredential(context.Background(), &milvuspb.CreateCredentialRequest{ + Username: "desc_user", + Password: crypto.Base64Encode("password"), + Description: &description, + }) + + require.NoError(t, err) + assert.NotEqual(t, commonpb.ErrorCode_Success, status.GetErrorCode()) + mixCoord.AssertNotCalled(t, "CreateCredential", mock.Anything, mock.Anything) +} + +func TestUpdateCredentialDescriptionOnlySkipsPasswordVerification(t *testing.T) { + paramtable.Init() + + description := "updated user description" + mixCoord := mocks.NewMockMixCoordClient(t) + mixCoord.EXPECT(). + UpdateCredential(mock.Anything, mock.MatchedBy(func(cred *internalpb.CredentialInfo) bool { + return cred.GetUsername() == "desc_user" && + cred.Description != nil && + cred.GetDescription() == description && + cred.GetEncryptedPassword() == "" && + cred.GetSha256Password() == "" + })). + Return(merr.Success(), nil) + + node := &Proxy{mixCoord: mixCoord} + node.UpdateStateCode(commonpb.StateCode_Healthy) + + status, err := node.UpdateCredential(context.Background(), &milvuspb.UpdateCredentialRequest{ + Username: "desc_user", + Description: &description, + }) + + require.NoError(t, err) + assert.Equal(t, commonpb.ErrorCode_Success, status.GetErrorCode()) +} + +func TestUpdateCredentialClearsDescriptionOnly(t *testing.T) { + paramtable.Init() + + description := "" + mixCoord := mocks.NewMockMixCoordClient(t) + mixCoord.EXPECT(). + UpdateCredential(mock.Anything, mock.MatchedBy(func(cred *internalpb.CredentialInfo) bool { + return cred.GetUsername() == "desc_user" && + cred.Description != nil && + cred.GetDescription() == description && + cred.GetEncryptedPassword() == "" && + cred.GetSha256Password() == "" + })). + Return(merr.Success(), nil) + + node := &Proxy{mixCoord: mixCoord} + node.UpdateStateCode(commonpb.StateCode_Healthy) + + status, err := node.UpdateCredential(context.Background(), &milvuspb.UpdateCredentialRequest{ + Username: "desc_user", + Description: &description, + }) + + require.NoError(t, err) + assert.Equal(t, commonpb.ErrorCode_Success, status.GetErrorCode()) +} + +func TestUpdateCredentialRejectsEmptyUpdate(t *testing.T) { + paramtable.Init() + + mixCoord := mocks.NewMockMixCoordClient(t) + mixCoord.On("UpdateCredential", mock.Anything, mock.Anything).Return(merr.Success(), nil).Maybe() + node := &Proxy{mixCoord: mixCoord} + node.UpdateStateCode(commonpb.StateCode_Healthy) + + status, err := node.UpdateCredential(context.Background(), &milvuspb.UpdateCredentialRequest{ + Username: "desc_user", + }) + + require.NoError(t, err) + assert.NotEqual(t, commonpb.ErrorCode_Success, status.GetErrorCode()) + assert.Contains(t, status.GetReason(), "must update either password or description") + mixCoord.AssertNotCalled(t, "UpdateCredential", mock.Anything, mock.Anything) +} + +func TestUpdateCredentialPasswordAndDescription(t *testing.T) { + paramtable.Init() + require.NoError(t, paramtable.Get().Save(Params.CommonCfg.SuperUsers.Key, "root")) + defer paramtable.Get().Reset(Params.CommonCfg.SuperUsers.Key) + + description := "updated user description" + mixCoord := mocks.NewMockMixCoordClient(t) + mixCoord.EXPECT(). + UpdateCredential(mock.Anything, mock.MatchedBy(func(cred *internalpb.CredentialInfo) bool { + return cred.GetUsername() == "desc_user" && + cred.Description != nil && + cred.GetDescription() == description && + cred.GetEncryptedPassword() != "" && + cred.GetSha256Password() == crypto.SHA256("new_password", "desc_user") + })). + Return(merr.Success(), nil) + + node := &Proxy{mixCoord: mixCoord} + node.UpdateStateCode(commonpb.StateCode_Healthy) + + status, err := node.UpdateCredential(GetContext(context.Background(), "root:password"), &milvuspb.UpdateCredentialRequest{ + Username: "desc_user", + OldPassword: crypto.Base64Encode("old_password"), + NewPassword: crypto.Base64Encode("new_password"), + Description: &description, + }) + + require.NoError(t, err) + assert.Equal(t, commonpb.ErrorCode_Success, status.GetErrorCode()) +} + +func TestUpdateCredentialRejectsOverLimitDescription(t *testing.T) { + paramtable.Init() + require.NoError(t, paramtable.Get().Save(Params.CommonCfg.SuperUsers.Key, "root")) + defer paramtable.Get().Reset(Params.CommonCfg.SuperUsers.Key) + + mixCoord := mocks.NewMockMixCoordClient(t) + mixCoord.On("UpdateCredential", mock.Anything, mock.Anything).Return(merr.Success(), nil).Maybe() + node := &Proxy{mixCoord: mixCoord} + node.UpdateStateCode(commonpb.StateCode_Healthy) + + description := strings.Repeat("x", 1025) + status, err := node.UpdateCredential(GetContext(context.Background(), "root:password"), &milvuspb.UpdateCredentialRequest{ + Username: "desc_user", + OldPassword: crypto.Base64Encode("old_password"), + NewPassword: crypto.Base64Encode("new_password"), + Description: &description, + }) + + require.NoError(t, err) + assert.NotEqual(t, commonpb.ErrorCode_Success, status.GetErrorCode()) + mixCoord.AssertNotCalled(t, "UpdateCredential", mock.Anything, mock.Anything) +} + +func TestUpdateCredentialPasswordValidationErrors(t *testing.T) { + paramtable.Init() + + tests := []struct { + name string + oldPassword string + newPassword string + reason string + }{ + { + name: "invalid old password base64", + oldPassword: "not-base64!!", + newPassword: crypto.Base64Encode("new_password"), + reason: "decode old password failed", + }, + { + name: "invalid new password base64", + oldPassword: crypto.Base64Encode("old_password"), + newPassword: "not-base64!!", + reason: "decode password failed", + }, + { + name: "invalid new password length", + oldPassword: crypto.Base64Encode("old_password"), + newPassword: crypto.Base64Encode("short"), + reason: "invalid password length", + }, + { + name: "old password verification failed", + oldPassword: crypto.Base64Encode("old_password"), + newPassword: crypto.Base64Encode("new_password"), + reason: "old password not correct", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mixCoord := mocks.NewMockMixCoordClient(t) + mixCoord.On("UpdateCredential", mock.Anything, mock.Anything).Return(merr.Success(), nil).Maybe() + node := &Proxy{mixCoord: mixCoord} + node.UpdateStateCode(commonpb.StateCode_Healthy) + + status, err := node.UpdateCredential(context.Background(), &milvuspb.UpdateCredentialRequest{ + Username: "desc_user", + OldPassword: test.oldPassword, + NewPassword: test.newPassword, + }) + + require.NoError(t, err) + assert.NotEqual(t, commonpb.ErrorCode_Success, status.GetErrorCode()) + assert.Contains(t, status.GetReason(), test.reason) + mixCoord.AssertNotCalled(t, "UpdateCredential", mock.Anything, mock.Anything) + }) + } +} diff --git a/internal/proxy/impl.go b/internal/proxy/impl.go index 16314b14b4..55b3afc2c4 100644 --- a/internal/proxy/impl.go +++ b/internal/proxy/impl.go @@ -5483,6 +5483,9 @@ func (node *Proxy) CreateCredential(ctx context.Context, req *milvuspb.CreateCre if err := ValidateUsername(username); err != nil { return merr.Status(err), nil } + if err := ValidateUserDescription(req.GetDescription()); err != nil { + return merr.Status(err), nil + } rawPassword, err := crypto.Base64Decode(req.Password) if err != nil { log.Error("decode password fail", @@ -5511,6 +5514,7 @@ func (node *Proxy) CreateCredential(ctx context.Context, req *milvuspb.CreateCre Username: req.Username, EncryptedPassword: encryptedPassword, Sha256Password: crypto.SHA256(rawPassword, req.Username), + Description: req.Description, } result, err := node.mixCoord.CreateCredential(ctx, credInfo) if err != nil { // for error like conntext timeout etc. @@ -5533,57 +5537,69 @@ func (node *Proxy) UpdateCredential(ctx context.Context, req *milvuspb.UpdateCre if err := merr.CheckHealthy(node.GetStateCode()); err != nil { return merr.Status(err), nil } - rawOldPassword, err := crypto.Base64Decode(req.OldPassword) - if err != nil { - log.Error("decode old password fail", - zap.Error(err)) - err = merr.WrapErrParameterInvalidErr(err, "decode old password failed") + if err := ValidateUserDescription(req.GetDescription()); err != nil { return merr.Status(err), nil } - rawNewPassword, err := crypto.Base64Decode(req.NewPassword) - if err != nil { - log.Error("decode password fail", - zap.Error(err)) - err = merr.WrapErrParameterInvalidErr(err, "decode password failed") - return merr.Status(err), nil - } - // valid new password - if err = ValidatePassword(rawNewPassword); err != nil { - log.Error("illegal password", - zap.Error(err)) + if req.GetNewPassword() == "" && req.Description == nil { + err := merr.WrapErrParameterInvalidMsg("must update either password or description") return merr.Status(err), nil } - skipPasswordVerify := false - if currentUser, _ := GetCurUserFromContext(ctx); currentUser != "" { - for _, s := range Params.CommonCfg.SuperUsers.GetAsStrings() { - if s == currentUser { - skipPasswordVerify = true + updateCredReq := &internalpb.CredentialInfo{ + Username: req.Username, + Description: req.Description, + } + + if req.GetNewPassword() != "" { + rawOldPassword, err := crypto.Base64Decode(req.OldPassword) + if err != nil { + log.Error("decode old password fail", + zap.Error(err)) + err = merr.WrapErrParameterInvalidErr(err, "decode old password failed") + return merr.Status(err), nil + } + rawNewPassword, err := crypto.Base64Decode(req.NewPassword) + if err != nil { + log.Error("decode password fail", + zap.Error(err)) + err = merr.WrapErrParameterInvalidErr(err, "decode password failed") + return merr.Status(err), nil + } + // valid new password + if err = ValidatePassword(rawNewPassword); err != nil { + log.Error("illegal password", + zap.Error(err)) + return merr.Status(err), nil + } + + skipPasswordVerify := false + if currentUser, _ := GetCurUserFromContext(ctx); currentUser != "" { + for _, s := range Params.CommonCfg.SuperUsers.GetAsStrings() { + if s == currentUser { + skipPasswordVerify = true + } } } - } - if !skipPasswordVerify && !passwordVerify(ctx, req.Username, rawOldPassword, privilege.GetPrivilegeCache()) { - err := merr.WrapErrPrivilegeNotAuthenticated("old password not correct for %s", req.GetUsername()) - return merr.Status(err), nil - } - // update meta data - encryptedPassword, err := crypto.PasswordEncrypt(rawNewPassword) - if err != nil { - log.Error("encrypt password fail", - zap.Error(err)) - err = merr.WrapErrServiceInternalErr(err, "encrypt password failed") - return merr.Status(err), nil + if !skipPasswordVerify && !passwordVerify(ctx, req.Username, rawOldPassword, privilege.GetPrivilegeCache()) { + err := merr.WrapErrPrivilegeNotAuthenticated("old password not correct for %s", req.GetUsername()) + return merr.Status(err), nil + } + // update meta data + encryptedPassword, err := crypto.PasswordEncrypt(rawNewPassword) + if err != nil { + log.Error("encrypt password fail", + zap.Error(err)) + err = merr.WrapErrServiceInternalErr(err, "encrypt password failed") + return merr.Status(err), nil + } + updateCredReq.Sha256Password = crypto.SHA256(rawNewPassword, req.Username) + updateCredReq.EncryptedPassword = encryptedPassword } if req.Base == nil { req.Base = &commonpb.MsgBase{} } req.Base.MsgType = commonpb.MsgType_UpdateCredential - updateCredReq := &internalpb.CredentialInfo{ - Username: req.Username, - Sha256Password: crypto.SHA256(rawNewPassword, req.Username), - EncryptedPassword: encryptedPassword, - } result, err := node.mixCoord.UpdateCredential(ctx, updateCredReq) if err != nil { // for error like conntext timeout etc. log.Error("update credential fail", diff --git a/internal/proxy/util.go b/internal/proxy/util.go index ab77394ab6..e126bbf0a2 100644 --- a/internal/proxy/util.go +++ b/internal/proxy/util.go @@ -1486,6 +1486,13 @@ func ValidateUsername(username string) error { return nil } +func ValidateUserDescription(description string) error { + if len(description) > Params.ProxyCfg.MaxUserDescriptionLength.GetAsInt() { + return merr.WrapErrParameterInvalidMsg("invalid user description with length %d, the length of user description must be less than or equal to %s", len(description), Params.ProxyCfg.MaxUserDescriptionLength.GetValue()) + } + return nil +} + func ValidatePassword(password string) error { if len(password) < Params.ProxyCfg.MinPasswordLength.GetAsInt() || len(password) > Params.ProxyCfg.MaxPasswordLength.GetAsInt() { return merr.WrapErrParameterInvalidRange(Params.ProxyCfg.MinPasswordLength.GetAsInt(), diff --git a/internal/rootcoord/ddl_callbacks_rbac_credential.go b/internal/rootcoord/ddl_callbacks_rbac_credential.go index ad01ca11c8..7612e50783 100644 --- a/internal/rootcoord/ddl_callbacks_rbac_credential.go +++ b/internal/rootcoord/ddl_callbacks_rbac_credential.go @@ -88,8 +88,10 @@ func (c *DDLCallback) alterUserV2AckCallback(ctx context.Context, result message return merr.Wrap(err, "failed to alter credential") } // update proxy's local cache - if err := c.UpdateCredCache(ctx, result.Message.MustBody().CredentialInfo); err != nil { - return merr.Wrap(err, "failed to update cred cache") + if result.Message.MustBody().CredentialInfo.GetSha256Password() != "" { + if err := c.UpdateCredCache(ctx, result.Message.MustBody().CredentialInfo); err != nil { + return merr.Wrap(err, "failed to update cred cache") + } } return nil } diff --git a/internal/rootcoord/ddl_callbacks_rbac_credential_test.go b/internal/rootcoord/ddl_callbacks_rbac_credential_test.go index 6ce1063eaa..9581eeaa62 100644 --- a/internal/rootcoord/ddl_callbacks_rbac_credential_test.go +++ b/internal/rootcoord/ddl_callbacks_rbac_credential_test.go @@ -18,14 +18,20 @@ package rootcoord import ( "context" + "sync/atomic" "testing" + "github.com/cockroachdb/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/milvus-io/milvus-proto/go-api/v3/commonpb" "github.com/milvus-io/milvus-proto/go-api/v3/milvuspb" + "github.com/milvus-io/milvus/internal/util/proxyutil" "github.com/milvus-io/milvus/pkg/v3/proto/internalpb" + "github.com/milvus-io/milvus/pkg/v3/proto/proxypb" "github.com/milvus-io/milvus/pkg/v3/proto/rootcoordpb" + "github.com/milvus-io/milvus/pkg/v3/streaming/util/message" "github.com/milvus-io/milvus/pkg/v3/util/funcutil" "github.com/milvus-io/milvus/pkg/v3/util/merr" ) @@ -70,6 +76,7 @@ func TestDDLCallbacksRBACCredential(t *testing.T) { status, err = core.UpdateCredential(context.Background(), &internalpb.CredentialInfo{ Username: testUserName, EncryptedPassword: "1234567", + Sha256Password: "1234567-sha256", }) require.NoError(t, merr.CheckRPCCall(status, err)) getCredentialResp, err = core.GetCredential(context.Background(), &rootcoordpb.GetCredentialRequest{ @@ -88,3 +95,81 @@ func TestDDLCallbacksRBACCredential(t *testing.T) { }) require.Error(t, merr.CheckRPCCall(getCredentialResp.Status, err)) } + +func TestAlterUserV2AckCallbackSkipsCredentialCacheForDescriptionOnlyUpdate(t *testing.T) { + description := "description only" + var updateCacheCalls atomic.Int32 + + meta := newMockMetaTable() + meta.AlterCredentialFunc = func(ctx context.Context, msg message.BroadcastResultAlterUserMessageV2) error { + return nil + } + + core := newTestCore(withMeta(meta)) + core.proxyClientManager = proxyutil.NewProxyClientManager(proxyutil.DefaultProxyCreator) + proxy := newMockProxy() + proxy.UpdateCredentialCacheFunc = func(ctx context.Context, request *proxypb.UpdateCredCacheRequest) (*commonpb.Status, error) { + updateCacheCalls.Add(1) + return merr.Success(), nil + } + core.proxyClientManager.GetProxyClients().Insert(TestProxyID, proxy) + + err := (&DDLCallback{Core: core}).alterUserV2AckCallback(context.Background(), buildAlterUserMessage(&internalpb.CredentialInfo{ + Username: "desc_user", + Description: &description, + }, 1)) + + require.NoError(t, err) + assert.Zero(t, updateCacheCalls.Load()) +} + +func TestAlterUserV2AckCallbackUpdatesCredentialCacheForPasswordUpdate(t *testing.T) { + var updateCacheCalls atomic.Int32 + + meta := newMockMetaTable() + meta.AlterCredentialFunc = func(ctx context.Context, msg message.BroadcastResultAlterUserMessageV2) error { + return nil + } + + core := newTestCore(withMeta(meta)) + core.proxyClientManager = proxyutil.NewProxyClientManager(proxyutil.DefaultProxyCreator) + proxy := newMockProxy() + proxy.UpdateCredentialCacheFunc = func(ctx context.Context, request *proxypb.UpdateCredCacheRequest) (*commonpb.Status, error) { + updateCacheCalls.Add(1) + assert.Equal(t, "desc_user", request.GetUsername()) + assert.Equal(t, "sha256", request.GetPassword()) + return merr.Success(), nil + } + core.proxyClientManager.GetProxyClients().Insert(TestProxyID, proxy) + + err := (&DDLCallback{Core: core}).alterUserV2AckCallback(context.Background(), buildAlterUserMessage(&internalpb.CredentialInfo{ + Username: "desc_user", + Sha256Password: "sha256", + }, 1)) + + require.NoError(t, err) + assert.Equal(t, int32(1), updateCacheCalls.Load()) +} + +func TestAlterUserV2AckCallbackReturnsCredentialCacheError(t *testing.T) { + meta := newMockMetaTable() + meta.AlterCredentialFunc = func(ctx context.Context, msg message.BroadcastResultAlterUserMessageV2) error { + return nil + } + + core := newTestCore(withMeta(meta)) + core.proxyClientManager = proxyutil.NewProxyClientManager(proxyutil.DefaultProxyCreator) + proxy := newMockProxy() + proxy.UpdateCredentialCacheFunc = func(ctx context.Context, request *proxypb.UpdateCredCacheRequest) (*commonpb.Status, error) { + return nil, errors.New("cache update failed") + } + core.proxyClientManager.GetProxyClients().Insert(TestProxyID, proxy) + + err := (&DDLCallback{Core: core}).alterUserV2AckCallback(context.Background(), buildAlterUserMessage(&internalpb.CredentialInfo{ + Username: "desc_user", + Sha256Password: "sha256", + }, 1)) + + require.Error(t, err) + assert.ErrorContains(t, err, "failed to update cred cache") +} diff --git a/internal/rootcoord/meta_table.go b/internal/rootcoord/meta_table.go index b4974d1588..4f460704fc 100644 --- a/internal/rootcoord/meta_table.go +++ b/internal/rootcoord/meta_table.go @@ -1741,6 +1741,14 @@ func (mt *MetaTable) CheckIfUpdateCredential(ctx context.Context, credInfo *inte if funcutil.IsEmptyString(credInfo.GetUsername()) { return merr.WrapErrParameterInvalidMsg("username is empty") } + hasEncryptedPassword := credInfo.GetEncryptedPassword() != "" + hasSha256Password := credInfo.GetSha256Password() != "" + if hasEncryptedPassword != hasSha256Password { + return merr.WrapErrParameterInvalidMsg("credential password update must include both encrypted and sha256 password") + } + if !hasEncryptedPassword && !hasSha256Password && credInfo.Description == nil { + return merr.WrapErrParameterInvalidMsg("credential update must change password or description") + } mt.permissionLock.RLock() defer mt.permissionLock.RUnlock() @@ -1774,9 +1782,20 @@ func (mt *MetaTable) AlterCredential(ctx context.Context, result message.Broadca ) return nil } + encryptedPassword := body.CredentialInfo.EncryptedPassword + description := body.CredentialInfo.GetDescription() + if existsCredential != nil { + if encryptedPassword == "" { + encryptedPassword = existsCredential.EncryptedPassword + } + if body.CredentialInfo.Description == nil { + description = existsCredential.Description + } + } credential := &model.Credential{ Username: body.CredentialInfo.Username, - EncryptedPassword: body.CredentialInfo.EncryptedPassword, + EncryptedPassword: encryptedPassword, + Description: description, TimeTick: result.GetControlChannelResult().TimeTick, } return mt.catalog.AlterCredential(ctx, credential) diff --git a/internal/rootcoord/meta_table_test.go b/internal/rootcoord/meta_table_test.go index ab6cf755ef..2999d7e5e8 100644 --- a/internal/rootcoord/meta_table_test.go +++ b/internal/rootcoord/meta_table_test.go @@ -162,6 +162,90 @@ func TestRbacCredential(t *testing.T) { require.NoError(t, err) } +func TestRbacCredentialAlterCredentialMergesPartialUpdates(t *testing.T) { + mt := generateMetaTable(t) + + ptr := func(s string) *string { + return &s + } + + username := "user" + funcutil.RandomString(10) + err := mt.AlterCredential(context.TODO(), buildAlterUserMessage(&internalpb.CredentialInfo{ + Username: username, + EncryptedPassword: "old-password", + Description: ptr("initial description"), + }, 1)) + require.NoError(t, err) + + err = mt.AlterCredential(context.TODO(), buildAlterUserMessage(&internalpb.CredentialInfo{ + Username: username, + EncryptedPassword: "new-password", + }, 2)) + require.NoError(t, err) + cred, err := mt.GetCredential(context.TODO(), username) + require.NoError(t, err) + assert.Equal(t, "new-password", cred.GetEncryptedPassword()) + assert.Equal(t, "initial description", cred.GetDescription()) + + err = mt.AlterCredential(context.TODO(), buildAlterUserMessage(&internalpb.CredentialInfo{ + Username: username, + Description: ptr("updated description"), + }, 3)) + require.NoError(t, err) + cred, err = mt.GetCredential(context.TODO(), username) + require.NoError(t, err) + assert.Equal(t, "new-password", cred.GetEncryptedPassword()) + assert.Equal(t, "updated description", cred.GetDescription()) + + err = mt.AlterCredential(context.TODO(), buildAlterUserMessage(&internalpb.CredentialInfo{ + Username: username, + Description: ptr(""), + }, 4)) + require.NoError(t, err) + cred, err = mt.GetCredential(context.TODO(), username) + require.NoError(t, err) + assert.Equal(t, "new-password", cred.GetEncryptedPassword()) + assert.Equal(t, "", cred.GetDescription()) +} + +func TestRbacCredentialRejectsInconsistentPasswordUpdate(t *testing.T) { + mt := generateMetaTable(t) + + username := "user" + funcutil.RandomString(10) + err := mt.AlterCredential(context.TODO(), buildAlterUserMessage(&internalpb.CredentialInfo{ + Username: username, + EncryptedPassword: "old-password", + }, 1)) + require.NoError(t, err) + + err = mt.CheckIfUpdateCredential(context.TODO(), &internalpb.CredentialInfo{ + Username: username, + EncryptedPassword: "new-password", + }) + require.Error(t, err) + assert.ErrorContains(t, err, "must include both encrypted and sha256 password") + + err = mt.CheckIfUpdateCredential(context.TODO(), &internalpb.CredentialInfo{ + Username: username, + Sha256Password: "sha256", + }) + require.Error(t, err) + assert.ErrorContains(t, err, "must include both encrypted and sha256 password") + + description := "description-only update" + err = mt.CheckIfUpdateCredential(context.TODO(), &internalpb.CredentialInfo{ + Username: username, + Description: &description, + }) + require.NoError(t, err) + + err = mt.CheckIfUpdateCredential(context.TODO(), &internalpb.CredentialInfo{ + Username: username, + }) + require.Error(t, err) + assert.ErrorContains(t, err, "credential update must change password or description") +} + func TestRbacCreateRole(t *testing.T) { mt := generateMetaTable(t) diff --git a/internal/rootcoord/root_coord.go b/internal/rootcoord/root_coord.go index acfa336ebf..8c912adb1f 100644 --- a/internal/rootcoord/root_coord.go +++ b/internal/rootcoord/root_coord.go @@ -2199,7 +2199,6 @@ func (c *Core) CreateCredential(ctx context.Context, credInfo *internalpb.Creden if err := merr.CheckHealthy(c.GetStateCode()); err != nil { return merr.Status(err), nil } - if err := c.broadcastAlterUserForCreateCredential(ctx, credInfo); err != nil { ctxLog.Warn("CreateCredential failed", zap.Error(err)) metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.FailLabel).Inc() @@ -2255,7 +2254,6 @@ func (c *Core) UpdateCredential(ctx context.Context, credInfo *internalpb.Creden if err := merr.CheckHealthy(c.GetStateCode()); err != nil { return merr.Status(err), nil } - if err := c.broadcastAlterUserForUpdateCredential(ctx, credInfo); err != nil { ctxLog.Warn("UpdateCredential append message failed", zap.Error(err)) metrics.RootCoordDDLReqCounter.WithLabelValues(method, metrics.FailLabel).Inc() diff --git a/pkg/go.mod b/pkg/go.mod index 728c9c6b0b..f57576c2cd 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -22,7 +22,7 @@ require ( github.com/jolestar/go-commons-pool/v2 v2.1.2 github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 github.com/klauspost/compress v1.18.0 - github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260514121806-9b60a053353f + github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e github.com/minio/minio-go/v7 v7.0.73 github.com/panjf2000/ants/v2 v2.11.3 github.com/prometheus/client_golang v1.20.5 diff --git a/pkg/go.sum b/pkg/go.sum index 50a4355620..604792942c 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -498,8 +498,8 @@ github.com/milvus-io/cgosymbolizer v0.0.0-20250318084424-114f4050c3a6 h1:YHMFI6L github.com/milvus-io/cgosymbolizer v0.0.0-20250318084424-114f4050c3a6/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/v3 v3.0.0-20260514121806-9b60a053353f h1:Z2paGeFL0i1a3Dqw82DwRwZ2ix3FtyKU7+u6rR7jbUs= -github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260514121806-9b60a053353f/go.mod h1:rbKpv5JToISTKTTLl0duL5r6wbYnjJ9SsD0QgXMzKy0= +github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e h1:axPnOhjsWqqxk0dOQRSIU2mdg52Kyl/8E6iDFZy/IpE= +github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e/go.mod h1:rbKpv5JToISTKTTLl0duL5r6wbYnjJ9SsD0QgXMzKy0= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.73 h1:qr2vi96Qm7kZ4v7LLebjte+MQh621fFWnv93p12htEo= diff --git a/pkg/proto/internal.proto b/pkg/proto/internal.proto index 22b56330f3..be78ce7cad 100644 --- a/pkg/proto/internal.proto +++ b/pkg/proto/internal.proto @@ -329,6 +329,7 @@ message CredentialInfo { // encrypted by sha256 (for good performance in cache mapping) string sha256_password = 5; // not save in metadata. uint64 time_tick = 6; // the timetick in wal which the credential updates + optional string description = 7; } message ListPolicyRequest { diff --git a/pkg/proto/internalpb/internal.pb.go b/pkg/proto/internalpb/internal.pb.go index 520575ad1d..38433f717a 100644 --- a/pkg/proto/internalpb/internal.pb.go +++ b/pkg/proto/internalpb/internal.pb.go @@ -2945,8 +2945,9 @@ type CredentialInfo struct { // Deprecated: Marked as deprecated in internal.proto. IsSuper bool `protobuf:"varint,4,opt,name=is_super,json=isSuper,proto3" json:"is_super,omitempty"` // not used. // encrypted by sha256 (for good performance in cache mapping) - Sha256Password string `protobuf:"bytes,5,opt,name=sha256_password,json=sha256Password,proto3" json:"sha256_password,omitempty"` // not save in metadata. - TimeTick uint64 `protobuf:"varint,6,opt,name=time_tick,json=timeTick,proto3" json:"time_tick,omitempty"` // the timetick in wal which the credential updates + Sha256Password string `protobuf:"bytes,5,opt,name=sha256_password,json=sha256Password,proto3" json:"sha256_password,omitempty"` // not save in metadata. + TimeTick uint64 `protobuf:"varint,6,opt,name=time_tick,json=timeTick,proto3" json:"time_tick,omitempty"` // the timetick in wal which the credential updates + Description *string `protobuf:"bytes,7,opt,name=description,proto3,oneof" json:"description,omitempty"` } func (x *CredentialInfo) Reset() { @@ -3025,6 +3026,13 @@ func (x *CredentialInfo) GetTimeTick() uint64 { return 0 } +func (x *CredentialInfo) GetDescription() string { + if x != nil && x.Description != nil { + return *x.Description + } + return "" +} + type ListPolicyRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -5292,7 +5300,7 @@ var file_internal_proto_rawDesc = []byte{ 0x20, 0x03, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x64, 0x65, 0x66, - 0x61, 0x75, 0x6c, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0xdc, 0x01, + 0x61, 0x75, 0x6c, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x93, 0x02, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x12, @@ -5306,304 +5314,308 @@ var file_internal_proto_rawDesc = []byte{ 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x69, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x69, 0x63, 0x6b, 0x22, 0x45, 0x0a, 0x11, - 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x30, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1c, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x73, 0x67, 0x42, 0x61, 0x73, 0x65, 0x52, 0x04, 0x62, - 0x61, 0x73, 0x65, 0x22, 0xdf, 0x01, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6f, 0x6c, 0x69, - 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, + 0x28, 0x04, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x54, 0x69, 0x63, 0x6b, 0x12, 0x25, 0x0a, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x22, 0x45, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x73, 0x67, + 0x42, 0x61, 0x73, 0x65, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x22, 0xdf, 0x01, 0x0a, 0x12, 0x4c, + 0x69, 0x73, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, + 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x6f, + 0x6c, 0x69, 0x63, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x75, + 0x73, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x52, 0x0a, 0x10, 0x70, 0x72, 0x69, 0x76, + 0x69, 0x6c, 0x65, 0x67, 0x65, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, + 0x67, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x70, 0x72, 0x69, + 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0x67, 0x0a, 0x19, + 0x53, 0x68, 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x04, 0x62, 0x61, 0x73, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x73, + 0x67, 0x42, 0x61, 0x73, 0x65, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, + 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, + 0x74, 0x74, 0x65, 0x72, 0x6e, 0x22, 0x9a, 0x01, 0x0a, 0x1a, 0x53, 0x68, 0x6f, 0x77, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x47, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, + 0x61, 0x69, 0x72, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x22, 0x45, 0x0a, 0x04, 0x52, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x02, 0x72, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, + 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x02, 0x72, 0x74, 0x12, 0x0c, 0x0a, 0x01, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x01, 0x72, 0x22, 0x32, 0x0a, 0x0a, 0x49, 0x6d, 0x70, + 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x22, 0xb7, 0x03, + 0x0a, 0x15, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x16, 0x0a, 0x04, 0x64, 0x62, 0x49, 0x44, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x04, 0x64, 0x62, 0x49, 0x44, 0x12, + 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x44, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, + 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x03, 0x52, 0x0c, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x73, + 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x3d, 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x06, 0x73, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x12, 0x37, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x49, 0x6d, 0x70, 0x6f, + 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x3b, 0x0a, + 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, + 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, 0x61, 0x69, + 0x72, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x61, + 0x74, 0x61, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0d, 0x64, 0x61, 0x74, 0x61, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x12, 0x14, 0x0a, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x44, 0x22, 0xee, 0x01, 0x0a, 0x0d, 0x49, 0x6d, 0x70, 0x6f, + 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x62, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x70, + 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, + 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x07, 0x6f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6d, + 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, 0x61, 0x69, 0x72, 0x52, + 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x5b, 0x0a, 0x0e, 0x49, 0x6d, 0x70, 0x6f, + 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, - 0x21, 0x0a, 0x0c, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x6e, 0x66, - 0x6f, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x52, 0x6f, 0x6c, 0x65, - 0x73, 0x12, 0x52, 0x0a, 0x10, 0x70, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x5f, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6d, 0x69, - 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, - 0x73, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0f, 0x70, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0x67, 0x0a, 0x19, 0x53, 0x68, 0x6f, 0x77, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x30, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x73, 0x67, 0x42, 0x61, 0x73, 0x65, 0x52, 0x04, - 0x62, 0x61, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x22, 0x9a, - 0x01, 0x0a, 0x1a, 0x53, 0x68, 0x6f, 0x77, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, - 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x47, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, 0x76, - 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, 0x61, 0x69, 0x72, 0x52, 0x0d, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x45, 0x0a, 0x04, 0x52, - 0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x02, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x1f, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x52, 0x02, 0x72, 0x74, 0x12, 0x0c, 0x0a, 0x01, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, - 0x01, 0x72, 0x22, 0x32, 0x0a, 0x0a, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x05, 0x70, 0x61, 0x74, 0x68, 0x73, 0x22, 0xb7, 0x03, 0x0a, 0x15, 0x49, 0x6d, 0x70, 0x6f, 0x72, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x12, 0x16, 0x0a, 0x04, 0x64, 0x62, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, - 0x18, 0x01, 0x52, 0x04, 0x64, 0x62, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x27, 0x0a, 0x0f, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0c, 0x70, 0x61, 0x72, - 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, 0x61, - 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x3d, - 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, - 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x37, 0x0a, - 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6d, - 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, - 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4b, 0x65, - 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x50, 0x61, 0x69, 0x72, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x64, 0x61, 0x74, - 0x61, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x6a, 0x6f, - 0x62, 0x49, 0x44, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x44, - 0x22, 0xee, 0x01, 0x0a, 0x0d, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x61, - 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x66, - 0x69, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, - 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, - 0x69, 0x6c, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x50, 0x61, 0x69, 0x72, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x22, 0x5b, 0x0a, 0x0e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6a, 0x6f, 0x62, 0x49, - 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x44, 0x22, 0x49, - 0x0a, 0x18, 0x47, 0x65, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, - 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x62, - 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x44, 0x22, 0x81, 0x02, 0x0a, 0x12, 0x49, 0x6d, - 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, - 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, - 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, - 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x23, - 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x54, - 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6d, 0x70, - 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0c, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x1d, - 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x6f, 0x77, 0x73, 0x22, 0xc8, 0x03, - 0x0a, 0x19, 0x47, 0x65, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, - 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, - 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x25, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4a, 0x6f, - 0x62, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, - 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, - 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x52, 0x0a, 0x0f, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, - 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, - 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x50, 0x72, 0x6f, 0x67, 0x72, - 0x65, 0x73, 0x73, 0x52, 0x0e, 0x74, 0x61, 0x73, 0x6b, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, - 0x73, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, - 0x72, 0x6f, 0x77, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x6d, 0x70, 0x6f, - 0x72, 0x74, 0x65, 0x64, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, - 0x6c, 0x5f, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, - 0x74, 0x61, 0x6c, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x54, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, - 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x62, 0x49, 0x44, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x64, 0x62, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x22, 0x56, - 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, - 0x0f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x86, 0x02, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x49, - 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, - 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, - 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6a, 0x6f, 0x62, 0x49, 0x44, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x06, 0x6a, 0x6f, 0x62, 0x49, 0x44, 0x73, 0x12, 0x3d, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6d, 0x69, - 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, - 0x61, 0x73, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x61, - 0x73, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, - 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, - 0x73, 0x73, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x22, - 0x74, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x62, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, - 0x49, 0x44, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0a, 0x73, 0x65, 0x67, 0x6d, 0x65, - 0x6e, 0x74, 0x49, 0x44, 0x73, 0x22, 0x3f, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x69, - 0x6e, 0x6c, 0x6f, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x49, 0x44, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x49, 0x44, 0x12, 0x16, - 0x0a, 0x06, 0x6c, 0x6f, 0x67, 0x49, 0x44, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x03, 0x52, 0x06, - 0x6c, 0x6f, 0x67, 0x49, 0x44, 0x73, 0x22, 0x82, 0x04, 0x0a, 0x0b, 0x53, 0x65, 0x67, 0x6d, 0x65, - 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, - 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x65, 0x67, 0x6d, 0x65, - 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x74, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x70, - 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x43, - 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x75, 0x6d, 0x5f, 0x72, 0x6f, - 0x77, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x6e, 0x75, 0x6d, 0x52, 0x6f, 0x77, - 0x73, 0x12, 0x37, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x6c, 0x65, - 0x76, 0x65, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, 0x76, - 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, - 0x76, 0x65, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x73, 0x6f, 0x72, 0x74, 0x65, 0x64, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x53, 0x6f, 0x72, 0x74, 0x65, 0x64, - 0x12, 0x43, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x73, 0x18, - 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x46, 0x69, - 0x65, 0x6c, 0x64, 0x42, 0x69, 0x6e, 0x6c, 0x6f, 0x67, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x65, 0x72, - 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x41, 0x0a, 0x0a, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x5f, 0x6c, - 0x6f, 0x67, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6d, 0x69, 0x6c, 0x76, - 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x69, 0x6e, 0x6c, 0x6f, 0x67, 0x52, 0x09, 0x64, - 0x65, 0x6c, 0x74, 0x61, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x41, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, - 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6d, - 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x69, 0x6e, 0x6c, 0x6f, 0x67, - 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x73, 0x4c, 0x6f, 0x67, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x17, - 0x47, 0x65, 0x74, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x0c, - 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x53, 0x65, 0x67, 0x6d, 0x65, - 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0c, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x73, 0x22, 0x4a, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, - 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, - 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4d, 0x73, 0x67, 0x42, 0x61, 0x73, 0x65, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, - 0x22, 0x71, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x4d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, - 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x49, - 0x6e, 0x66, 0x6f, 0x22, 0x6d, 0x0a, 0x10, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, - 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, - 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, - 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x61, - 0x6d, 0x65, 0x22, 0x7a, 0x0a, 0x17, 0x53, 0x79, 0x6e, 0x63, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x45, 0x0a, - 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x27, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x5d, - 0x0a, 0x10, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x45, 0x7a, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x30, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x73, 0x67, 0x42, 0x61, 0x73, 0x65, 0x52, 0x04, - 0x62, 0x61, 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x5a, 0x0a, - 0x11, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x45, 0x7a, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x14, 0x0a, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6a, 0x6f, 0x62, 0x49, 0x44, 0x22, 0x49, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x49, 0x6d, 0x70, 0x6f, + 0x72, 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6a, 0x6f, + 0x62, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x44, + 0x22, 0x81, 0x02, 0x0a, 0x12, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x50, + 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x73, 0x69, 0x7a, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, + 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x77, + 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, + 0x64, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x72, + 0x6f, 0x77, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x52, 0x6f, 0x77, 0x73, 0x22, 0xc8, 0x03, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x49, 0x6d, 0x70, 0x6f, + 0x72, 0x74, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, - 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x7a, 0x6b, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x7a, 0x6b, 0x2a, 0x59, 0x0a, 0x0a, 0x53, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, - 0x4c, 0x54, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x50, 0x55, 0x52, 0x45, 0x5f, 0x41, 0x4e, 0x4e, - 0x5f, 0x53, 0x45, 0x41, 0x52, 0x43, 0x48, 0x5f, 0x4e, 0x4f, 0x5f, 0x46, 0x49, 0x4c, 0x54, 0x45, - 0x52, 0x10, 0x01, 0x12, 0x1f, 0x0a, 0x1b, 0x50, 0x55, 0x52, 0x45, 0x5f, 0x41, 0x4e, 0x4e, 0x5f, - 0x53, 0x45, 0x41, 0x52, 0x43, 0x48, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x46, 0x49, 0x4c, 0x54, - 0x45, 0x52, 0x10, 0x02, 0x2a, 0x45, 0x0a, 0x09, 0x52, 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, - 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x10, 0x00, 0x12, 0x0c, - 0x0a, 0x08, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, - 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, - 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x03, 0x2a, 0xc8, 0x01, 0x0a, 0x08, - 0x52, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x44, 0x44, 0x4c, 0x43, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x44, - 0x44, 0x4c, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x01, 0x12, 0x0c, 0x0a, - 0x08, 0x44, 0x44, 0x4c, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x44, - 0x44, 0x4c, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x44, 0x44, 0x4c, - 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, - 0x44, 0x4d, 0x4c, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x44, - 0x4d, 0x4c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x10, 0x06, 0x12, 0x0f, 0x0a, 0x0b, 0x44, 0x4d, - 0x4c, 0x42, 0x75, 0x6c, 0x6b, 0x4c, 0x6f, 0x61, 0x64, 0x10, 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x44, - 0x51, 0x4c, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x10, 0x08, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x51, - 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x10, 0x09, 0x12, 0x11, 0x0a, 0x09, 0x44, 0x4d, 0x4c, 0x55, - 0x70, 0x73, 0x65, 0x72, 0x74, 0x10, 0x0a, 0x1a, 0x02, 0x08, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x44, - 0x44, 0x4c, 0x44, 0x42, 0x10, 0x0b, 0x2a, 0xa4, 0x01, 0x0a, 0x0e, 0x49, 0x6d, 0x70, 0x6f, 0x72, - 0x74, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x6f, 0x6e, - 0x65, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x10, 0x01, - 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x72, 0x65, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, - 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x10, - 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x04, 0x12, 0x0d, 0x0a, - 0x09, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x10, 0x05, 0x12, 0x11, 0x0a, 0x0d, - 0x49, 0x6e, 0x64, 0x65, 0x78, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x67, 0x10, 0x06, 0x12, - 0x0b, 0x0a, 0x07, 0x53, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x10, 0x07, 0x12, 0x0f, 0x0a, 0x0b, - 0x55, 0x6e, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x10, 0x08, 0x12, 0x0e, 0x0a, - 0x0a, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x10, 0x09, 0x42, 0x35, 0x5a, - 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x69, 0x6c, 0x76, - 0x75, 0x73, 0x2d, 0x69, 0x6f, 0x2f, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2f, 0x70, 0x6b, 0x67, - 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x49, + 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, + 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, + 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0e, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x52, 0x0a, 0x0f, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x70, + 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x29, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, + 0x73, 0x6b, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x0e, 0x74, 0x61, 0x73, 0x6b, + 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0c, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x52, 0x6f, 0x77, 0x73, 0x12, + 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x1f, + 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, + 0x54, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x12, 0x0a, + 0x04, 0x64, 0x62, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x64, 0x62, 0x49, + 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x22, 0x56, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x70, + 0x6f, 0x72, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x64, + 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x86, 0x02, + 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6a, 0x6f, + 0x62, 0x49, 0x44, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6a, 0x6f, 0x62, 0x49, + 0x44, 0x73, 0x12, 0x3d, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x73, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x70, + 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x03, 0x52, + 0x0a, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x63, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, + 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0x74, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x53, 0x65, 0x67, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x16, 0x0a, 0x06, 0x64, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x64, 0x62, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x1e, 0x0a, 0x0a, + 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x03, + 0x52, 0x0a, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x73, 0x22, 0x3f, 0x0a, 0x0b, + 0x46, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x69, 0x6e, 0x6c, 0x6f, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x66, + 0x69, 0x65, 0x6c, 0x64, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x67, 0x49, 0x44, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x03, 0x52, 0x06, 0x6c, 0x6f, 0x67, 0x49, 0x44, 0x73, 0x22, 0x82, 0x04, + 0x0a, 0x0b, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, + 0x09, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x09, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x63, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, + 0x20, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x19, 0x0a, + 0x08, 0x6e, 0x75, 0x6d, 0x5f, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x07, 0x6e, 0x75, 0x6d, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x37, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, + 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x37, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x21, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x65, + 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, + 0x5f, 0x73, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, + 0x73, 0x53, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x12, 0x43, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x65, 0x72, + 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6d, + 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x69, 0x6e, 0x6c, 0x6f, 0x67, + 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x41, 0x0a, 0x0a, + 0x64, 0x65, 0x6c, 0x74, 0x61, 0x5f, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x22, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x69, + 0x6e, 0x6c, 0x6f, 0x67, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x4c, 0x6f, 0x67, 0x73, 0x12, + 0x41, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x73, 0x5f, 0x6c, 0x6f, 0x67, 0x73, 0x18, 0x0b, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x42, 0x69, 0x6e, 0x6c, 0x6f, 0x67, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x73, 0x4c, 0x6f, + 0x67, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x0c, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, + 0x66, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6d, 0x69, 0x6c, 0x76, + 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x53, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0c, 0x73, + 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x73, 0x22, 0x4a, 0x0a, 0x16, 0x47, + 0x65, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x73, 0x67, 0x42, 0x61, 0x73, + 0x65, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x22, 0x71, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x51, 0x75, + 0x6f, 0x74, 0x61, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x6d, 0x65, 0x74, 0x72, 0x69, + 0x63, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, + 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x6d, 0x0a, 0x10, 0x46, 0x69, + 0x6c, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x7a, 0x0a, 0x17, 0x53, 0x79, 0x6e, + 0x63, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x45, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x5d, 0x0a, 0x10, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x45, + 0x7a, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x04, 0x62, 0x61, 0x73, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x73, + 0x67, 0x42, 0x61, 0x73, 0x65, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, + 0x62, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, + 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x5a, 0x0a, 0x11, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x45, 0x7a, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x69, 0x6c, 0x76, + 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x10, + 0x0a, 0x03, 0x65, 0x7a, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x7a, 0x6b, + 0x2a, 0x59, 0x0a, 0x0a, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, + 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x50, + 0x55, 0x52, 0x45, 0x5f, 0x41, 0x4e, 0x4e, 0x5f, 0x53, 0x45, 0x41, 0x52, 0x43, 0x48, 0x5f, 0x4e, + 0x4f, 0x5f, 0x46, 0x49, 0x4c, 0x54, 0x45, 0x52, 0x10, 0x01, 0x12, 0x1f, 0x0a, 0x1b, 0x50, 0x55, + 0x52, 0x45, 0x5f, 0x41, 0x4e, 0x4e, 0x5f, 0x53, 0x45, 0x41, 0x52, 0x43, 0x48, 0x5f, 0x57, 0x49, + 0x54, 0x48, 0x5f, 0x46, 0x49, 0x4c, 0x54, 0x45, 0x52, 0x10, 0x02, 0x2a, 0x45, 0x0a, 0x09, 0x52, + 0x61, 0x74, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, + 0x74, 0x65, 0x72, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, + 0x65, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x10, 0x03, 0x2a, 0xc8, 0x01, 0x0a, 0x08, 0x52, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x11, 0x0a, 0x0d, 0x44, 0x44, 0x4c, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x44, 0x44, 0x4c, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x44, 0x4c, 0x49, 0x6e, 0x64, 0x65, 0x78, + 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x44, 0x4c, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x10, 0x03, + 0x12, 0x11, 0x0a, 0x0d, 0x44, 0x44, 0x4c, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x4d, 0x4c, 0x49, 0x6e, 0x73, 0x65, 0x72, 0x74, + 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x4d, 0x4c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x10, + 0x06, 0x12, 0x0f, 0x0a, 0x0b, 0x44, 0x4d, 0x4c, 0x42, 0x75, 0x6c, 0x6b, 0x4c, 0x6f, 0x61, 0x64, + 0x10, 0x07, 0x12, 0x0d, 0x0a, 0x09, 0x44, 0x51, 0x4c, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x10, + 0x08, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x51, 0x4c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x10, 0x09, 0x12, + 0x11, 0x0a, 0x09, 0x44, 0x4d, 0x4c, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x10, 0x0a, 0x1a, 0x02, + 0x08, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x44, 0x4c, 0x44, 0x42, 0x10, 0x0b, 0x2a, 0xa4, 0x01, + 0x0a, 0x0e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x6f, 0x6e, 0x65, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x65, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x72, 0x65, 0x49, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x6d, 0x70, + 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x61, 0x69, 0x6c, + 0x65, 0x64, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, + 0x64, 0x10, 0x05, 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x42, 0x75, 0x69, 0x6c, + 0x64, 0x69, 0x6e, 0x67, 0x10, 0x06, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x6f, 0x72, 0x74, 0x69, 0x6e, + 0x67, 0x10, 0x07, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x6e, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, + 0x65, 0x64, 0x10, 0x08, 0x12, 0x0e, 0x0a, 0x0a, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x10, 0x09, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x6d, 0x69, 0x6c, 0x76, 0x75, 0x73, 0x2d, 0x69, 0x6f, 0x2f, 0x6d, 0x69, 0x6c, + 0x76, 0x75, 0x73, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -6434,6 +6446,7 @@ func file_internal_proto_init() { } } } + file_internal_proto_msgTypes[28].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/pkg/util/paramtable/component_param.go b/pkg/util/paramtable/component_param.go index deb577e53d..1d7f7b32c7 100644 --- a/pkg/util/paramtable/component_param.go +++ b/pkg/util/paramtable/component_param.go @@ -2053,6 +2053,7 @@ type proxyConfig struct { MaxNameLength ParamItem `refreshable:"true"` MaxCollectionDescriptionLength ParamItem `refreshable:"true"` MaxUsernameLength ParamItem `refreshable:"true"` + MaxUserDescriptionLength ParamItem `refreshable:"true"` MinPasswordLength ParamItem `refreshable:"true"` MaxPasswordLength ParamItem `refreshable:"true"` MaxFieldNum ParamItem `refreshable:"true"` @@ -2175,6 +2176,14 @@ func (p *proxyConfig) init(base *BaseTable) { } p.MaxUsernameLength.Init(base.mgr) + p.MaxUserDescriptionLength = ParamItem{ + Key: "proxy.maxUserDescriptionLength", + DefaultValue: "1024", + Version: "2.6.19", + PanicIfEmpty: true, + } + p.MaxUserDescriptionLength.Init(base.mgr) + p.MaxPasswordLength = ParamItem{ Key: "proxy.maxPasswordLength", DefaultValue: "72", // bcrypt max length diff --git a/pkg/util/paramtable/component_param_test.go b/pkg/util/paramtable/component_param_test.go index 25ab1e9d0f..791152ca56 100644 --- a/pkg/util/paramtable/component_param_test.go +++ b/pkg/util/paramtable/component_param_test.go @@ -236,6 +236,7 @@ func TestComponentParam(t *testing.T) { t.Logf("MsgStreamTimeTickBufSize: %d", Params.MsgStreamTimeTickBufSize.GetAsInt64()) t.Logf("MaxNameLength: %d", Params.MaxNameLength.GetAsInt64()) + assert.Equal(t, 1024, Params.MaxUserDescriptionLength.GetAsInt()) t.Logf("MaxFieldNum: %d", Params.MaxFieldNum.GetAsInt64()) diff --git a/tests/go_client/go.mod b/tests/go_client/go.mod index 9372447ef3..3b958bd0dd 100644 --- a/tests/go_client/go.mod +++ b/tests/go_client/go.mod @@ -5,7 +5,7 @@ go 1.26.4 require ( github.com/apache/arrow/go/v17 v17.0.0 github.com/cockroachdb/errors v1.9.1 - github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260526093827-489331a5a41f + github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e github.com/milvus-io/milvus/client/v2 v2.0.0-20241125024034-0b9edb62a92d github.com/milvus-io/milvus/pkg/v3 v3.0.0-beta github.com/minio/minio-go/v7 v7.0.73 diff --git a/tests/go_client/go.sum b/tests/go_client/go.sum index 13bded1632..4bad4e52cc 100644 --- a/tests/go_client/go.sum +++ b/tests/go_client/go.sum @@ -245,8 +245,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= -github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260526093827-489331a5a41f h1:W+paUzc+AA381Rydx2DJffoQuM4l+efVqO+2i9GdfMw= -github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260526093827-489331a5a41f/go.mod h1:rbKpv5JToISTKTTLl0duL5r6wbYnjJ9SsD0QgXMzKy0= +github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e h1:axPnOhjsWqqxk0dOQRSIU2mdg52Kyl/8E6iDFZy/IpE= +github.com/milvus-io/milvus-proto/go-api/v3 v3.0.0-20260612100615-43795e8f8f6e/go.mod h1:rbKpv5JToISTKTTLl0duL5r6wbYnjJ9SsD0QgXMzKy0= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= diff --git a/tests/integration/rbac/rbac_basic_test.go b/tests/integration/rbac/rbac_basic_test.go index 1be96b68f6..0b5b7d2f5d 100644 --- a/tests/integration/rbac/rbac_basic_test.go +++ b/tests/integration/rbac/rbac_basic_test.go @@ -26,6 +26,7 @@ import ( "github.com/milvus-io/milvus-proto/go-api/v3/commonpb" "github.com/milvus-io/milvus-proto/go-api/v3/milvuspb" "github.com/milvus-io/milvus/pkg/v3/util" + "github.com/milvus-io/milvus/pkg/v3/util/crypto" "github.com/milvus-io/milvus/pkg/v3/util/merr" "github.com/milvus-io/milvus/pkg/v3/util/paramtable" "github.com/milvus-io/milvus/tests/integration" @@ -107,6 +108,76 @@ func (s *RBACBasicTestSuite) TestDropRole() { } } +func (s *RBACBasicTestSuite) TestCredentialDescriptionRoundTrip() { + rootCtx := GetContext(context.Background(), defaultAuth) + userName := fmt.Sprintf("desc_user_%d", rand.Int31n(1000000)) + oldPassword := "old_password" + newPassword := "new_password" + description := "CJK user description \xe6\x9d\x83\xe9\x99\x90\xe7\x94\xa8\xe6\x88\xb7" + updatedDescription := "updated description \xe6\x9b\xb4\xe6\x96\xb0" + + resp, err := s.Cluster.MilvusClient.CreateCredential(rootCtx, &milvuspb.CreateCredentialRequest{ + Username: userName, + Password: crypto.Base64Encode(oldPassword), + Description: &description, + }) + s.NoError(err) + s.True(merr.Ok(resp)) + defer s.Cluster.MilvusClient.DeleteCredential(rootCtx, &milvuspb.DeleteCredentialRequest{Username: userName}) //nolint + + selectResp, err := s.Cluster.MilvusClient.SelectUser(rootCtx, &milvuspb.SelectUserRequest{ + User: &milvuspb.UserEntity{Name: userName}, + IncludeRoleInfo: false, + }) + s.NoError(err) + s.True(merr.Ok(selectResp.GetStatus())) + s.Len(selectResp.GetResults(), 1) + s.Equal(description, selectResp.GetResults()[0].GetDescription()) + + updateResp, err := s.Cluster.MilvusClient.UpdateCredential(rootCtx, &milvuspb.UpdateCredentialRequest{ + Username: userName, + Description: &updatedDescription, + }) + s.NoError(err) + s.True(merr.Ok(updateResp)) + + oldUserCtx := GetContext(context.Background(), fmt.Sprintf("%s:%s", userName, oldPassword)) + authResp, err := s.Cluster.MilvusClient.ShowCollections(oldUserCtx, &milvuspb.ShowCollectionsRequest{}) + s.NoError(err) + s.True(merr.Ok(authResp.GetStatus())) + + selectResp, err = s.Cluster.MilvusClient.SelectUser(rootCtx, &milvuspb.SelectUserRequest{ + User: &milvuspb.UserEntity{Name: userName}, + IncludeRoleInfo: false, + }) + s.NoError(err) + s.True(merr.Ok(selectResp.GetStatus())) + s.Len(selectResp.GetResults(), 1) + s.Equal(updatedDescription, selectResp.GetResults()[0].GetDescription()) + + updateResp, err = s.Cluster.MilvusClient.UpdateCredential(rootCtx, &milvuspb.UpdateCredentialRequest{ + Username: userName, + OldPassword: crypto.Base64Encode(oldPassword), + NewPassword: crypto.Base64Encode(newPassword), + }) + s.NoError(err) + s.True(merr.Ok(updateResp)) + + newUserCtx := GetContext(context.Background(), fmt.Sprintf("%s:%s", userName, newPassword)) + authResp, err = s.Cluster.MilvusClient.ShowCollections(newUserCtx, &milvuspb.ShowCollectionsRequest{}) + s.NoError(err) + s.True(merr.Ok(authResp.GetStatus())) + + selectResp, err = s.Cluster.MilvusClient.SelectUser(rootCtx, &milvuspb.SelectUserRequest{ + User: &milvuspb.UserEntity{Name: userName}, + IncludeRoleInfo: false, + }) + s.NoError(err) + s.True(merr.Ok(selectResp.GetStatus())) + s.Len(selectResp.GetResults(), 1) + s.Equal(updatedDescription, selectResp.GetResults()[0].GetDescription()) +} + func TestRBAC(t *testing.T) { suite.Run(t, new(RBACBasicTestSuite)) }