From ce338f180a7af60d85c76dba3baab6220ab04d6d Mon Sep 17 00:00:00 2001 From: wanyaoqi <18528551+wanyaoqi@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:02:34 +0800 Subject: [PATCH] fix(host,host-deployer): host-deployer add password quality check (#23875) --- pkg/baremetal/manager.go | 4 +- pkg/hostman/guestfs/core.go | 3 +- pkg/hostman/guestfs/fsdriver/android.go | 2 +- pkg/hostman/guestfs/fsdriver/esxi.go | 2 +- pkg/hostman/guestfs/fsdriver/interface.go | 2 +- pkg/hostman/guestfs/fsdriver/linux.go | 63 +- pkg/hostman/guestfs/fsdriver/macos.go | 2 +- pkg/hostman/guestfs/fsdriver/windows.go | 2 +- pkg/hostman/guestfs/kvmpart/localfs.go | 39 +- pkg/hostman/guestman/guestman.go | 5 +- pkg/hostman/hostdeployer/apis/deploy.pb.go | 498 +++-- pkg/hostman/hostdeployer/apis/deploy.proto | 17 +- .../hostdeployer/apis/deploy_grpc.pb.go | 12 +- pkg/hostman/hostdeployer/apis/utils.go | 2 + pkg/hostman/storageman/storage_agent.go | 4 +- pkg/util/pwquality/doc.go | 15 + pkg/util/pwquality/pwquality.go | 634 ++++++ pkg/util/pwquality/pwquality_test.go | 1980 +++++++++++++++++ 18 files changed, 2984 insertions(+), 302 deletions(-) create mode 100644 pkg/util/pwquality/doc.go create mode 100644 pkg/util/pwquality/pwquality.go create mode 100644 pkg/util/pwquality/pwquality_test.go diff --git a/pkg/baremetal/manager.go b/pkg/baremetal/manager.go index bce876d86c..13d56b9cf0 100644 --- a/pkg/baremetal/manager.go +++ b/pkg/baremetal/manager.go @@ -3063,10 +3063,12 @@ func (s *SBaremetalServer) SyncPartitionSize(term *ssh.Client, rootDisk *disktoo func (s *SBaremetalServer) DoDeploy(tool *disktool.SSHPartitionTool, term *ssh.Client, data jsonutils.JSONObject, isInit bool) (jsonutils.JSONObject, error) { publicKey := deployapi.GetKeys(data) + isRandomPassword := false password, _ := data.GetString("password") resetPassword := jsonutils.QueryBoolean(data, "reset_password", false) if resetPassword && len(password) == 0 { password = seclib.RandomPassword(12) + isRandomPassword = true } deployArray := make([]*deployapi.DeployContent, 0) if data.Contains("deploys") { @@ -3077,7 +3079,7 @@ func (s *SBaremetalServer) DoDeploy(tool *disktool.SSHPartitionTool, term *ssh.C } userData, _ := s.desc.GetString("user_data") deployInfo := deployapi.NewDeployInfo(publicKey, deployArray, - password, isInit, true, o.Options.LinuxDefaultRootUser, o.Options.WindowsDefaultAdminUser, false, "", + password, isRandomPassword, isInit, true, o.Options.LinuxDefaultRootUser, o.Options.WindowsDefaultAdminUser, false, "", false, "", userData, ) diff --git a/pkg/hostman/guestfs/core.go b/pkg/hostman/guestfs/core.go index 759a220ce6..7ec7954ce6 100644 --- a/pkg/hostman/guestfs/core.go +++ b/pkg/hostman/guestfs/core.go @@ -179,8 +179,7 @@ func DoDeployGuestFs(rootfs fsdriver.IRootFsDriver, guestDesc *deployapi.GuestDe return nil, errors.Wrap(err, "DeployPublicKey") } var secret string - if secret, err = rootfs.ChangeUserPasswd(partition, account, gid, - deployInfo.PublicKey.PublicKey, deployInfo.Password); err != nil { + if secret, err = rootfs.ChangeUserPasswd(partition, account, gid, deployInfo.PublicKey.PublicKey, deployInfo.Password, deployInfo.IsRandomPassword); err != nil { return nil, errors.Wrap(err, "ChangeUserPasswd") } if len(secret) > 0 { diff --git a/pkg/hostman/guestfs/fsdriver/android.go b/pkg/hostman/guestfs/fsdriver/android.go index 4484d03221..5bc5c55e14 100644 --- a/pkg/hostman/guestfs/fsdriver/android.go +++ b/pkg/hostman/guestfs/fsdriver/android.go @@ -78,7 +78,7 @@ func (m *sBaseAndroidRootFs) DeployPublicKey(rootfs IDiskPartition, uname string return nil } -func (m *sBaseAndroidRootFs) ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string) (string, error) { +func (m *sBaseAndroidRootFs) ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string, isRandomPassword bool) (string, error) { return "", nil } diff --git a/pkg/hostman/guestfs/fsdriver/esxi.go b/pkg/hostman/guestfs/fsdriver/esxi.go index e618d85c00..58b71767b4 100644 --- a/pkg/hostman/guestfs/fsdriver/esxi.go +++ b/pkg/hostman/guestfs/fsdriver/esxi.go @@ -56,7 +56,7 @@ func (m *SEsxiRootFs) GetOs() string { return "VMWare" } -func (m *SEsxiRootFs) ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string) (string, error) { +func (m *SEsxiRootFs) ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string, isRandomPassword bool) (string, error) { return utils.EncryptAESBase64(gid, "(blank)") } diff --git a/pkg/hostman/guestfs/fsdriver/interface.go b/pkg/hostman/guestfs/fsdriver/interface.go index 9385a93538..8a9af163af 100644 --- a/pkg/hostman/guestfs/fsdriver/interface.go +++ b/pkg/hostman/guestfs/fsdriver/interface.go @@ -83,7 +83,7 @@ type IRootFsDriver interface { DeployFstabScripts(IDiskPartition, []*deployapi.Disk) error GetLoginAccount(IDiskPartition, string, bool, bool) (string, error) DeployPublicKey(IDiskPartition, string, *deployapi.SSHKeys) error - ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string) (string, error) + ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string, isRandomPassword bool) (string, error) DeployYunionroot(rootFs IDiskPartition, pubkeys *deployapi.SSHKeys, isInit bool, enableCloudInit bool) error EnableSerialConsole(IDiskPartition, *jsonutils.JSONDict) error DisableSerialConsole(IDiskPartition) error diff --git a/pkg/hostman/guestfs/fsdriver/linux.go b/pkg/hostman/guestfs/fsdriver/linux.go index e61e924e3b..081f2d5250 100644 --- a/pkg/hostman/guestfs/fsdriver/linux.go +++ b/pkg/hostman/guestfs/fsdriver/linux.go @@ -42,6 +42,7 @@ import ( "yunion.io/x/onecloud/pkg/util/fstabutils" "yunion.io/x/onecloud/pkg/util/netutils2" "yunion.io/x/onecloud/pkg/util/procutils" + "yunion.io/x/onecloud/pkg/util/pwquality" "yunion.io/x/onecloud/pkg/util/seclib2" "yunion.io/x/onecloud/pkg/util/sysutils" ) @@ -226,7 +227,24 @@ func (l *sLinuxRootFs) GetLoginAccount(rootFs IDiskPartition, sUser string, defa return selUsr, nil } -func (l *sLinuxRootFs) ChangeUserPasswd(rootFs IDiskPartition, account, gid, publicKey, password string) (string, error) { +func (l *sLinuxRootFs) checkInputPasswd(rootFs IDiskPartition, config *pwquality.Config, account, gid, publicKey, password string) string { + if config == nil { + return password + } + + err := config.Validate(password, account) + if err != nil && errors.Cause(err) == pwquality.ErrPasswordTooWeak { + log.Infof("password %s too weak, try regenerate password", password) + npassword := config.GeneratePassword(seclib2.RandomPassword2) + if len(npassword) > 0 { + log.Infof("regenerate password %s", npassword) + password = npassword + } + } + return password +} + +func (l *sLinuxRootFs) ChangeUserPasswd(rootFs IDiskPartition, account, gid, publicKey, password string, isRandomPassword bool) (string, error) { var secret string var err error err = rootFs.Passwd(account, password, false) @@ -1099,6 +1117,27 @@ func (d *sDebianLikeRootFs) DeployNetworkingScripts(rootFs IDiskPartition, nics return rootFs.FilePutContents(fn, cmds.String(), false, false) } +func (r *sDebianLikeRootFs) ChangeUserPasswd(rootFs IDiskPartition, account, gid, publicKey, password string, isRandomPassword bool) (string, error) { + if isRandomPassword { + var pwqualityConf *pwquality.Config + if rootFs.Exists("/etc/security/pwquality.conf", false) { + pwConfig, err := rootFs.FileGetContents("/etc/security/pwquality.conf", false) + if err == nil { + pwqualityConf = pwquality.ParseConfig(pwConfig) + } + } + if rootFs.Exists("/etc/pam.d/common-password", false) { + pamConfig, err := rootFs.FileGetContents("/etc/pam.d/common-password", false) + if err == nil { + pwqualityConf = pwquality.ParsePAMConfig(pamConfig, pwqualityConf) + } + } + password = r.checkInputPasswd(rootFs, pwqualityConf, account, gid, publicKey, password) + } + + return r.sLinuxRootFs.ChangeUserPasswd(rootFs, account, gid, publicKey, password, isRandomPassword) +} + type SDebianRootFs struct { *sDebianLikeRootFs } @@ -1394,6 +1433,26 @@ func (r *sRedhatLikeRootFs) Centos5DeployNetworkingScripts(rootFs IDiskPartition return nil } +func (r *sRedhatLikeRootFs) ChangeUserPasswd(rootFs IDiskPartition, account, gid, publicKey, password string, isRandomPassword bool) (string, error) { + if isRandomPassword { + var pwqualityConf *pwquality.Config + if rootFs.Exists("/etc/security/pwquality.conf", false) { + pwConfig, err := rootFs.FileGetContents("/etc/security/pwquality.conf", false) + if err == nil { + pwqualityConf = pwquality.ParseConfig(pwConfig) + } + } + if rootFs.Exists("/etc/pam.d/system-auth", false) { + pamConfig, err := rootFs.FileGetContents("/etc/pam.d/system-auth", false) + if err == nil { + pwqualityConf = pwquality.ParsePAMConfig(pamConfig, pwqualityConf) + } + } + password = r.checkInputPasswd(rootFs, pwqualityConf, account, gid, publicKey, password) + } + return r.sLinuxRootFs.ChangeUserPasswd(rootFs, account, gid, publicKey, password, isRandomPassword) +} + func getMainNic(nics []*types.SServerNic) *types.SServerNic { for i := range nics { if nics[i].IsDefault { @@ -2375,7 +2434,7 @@ func (d *SCoreOsRootFs) DeployFstabScripts(rootFs IDiskPartition, disks []*deplo return nil } -func (d *SCoreOsRootFs) ChangeUserPasswd(rootFs IDiskPartition, account, gid, publicKey, password string) (string, error) { +func (d *SCoreOsRootFs) ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string, isRandomPassword bool) (string, error) { keys := []string{} if len(publicKey) > 0 { keys = append(keys, publicKey) diff --git a/pkg/hostman/guestfs/fsdriver/macos.go b/pkg/hostman/guestfs/fsdriver/macos.go index 78ea7ccfe8..d8a0b2ff51 100644 --- a/pkg/hostman/guestfs/fsdriver/macos.go +++ b/pkg/hostman/guestfs/fsdriver/macos.go @@ -86,7 +86,7 @@ func (m *SMacOSRootFs) addScripts(lines []string) { m.scripts = append(m.scripts, "") } -func (m *SMacOSRootFs) ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string) (string, error) { +func (m *SMacOSRootFs) ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string, isRandomPassword bool) (string, error) { lines := []string{ fmt.Sprintf("dscl . -passwd /Users/%s %s", account, password), fmt.Sprintf("rm -fr /Users/%s/Library/Keychains/*", account), diff --git a/pkg/hostman/guestfs/fsdriver/windows.go b/pkg/hostman/guestfs/fsdriver/windows.go index 0898272594..23a219b1c4 100644 --- a/pkg/hostman/guestfs/fsdriver/windows.go +++ b/pkg/hostman/guestfs/fsdriver/windows.go @@ -444,7 +444,7 @@ func (w *SWindowsRootFs) CommitChanges(part IDiskPartition) error { return nil } -func (w *SWindowsRootFs) ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string) (string, error) { +func (w *SWindowsRootFs) ChangeUserPasswd(part IDiskPartition, account, gid, publicKey, password string, isRandomPassword bool) (string, error) { rinfo := w.GetReleaseInfo(part) confPath := part.GetLocalPath("/windows/system32/config", true) tool := winutils.NewWinRegTool(confPath) diff --git a/pkg/hostman/guestfs/kvmpart/localfs.go b/pkg/hostman/guestfs/kvmpart/localfs.go index 521039fb6f..220b004eee 100644 --- a/pkg/hostman/guestfs/kvmpart/localfs.go +++ b/pkg/hostman/guestfs/kvmpart/localfs.go @@ -16,9 +16,9 @@ package kvmpart import ( "fmt" - "io" "io/ioutil" "os" + "os/exec" "path" "strings" @@ -139,40 +139,17 @@ func (f *SLocalGuestFS) Zerofiles(dir string, caseInsensitive bool) error { } func (f *SLocalGuestFS) Passwd(account, password string, caseInsensitive bool) error { - var proc = procutils.NewCommand("chroot", f.mountPath, "passwd", account) - stdin, err := proc.StdinPipe() - if err != nil { - return err - } - defer stdin.Close() + var proc = exec.Command("chroot", f.mountPath, "passwd", account) - outb, err := proc.StdoutPipe() - if err != nil { - return err - } - defer outb.Close() + passwordInput := fmt.Sprintf("%s\n%s\n", password, password) + proc.Stdin = strings.NewReader(passwordInput) - errb, err := proc.StderrPipe() + out, err := proc.CombinedOutput() if err != nil { - return err + return errors.Wrapf(err, "failed change passwd %s", out) } - defer errb.Close() - - if err := proc.Start(); err != nil { - return err - } - io.WriteString(stdin, fmt.Sprintf("%s\n", password)) - io.WriteString(stdin, fmt.Sprintf("%s\n", password)) - stdoutPut, err := ioutil.ReadAll(outb) - if err != nil { - return err - } - stderrOutPut, err := ioutil.ReadAll(errb) - if err != nil { - return err - } - log.Infof("Passwd %s %s", stdoutPut, stderrOutPut) - return proc.Wait() + log.Infof("Passwd %s", out) + return nil } func (f *SLocalGuestFS) Stat(usrDir string, caseInsensitive bool) os.FileInfo { diff --git a/pkg/hostman/guestman/guestman.go b/pkg/hostman/guestman/guestman.go index f633765f0f..2131f54328 100644 --- a/pkg/hostman/guestman/guestman.go +++ b/pkg/hostman/guestman/guestman.go @@ -986,10 +986,13 @@ func (m *SGuestManager) startDeploy( return nil, errors.Wrapf(err, "unmarshal to array of deployapi.DeployContent") } } + + isRandomPassword := false password, _ := deployParams.Body.GetString("password") resetPassword := jsonutils.QueryBoolean(deployParams.Body, "reset_password", false) if resetPassword && len(password) == 0 { password = seclib.RandomPassword2(14) + isRandomPassword = true } enableCloudInit := jsonutils.QueryBoolean(deployParams.Body, "enable_cloud_init", false) loginAccount, _ := deployParams.Body.GetString("login_account") @@ -1007,7 +1010,7 @@ func (m *SGuestManager) startDeploy( guestInfo, err := guest.DeployFs(ctx, deployParams.UserCred, deployapi.NewDeployInfo( publicKey, deployArray, - password, deployParams.IsInit, false, + password, isRandomPassword, deployParams.IsInit, false, options.HostOptions.LinuxDefaultRootUser, options.HostOptions.WindowsDefaultAdminUser, enableCloudInit, loginAccount, deployTelegraf, telegrafConfig, guest.GetDesc().UserData, diff --git a/pkg/hostman/hostdeployer/apis/deploy.pb.go b/pkg/hostman/hostdeployer/apis/deploy.pb.go index e3f0283360..ba148d3011 100644 --- a/pkg/hostman/hostdeployer/apis/deploy.pb.go +++ b/pkg/hostman/hostdeployer/apis/deploy.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 -// protoc v3.21.2 +// protoc-gen-go v1.20.0 +// protoc v3.21.5 // source: deploy.proto // protoc --version=libprotoc 3.11.3 @@ -11,6 +11,7 @@ package apis import ( + proto "github.com/golang/protobuf/proto" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -24,6 +25,10 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + type GuestDesc struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -668,14 +673,15 @@ type DeployInfo struct { PublicKey *SSHKeys `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` Deploys []*DeployContent `protobuf:"bytes,2,rep,name=deploys,proto3" json:"deploys,omitempty"` Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` - IsInit bool `protobuf:"varint,4,opt,name=is_init,json=isInit,proto3" json:"is_init,omitempty"` - EnableTty bool `protobuf:"varint,5,opt,name=enable_tty,json=enableTty,proto3" json:"enable_tty,omitempty"` - DefaultRootUser bool `protobuf:"varint,6,opt,name=default_root_user,json=defaultRootUser,proto3" json:"default_root_user,omitempty"` - WindowsDefaultAdminUser bool `protobuf:"varint,7,opt,name=windows_default_admin_user,json=windowsDefaultAdminUser,proto3" json:"windows_default_admin_user,omitempty"` - EnableCloudInit bool `protobuf:"varint,8,opt,name=enable_cloud_init,json=enableCloudInit,proto3" json:"enable_cloud_init,omitempty"` - LoginAccount string `protobuf:"bytes,9,opt,name=login_account,json=loginAccount,proto3" json:"login_account,omitempty"` - Telegraf *Telegraf `protobuf:"bytes,10,opt,name=telegraf,proto3" json:"telegraf,omitempty"` - UserData string `protobuf:"bytes,11,opt,name=user_data,json=userData,proto3" json:"user_data,omitempty"` + IsRandomPassword bool `protobuf:"varint,4,opt,name=is_random_password,json=isRandomPassword,proto3" json:"is_random_password,omitempty"` + IsInit bool `protobuf:"varint,5,opt,name=is_init,json=isInit,proto3" json:"is_init,omitempty"` + EnableTty bool `protobuf:"varint,6,opt,name=enable_tty,json=enableTty,proto3" json:"enable_tty,omitempty"` + DefaultRootUser bool `protobuf:"varint,7,opt,name=default_root_user,json=defaultRootUser,proto3" json:"default_root_user,omitempty"` + WindowsDefaultAdminUser bool `protobuf:"varint,8,opt,name=windows_default_admin_user,json=windowsDefaultAdminUser,proto3" json:"windows_default_admin_user,omitempty"` + EnableCloudInit bool `protobuf:"varint,9,opt,name=enable_cloud_init,json=enableCloudInit,proto3" json:"enable_cloud_init,omitempty"` + LoginAccount string `protobuf:"bytes,10,opt,name=login_account,json=loginAccount,proto3" json:"login_account,omitempty"` + Telegraf *Telegraf `protobuf:"bytes,11,opt,name=telegraf,proto3" json:"telegraf,omitempty"` + UserData string `protobuf:"bytes,12,opt,name=user_data,json=userData,proto3" json:"user_data,omitempty"` } func (x *DeployInfo) Reset() { @@ -731,6 +737,13 @@ func (x *DeployInfo) GetPassword() string { return "" } +func (x *DeployInfo) GetIsRandomPassword() bool { + if x != nil { + return x.IsRandomPassword + } + return false +} + func (x *DeployInfo) GetIsInit() bool { if x != nil { return x.IsInit @@ -2267,7 +2280,7 @@ var file_deploy_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x6d, 0x72, 0x65, 0x66, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x6d, 0x72, 0x65, 0x66, 0x22, 0xc0, 0x03, 0x0a, 0x0a, 0x44, 0x65, 0x70, + 0x09, 0x52, 0x05, 0x76, 0x6d, 0x72, 0x65, 0x66, 0x22, 0xee, 0x03, 0x0a, 0x0a, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2c, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x53, 0x53, 0x48, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, @@ -2276,238 +2289,241 @@ var file_deploy_proto_rawDesc = []byte{ 0x70, 0x6c, 0x6f, 0x79, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, - 0x12, 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x06, 0x69, 0x73, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x5f, 0x74, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x54, 0x74, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x64, 0x65, 0x66, 0x61, - 0x75, 0x6c, 0x74, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6f, 0x74, - 0x55, 0x73, 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x1a, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x5f, - 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x75, 0x73, - 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, - 0x73, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x73, 0x65, - 0x72, 0x12, 0x2a, 0x0a, 0x11, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x6f, 0x75, - 0x64, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x65, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x23, 0x0a, - 0x0d, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x08, 0x74, 0x65, 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x54, 0x65, 0x6c, 0x65, - 0x67, 0x72, 0x61, 0x66, 0x52, 0x08, 0x74, 0x65, 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x12, 0x1b, - 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x22, 0x2f, 0x0a, 0x08, 0x54, - 0x65, 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6c, 0x65, 0x67, - 0x72, 0x61, 0x66, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x74, 0x65, 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x43, 0x6f, 0x6e, 0x66, 0x22, 0xac, 0x01, 0x0a, - 0x07, 0x53, 0x53, 0x48, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, - 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, - 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x10, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, - 0x64, 0x6d, 0x69, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, - 0x12, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, - 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x55, 0x0a, 0x0d, 0x44, - 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, - 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xe2, 0x01, 0x0a, 0x15, - 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x47, 0x75, 0x65, 0x73, 0x74, 0x46, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x73, 0x74, 0x72, 0x6f, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x73, 0x74, 0x72, 0x6f, 0x12, 0x18, 0x0a, - 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x63, 0x68, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x6c, - 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, - 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x6f, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x65, 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x5f, - 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, - 0x74, 0x65, 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x65, 0x64, - 0x22, 0xaa, 0x01, 0x0a, 0x08, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, - 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, - 0x68, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x5f, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x65, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x25, 0x0a, 0x0e, - 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x46, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x5f, 0x61, - 0x6c, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x41, 0x6c, 0x67, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x73, 0x6b, 0x49, 0x64, 0x22, 0xce, 0x01, - 0x0a, 0x0c, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x2b, - 0x0a, 0x09, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, - 0x6f, 0x52, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2e, 0x0a, 0x0a, 0x67, - 0x75, 0x65, 0x73, 0x74, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x47, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x73, 0x63, - 0x52, 0x09, 0x67, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x73, 0x63, 0x12, 0x31, 0x0a, 0x0b, 0x64, - 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2e, - 0x0a, 0x09, 0x76, 0x64, 0x64, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x56, 0x44, 0x44, 0x4b, 0x43, 0x6f, 0x6e, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x76, 0x64, 0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0xbd, - 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x46, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x12, 0x2c, 0x0a, 0x12, 0x69, 0x73, 0x5f, 0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x5f, 0x70, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x69, 0x73, + 0x52, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x17, + 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x06, 0x69, 0x73, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x5f, 0x74, 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x54, 0x74, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, + 0x74, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6f, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x1a, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x5f, 0x64, 0x65, + 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x75, 0x73, 0x65, 0x72, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x17, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x44, + 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x73, 0x65, 0x72, 0x12, + 0x2a, 0x0a, 0x11, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x5f, + 0x69, 0x6e, 0x69, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6c, + 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x2a, 0x0a, 0x08, 0x74, 0x65, 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x54, 0x65, 0x6c, 0x65, 0x67, 0x72, + 0x61, 0x66, 0x52, 0x08, 0x74, 0x65, 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x12, 0x1b, 0x0a, 0x09, + 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x75, 0x73, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x22, 0x2f, 0x0a, 0x08, 0x54, 0x65, 0x6c, + 0x65, 0x67, 0x72, 0x61, 0x66, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x65, 0x6c, 0x65, 0x67, 0x72, 0x61, + 0x66, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, + 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x43, 0x6f, 0x6e, 0x66, 0x22, 0xac, 0x01, 0x0a, 0x07, 0x53, + 0x53, 0x48, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, + 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, + 0x79, 0x12, 0x28, 0x0a, 0x10, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x64, 0x6d, + 0x69, 0x6e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x55, 0x0a, 0x0d, 0x44, 0x65, 0x70, + 0x6c, 0x6f, 0x79, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x18, + 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xe2, 0x01, 0x0a, 0x15, 0x44, 0x65, + 0x70, 0x6c, 0x6f, 0x79, 0x47, 0x75, 0x65, 0x73, 0x74, 0x46, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x73, 0x74, 0x72, 0x6f, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x73, 0x74, 0x72, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x63, 0x68, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x63, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, + 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, + 0x67, 0x75, 0x61, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x6f, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x65, 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x5f, 0x64, 0x65, + 0x70, 0x6c, 0x6f, 0x79, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x74, 0x65, + 0x6c, 0x65, 0x67, 0x72, 0x61, 0x66, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x65, 0x64, 0x22, 0xaa, + 0x01, 0x0a, 0x08, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x70, + 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, + 0x29, 0x0a, 0x10, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x65, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, + 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x5f, 0x61, 0x6c, 0x67, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x41, + 0x6c, 0x67, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x73, 0x6b, 0x49, 0x64, 0x22, 0xce, 0x01, 0x0a, 0x0c, + 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x09, + 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x08, 0x64, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2e, 0x0a, 0x0a, 0x67, 0x75, 0x65, + 0x73, 0x74, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, + 0x61, 0x70, 0x69, 0x73, 0x2e, 0x47, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x73, 0x63, 0x52, 0x09, + 0x67, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x73, 0x63, 0x12, 0x31, 0x0a, 0x0b, 0x64, 0x65, 0x70, + 0x6c, 0x6f, 0x79, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, + 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2e, 0x0a, 0x09, + 0x76, 0x64, 0x64, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x56, 0x44, 0x44, 0x4b, 0x43, 0x6f, 0x6e, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x08, 0x76, 0x64, 0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0xbd, 0x01, 0x0a, + 0x0e, 0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x46, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, + 0x2b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1e, 0x0a, 0x0a, + 0x68, 0x79, 0x70, 0x65, 0x72, 0x76, 0x69, 0x73, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x68, 0x79, 0x70, 0x65, 0x72, 0x76, 0x69, 0x73, 0x6f, 0x72, 0x12, 0x2e, 0x0a, 0x09, + 0x76, 0x64, 0x64, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x56, 0x44, 0x44, 0x4b, 0x43, 0x6f, 0x6e, 0x49, 0x6e, + 0x66, 0x6f, 0x52, 0x08, 0x76, 0x64, 0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2e, 0x0a, 0x0a, + 0x67, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x47, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x73, + 0x63, 0x52, 0x09, 0x67, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x73, 0x63, 0x22, 0x76, 0x0a, 0x0e, + 0x46, 0x73, 0x45, 0x78, 0x74, 0x34, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x28, + 0x0a, 0x0f, 0x43, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x43, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x73, + 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x3a, 0x0a, 0x18, 0x52, 0x65, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, + 0x74, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x18, 0x52, 0x65, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, + 0x74, 0x61, 0x67, 0x65, 0x22, 0x7e, 0x0a, 0x0e, 0x46, 0x73, 0x46, 0x32, 0x66, 0x73, 0x46, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x43, 0x61, 0x73, 0x65, 0x49, 0x6e, + 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0f, 0x43, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, + 0x12, 0x42, 0x0a, 0x1c, 0x4f, 0x76, 0x65, 0x72, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x1c, 0x4f, 0x76, 0x65, 0x72, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, + 0x74, 0x61, 0x67, 0x65, 0x22, 0x60, 0x0a, 0x0a, 0x46, 0x73, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x12, 0x28, 0x0a, 0x04, 0x65, 0x78, 0x74, 0x34, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x46, 0x73, 0x45, 0x78, 0x74, 0x34, 0x46, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x04, 0x65, 0x78, 0x74, 0x34, 0x12, 0x28, 0x0a, 0x04, + 0x66, 0x32, 0x66, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, + 0x73, 0x2e, 0x46, 0x73, 0x46, 0x32, 0x66, 0x73, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, + 0x52, 0x04, 0x66, 0x32, 0x66, 0x73, 0x22, 0xa1, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x6d, 0x61, + 0x74, 0x46, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x09, 0x64, 0x69, 0x73, + 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, + 0x70, 0x69, 0x73, 0x2e, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x64, 0x69, + 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x73, 0x5f, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x73, 0x46, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x75, 0x75, 0x69, 0x64, 0x12, 0x31, 0x0a, 0x0b, 0x66, 0x73, 0x5f, 0x66, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, + 0x70, 0x69, 0x73, 0x2e, 0x46, 0x73, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x0a, + 0x66, 0x73, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x22, 0x6f, 0x0a, 0x0b, 0x52, 0x65, + 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x73, + 0x74, 0x72, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x73, 0x74, 0x72, + 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x61, + 0x72, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x63, 0x68, 0x12, + 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x5d, 0x0a, 0x12, 0x53, + 0x61, 0x76, 0x65, 0x54, 0x6f, 0x47, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x69, 0x73, 0x6b, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1e, - 0x0a, 0x0a, 0x68, 0x79, 0x70, 0x65, 0x72, 0x76, 0x69, 0x73, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x68, 0x79, 0x70, 0x65, 0x72, 0x76, 0x69, 0x73, 0x6f, 0x72, 0x12, 0x2e, - 0x0a, 0x09, 0x76, 0x64, 0x64, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x56, 0x44, 0x44, 0x4b, 0x43, 0x6f, 0x6e, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x76, 0x64, 0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2e, - 0x0a, 0x0a, 0x67, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x47, 0x75, 0x65, 0x73, 0x74, 0x44, - 0x65, 0x73, 0x63, 0x52, 0x09, 0x67, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x73, 0x63, 0x22, 0x76, - 0x0a, 0x0e, 0x46, 0x73, 0x45, 0x78, 0x74, 0x34, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x12, 0x28, 0x0a, 0x0f, 0x43, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, - 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x43, 0x61, 0x73, 0x65, 0x49, - 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x3a, 0x0a, 0x18, 0x52, 0x65, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x50, 0x65, 0x72, 0x63, - 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x18, 0x52, 0x65, - 0x73, 0x65, 0x72, 0x76, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x50, 0x65, 0x72, 0x63, - 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x22, 0x7e, 0x0a, 0x0e, 0x46, 0x73, 0x46, 0x32, 0x66, 0x73, - 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x43, 0x61, 0x73, 0x65, - 0x49, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0f, 0x43, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, - 0x76, 0x65, 0x12, 0x42, 0x0a, 0x1c, 0x4f, 0x76, 0x65, 0x72, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x50, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x61, - 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x1c, 0x4f, 0x76, 0x65, 0x72, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x61, 0x74, 0x69, 0x6f, 0x50, 0x65, 0x72, 0x63, - 0x65, 0x6e, 0x74, 0x61, 0x67, 0x65, 0x22, 0x60, 0x0a, 0x0a, 0x46, 0x73, 0x46, 0x65, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x04, 0x65, 0x78, 0x74, 0x34, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x46, 0x73, 0x45, 0x78, 0x74, 0x34, - 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x04, 0x65, 0x78, 0x74, 0x34, 0x12, 0x28, - 0x0a, 0x04, 0x66, 0x32, 0x66, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, - 0x70, 0x69, 0x73, 0x2e, 0x46, 0x73, 0x46, 0x32, 0x66, 0x73, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x73, 0x52, 0x04, 0x66, 0x32, 0x66, 0x73, 0x22, 0xa1, 0x01, 0x0a, 0x0e, 0x46, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x46, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x09, 0x64, - 0x69, 0x73, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, - 0x64, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x73, 0x5f, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x73, 0x46, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x75, 0x69, 0x64, 0x12, 0x31, 0x0a, 0x0b, 0x66, 0x73, 0x5f, - 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x46, 0x73, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x52, 0x0a, 0x66, 0x73, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x22, 0x6f, 0x0a, 0x0b, - 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x0a, 0x06, 0x64, - 0x69, 0x73, 0x74, 0x72, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x73, - 0x74, 0x72, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, - 0x04, 0x61, 0x72, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x63, - 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x5d, 0x0a, - 0x12, 0x53, 0x61, 0x76, 0x65, 0x54, 0x6f, 0x47, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x61, 0x72, - 0x61, 0x6d, 0x73, 0x12, 0x2b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x69, - 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, - 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x22, 0x65, 0x0a, 0x14, - 0x53, 0x61, 0x76, 0x65, 0x54, 0x6f, 0x47, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x6f, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x34, 0x0a, - 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x52, 0x65, 0x6c, 0x65, 0x61, - 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, - 0x6e, 0x66, 0x6f, 0x22, 0x43, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x49, 0x6d, 0x61, 0x67, - 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x50, 0x72, 0x61, 0x6d, 0x61, 0x73, 0x12, 0x2b, 0x0a, 0x09, 0x64, - 0x69, 0x73, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, - 0x64, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0xda, 0x02, 0x0a, 0x09, 0x49, 0x6d, 0x61, - 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2a, 0x0a, 0x07, 0x6f, 0x73, 0x5f, 0x69, 0x6e, 0x66, - 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x52, - 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x6f, 0x73, 0x49, 0x6e, - 0x66, 0x6f, 0x12, 0x17, 0x0a, 0x07, 0x6f, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x69, - 0x73, 0x5f, 0x75, 0x65, 0x66, 0x69, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x73, 0x55, 0x65, 0x66, 0x69, 0x53, 0x75, 0x70, 0x70, - 0x6f, 0x72, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x69, 0x73, 0x5f, 0x6c, 0x76, 0x6d, 0x5f, 0x70, 0x61, - 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, - 0x73, 0x4c, 0x76, 0x6d, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, - 0x0b, 0x69, 0x73, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x52, 0x65, 0x61, 0x64, 0x6f, 0x6e, 0x6c, 0x79, 0x12, 0x36, - 0x0a, 0x17, 0x70, 0x68, 0x79, 0x73, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x15, 0x70, 0x68, 0x79, 0x73, 0x69, 0x63, 0x61, 0x6c, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x35, 0x0a, 0x17, 0x69, 0x73, 0x5f, 0x69, 0x6e, 0x73, - 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x5f, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x5f, 0x69, 0x6e, 0x69, - 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x69, 0x73, 0x49, 0x6e, 0x73, 0x74, 0x61, - 0x6c, 0x6c, 0x65, 0x64, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x26, 0x0a, - 0x0f, 0x69, 0x73, 0x5f, 0x62, 0x69, 0x6f, 0x73, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x73, 0x42, 0x69, 0x6f, 0x73, 0x53, 0x75, - 0x70, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x2b, 0x0a, 0x0c, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, - 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x70, 0x61, - 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x50, 0x61, - 0x74, 0x68, 0x22, 0x7d, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x45, 0x73, 0x78, - 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x2e, 0x0a, 0x09, - 0x76, 0x64, 0x64, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x56, 0x44, 0x44, 0x4b, 0x43, 0x6f, 0x6e, 0x49, 0x6e, - 0x66, 0x6f, 0x52, 0x08, 0x76, 0x64, 0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x33, 0x0a, 0x0b, - 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, - 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x49, 0x6e, 0x66, - 0x6f, 0x22, 0x43, 0x0a, 0x17, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x28, 0x0a, 0x05, - 0x64, 0x69, 0x73, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, - 0x69, 0x73, 0x2e, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x05, 0x64, 0x69, 0x73, 0x6b, 0x73, 0x22, 0x67, 0x0a, 0x0b, 0x42, 0x6f, 0x6f, 0x74, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x42, 0x6f, 0x6f, 0x74, 0x4f, 0x72, 0x64, - 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x42, 0x6f, 0x6f, 0x74, 0x4f, 0x72, - 0x64, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x65, 0x76, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x44, 0x65, 0x76, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, - 0x0b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x0b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, - 0x60, 0x0a, 0x13, 0x4f, 0x76, 0x6d, 0x66, 0x42, 0x6f, 0x6f, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, - 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x76, 0x6d, 0x66, 0x56, 0x61, - 0x72, 0x73, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x76, - 0x6d, 0x66, 0x56, 0x61, 0x72, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x04, 0x64, 0x65, - 0x76, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, - 0x42, 0x6f, 0x6f, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x04, 0x64, 0x65, 0x76, - 0x73, 0x32, 0x82, 0x04, 0x0a, 0x0b, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x12, 0x40, 0x0a, 0x0d, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x47, 0x75, 0x65, 0x73, 0x74, - 0x46, 0x73, 0x12, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, - 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x65, - 0x70, 0x6c, 0x6f, 0x79, 0x47, 0x75, 0x65, 0x73, 0x74, 0x46, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x46, 0x73, 0x12, - 0x14, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x46, 0x73, 0x50, - 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x12, 0x2d, 0x0a, 0x08, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x46, 0x73, 0x12, 0x14, - 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x46, 0x73, 0x50, 0x61, - 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x12, 0x44, 0x0a, 0x0c, 0x53, 0x61, 0x76, 0x65, 0x54, 0x6f, 0x47, 0x6c, 0x61, 0x6e, 0x63, - 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x54, 0x6f, 0x47, - 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x1a, 0x2e, 0x61, 0x70, - 0x69, 0x73, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x54, 0x6f, 0x47, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x62, 0x65, - 0x49, 0x6d, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x73, - 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x50, - 0x72, 0x61, 0x6d, 0x61, 0x73, 0x1a, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x49, 0x6d, 0x61, - 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x4f, 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, - 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, - 0x6b, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, - 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x41, 0x0a, 0x13, 0x44, 0x69, 0x73, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x12, 0x1d, - 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x43, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x1a, 0x0b, 0x2e, - 0x61, 0x70, 0x69, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3a, 0x0a, 0x10, 0x53, 0x65, - 0x74, 0x4f, 0x76, 0x6d, 0x66, 0x42, 0x6f, 0x6f, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x19, - 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x4f, 0x76, 0x6d, 0x66, 0x42, 0x6f, 0x6f, 0x74, 0x4f, 0x72, - 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x73, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x34, 0x5a, 0x32, 0x79, 0x75, 0x6e, 0x69, 0x6f, 0x6e, - 0x2e, 0x69, 0x6f, 0x2f, 0x78, 0x2f, 0x6f, 0x6e, 0x65, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x70, - 0x6b, 0x67, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x6e, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x64, - 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, + 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x22, 0x65, 0x0a, 0x14, 0x53, 0x61, + 0x76, 0x65, 0x54, 0x6f, 0x47, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x6f, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x34, 0x0a, 0x0c, 0x72, + 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, + 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0b, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, + 0x6f, 0x22, 0x43, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x49, + 0x6e, 0x66, 0x6f, 0x50, 0x72, 0x61, 0x6d, 0x61, 0x73, 0x12, 0x2b, 0x0a, 0x09, 0x64, 0x69, 0x73, + 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, + 0x70, 0x69, 0x73, 0x2e, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x64, 0x69, + 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0xda, 0x02, 0x0a, 0x09, 0x49, 0x6d, 0x61, 0x67, 0x65, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2a, 0x0a, 0x07, 0x6f, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x52, 0x65, 0x6c, + 0x65, 0x61, 0x73, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x6f, 0x73, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x17, 0x0a, 0x07, 0x6f, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x6f, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x69, 0x73, 0x5f, + 0x75, 0x65, 0x66, 0x69, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0d, 0x69, 0x73, 0x55, 0x65, 0x66, 0x69, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, + 0x74, 0x12, 0x28, 0x0a, 0x10, 0x69, 0x73, 0x5f, 0x6c, 0x76, 0x6d, 0x5f, 0x70, 0x61, 0x72, 0x74, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x73, 0x4c, + 0x76, 0x6d, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x69, + 0x73, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0a, 0x69, 0x73, 0x52, 0x65, 0x61, 0x64, 0x6f, 0x6e, 0x6c, 0x79, 0x12, 0x36, 0x0a, 0x17, + 0x70, 0x68, 0x79, 0x73, 0x69, 0x63, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x70, + 0x68, 0x79, 0x73, 0x69, 0x63, 0x61, 0x6c, 0x50, 0x61, 0x72, 0x74, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x35, 0x0a, 0x17, 0x69, 0x73, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6c, 0x6c, 0x65, 0x64, 0x5f, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x69, 0x73, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, + 0x65, 0x64, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x69, + 0x73, 0x5f, 0x62, 0x69, 0x6f, 0x73, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x73, 0x42, 0x69, 0x6f, 0x73, 0x53, 0x75, 0x70, 0x70, + 0x6f, 0x72, 0x74, 0x22, 0x2b, 0x0a, 0x0c, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x50, 0x61, 0x74, 0x68, + 0x22, 0x7d, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x45, 0x73, 0x78, 0x69, 0x44, + 0x69, 0x73, 0x6b, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x12, 0x2e, 0x0a, 0x09, 0x76, 0x64, + 0x64, 0x6b, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, + 0x61, 0x70, 0x69, 0x73, 0x2e, 0x56, 0x44, 0x44, 0x4b, 0x43, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x08, 0x76, 0x64, 0x64, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x33, 0x0a, 0x0b, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x49, 0x6e, 0x66, 0x6f, 0x22, + 0x43, 0x0a, 0x17, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x43, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x28, 0x0a, 0x05, 0x64, 0x69, + 0x73, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x73, + 0x2e, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x64, + 0x69, 0x73, 0x6b, 0x73, 0x22, 0x67, 0x0a, 0x0b, 0x42, 0x6f, 0x6f, 0x74, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x42, 0x6f, 0x6f, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x42, 0x6f, 0x6f, 0x74, 0x4f, 0x72, 0x64, 0x65, + 0x72, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x65, 0x76, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x07, 0x44, 0x65, 0x76, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x41, + 0x74, 0x74, 0x61, 0x63, 0x68, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x22, 0x60, 0x0a, + 0x13, 0x4f, 0x76, 0x6d, 0x66, 0x42, 0x6f, 0x6f, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x50, 0x61, + 0x72, 0x61, 0x6d, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x76, 0x6d, 0x66, 0x56, 0x61, 0x72, 0x73, + 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x76, 0x6d, 0x66, + 0x56, 0x61, 0x72, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x04, 0x64, 0x65, 0x76, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x42, 0x6f, + 0x6f, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x52, 0x04, 0x64, 0x65, 0x76, 0x73, 0x32, + 0x82, 0x04, 0x0a, 0x0b, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, + 0x40, 0x0a, 0x0d, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x47, 0x75, 0x65, 0x73, 0x74, 0x46, 0x73, + 0x12, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x50, 0x61, + 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6c, + 0x6f, 0x79, 0x47, 0x75, 0x65, 0x73, 0x74, 0x46, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x46, 0x73, 0x12, 0x14, 0x2e, + 0x61, 0x70, 0x69, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x69, 0x7a, 0x65, 0x46, 0x73, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x73, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x2d, 0x0a, 0x08, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x46, 0x73, 0x12, 0x14, 0x2e, 0x61, + 0x70, 0x69, 0x73, 0x2e, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x46, 0x73, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x73, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x44, 0x0a, 0x0c, 0x53, 0x61, 0x76, 0x65, 0x54, 0x6f, 0x47, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, + 0x18, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x54, 0x6f, 0x47, 0x6c, 0x61, + 0x6e, 0x63, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x73, + 0x2e, 0x53, 0x61, 0x76, 0x65, 0x54, 0x6f, 0x47, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x62, 0x65, 0x49, 0x6d, + 0x61, 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x50, + 0x72, 0x6f, 0x62, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x50, 0x72, 0x61, + 0x6d, 0x61, 0x73, 0x1a, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x4f, 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x45, + 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, + 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x45, 0x73, + 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x41, 0x0a, 0x13, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x12, 0x1d, 0x2e, 0x61, + 0x70, 0x69, 0x73, 0x2e, 0x45, 0x73, 0x78, 0x69, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x1a, 0x0b, 0x2e, 0x61, 0x70, + 0x69, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3a, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x4f, + 0x76, 0x6d, 0x66, 0x42, 0x6f, 0x6f, 0x74, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, + 0x70, 0x69, 0x73, 0x2e, 0x4f, 0x76, 0x6d, 0x66, 0x42, 0x6f, 0x6f, 0x74, 0x4f, 0x72, 0x64, 0x65, + 0x72, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x73, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x42, 0x34, 0x5a, 0x32, 0x79, 0x75, 0x6e, 0x69, 0x6f, 0x6e, 0x2e, 0x69, + 0x6f, 0x2f, 0x78, 0x2f, 0x6f, 0x6e, 0x65, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x70, 0x6b, 0x67, + 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, 0x6e, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x64, 0x65, 0x70, + 0x6c, 0x6f, 0x79, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/pkg/hostman/hostdeployer/apis/deploy.proto b/pkg/hostman/hostdeployer/apis/deploy.proto index f8b759332f..471981beb6 100644 --- a/pkg/hostman/hostdeployer/apis/deploy.proto +++ b/pkg/hostman/hostdeployer/apis/deploy.proto @@ -85,14 +85,15 @@ message DeployInfo { SSHKeys public_key = 1; repeated DeployContent deploys = 2; string password = 3; - bool is_init = 4; - bool enable_tty = 5; - bool default_root_user = 6; - bool windows_default_admin_user = 7; - bool enable_cloud_init = 8; - string login_account = 9; - Telegraf telegraf = 10; - string user_data = 11; + bool is_random_password =4; + bool is_init = 5; + bool enable_tty = 6; + bool default_root_user = 7; + bool windows_default_admin_user = 8; + bool enable_cloud_init = 9; + string login_account = 10; + Telegraf telegraf = 11; + string user_data = 12; } message Telegraf { diff --git a/pkg/hostman/hostdeployer/apis/deploy_grpc.pb.go b/pkg/hostman/hostdeployer/apis/deploy_grpc.pb.go index 082a8a7dfe..c696896326 100644 --- a/pkg/hostman/hostdeployer/apis/deploy_grpc.pb.go +++ b/pkg/hostman/hostdeployer/apis/deploy_grpc.pb.go @@ -1,8 +1,4 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.2.0 -// - protoc v3.21.2 -// source: deploy.proto package apis @@ -15,7 +11,6 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 // DeployAgentClient is the client API for DeployAgent service. @@ -165,7 +160,7 @@ type UnsafeDeployAgentServer interface { } func RegisterDeployAgentServer(s grpc.ServiceRegistrar, srv DeployAgentServer) { - s.RegisterService(&DeployAgent_ServiceDesc, srv) + s.RegisterService(&_DeployAgent_serviceDesc, srv) } func _DeployAgent_DeployGuestFs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { @@ -312,10 +307,7 @@ func _DeployAgent_SetOvmfBootOrder_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } -// DeployAgent_ServiceDesc is the grpc.ServiceDesc for DeployAgent service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var DeployAgent_ServiceDesc = grpc.ServiceDesc{ +var _DeployAgent_serviceDesc = grpc.ServiceDesc{ ServiceName: "apis.DeployAgent", HandlerType: (*DeployAgentServer)(nil), Methods: []grpc.MethodDesc{ diff --git a/pkg/hostman/hostdeployer/apis/utils.go b/pkg/hostman/hostdeployer/apis/utils.go index 7d8cff0df2..79e4a09a9f 100644 --- a/pkg/hostman/hostdeployer/apis/utils.go +++ b/pkg/hostman/hostdeployer/apis/utils.go @@ -32,6 +32,7 @@ func NewDeployInfo( publicKey *SSHKeys, deploys []*DeployContent, password string, + isRandomPassword, isInit bool, enableTty bool, defaultRootUser bool, @@ -46,6 +47,7 @@ func NewDeployInfo( PublicKey: publicKey, Deploys: deploys, Password: password, + IsRandomPassword: isRandomPassword, IsInit: isInit, EnableTty: enableTty, DefaultRootUser: defaultRootUser, diff --git a/pkg/hostman/storageman/storage_agent.go b/pkg/hostman/storageman/storage_agent.go index 73af4b74c1..7393e0b0ef 100644 --- a/pkg/hostman/storageman/storage_agent.go +++ b/pkg/hostman/storageman/storage_agent.go @@ -336,10 +336,12 @@ func (as *SAgentStorage) AgentDeployGuest(ctx context.Context, data interface{}) } } + isRandomPassword := false passwd, _ := dataDict.GetString("password") resetPassword := jsonutils.QueryBoolean(dataDict, "reset_password", false) if resetPassword && len(passwd) == 0 { passwd = seclib.RandomPassword(12) + isRandomPassword = true } enableCloudInit := jsonutils.QueryBoolean(dataDict, "enable_cloud_init", false) @@ -350,7 +352,7 @@ func (as *SAgentStorage) AgentDeployGuest(ctx context.Context, data interface{}) return nil, errors.Errorf("missing telegraf_conf") } - deployInfo := deployapi.NewDeployInfo(&key, deployArray, passwd, init, false, + deployInfo := deployapi.NewDeployInfo(&key, deployArray, passwd, isRandomPassword, init, false, options.HostOptions.LinuxDefaultRootUser, options.HostOptions.WindowsDefaultAdminUser, enableCloudInit, loginAccount, deployTelegraf, telegrafConfig, desc.UserData, diff --git a/pkg/util/pwquality/doc.go b/pkg/util/pwquality/doc.go new file mode 100644 index 0000000000..931ad8ede6 --- /dev/null +++ b/pkg/util/pwquality/doc.go @@ -0,0 +1,15 @@ +// Copyright 2019 Yunion +// +// Licensed 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 pwquality // import "yunion.io/x/onecloud/pkg/util/pwquality" diff --git a/pkg/util/pwquality/pwquality.go b/pkg/util/pwquality/pwquality.go new file mode 100644 index 0000000000..26a51294b8 --- /dev/null +++ b/pkg/util/pwquality/pwquality.go @@ -0,0 +1,634 @@ +// Copyright 2019 Yunion +// +// Licensed 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 pwquality + +import ( + "strconv" + "strings" + "unicode" + + "yunion.io/x/pkg/errors" +) + +// ErrPasswordTooWeak 表示密码强度不符合要求的统一错误 +var ErrPasswordTooWeak = errors.Error("password too weak") + +// Config 存储 pwquality 配置 +type Config struct { + Minlen int // 最小长度 + Dcredit int // 数字字符信用值(负数表示至少需要多少个字符,正数表示每个字符可减少的长度要求) + Ucredit int // 大写字母信用值 + Lcredit int // 小写字母信用值 + Ocredit int // 特殊字符信用值 + Minclass int // 最小字符类数量(数字、大写、小写、特殊) + Maxrepeat int // 最大重复字符数(0 表示不限制) + Maxclassrepeat int // 最大同类字符重复数(0 表示不限制) + Maxsequence int // 最大连续字符序列长度(0 表示不限制) + // Enforcing 是否强制执行密码策略(1=强制执行,0=仅警告,默认1) + // 注意:此参数在 libpwquality 1.2.0+ 版本中支持,较老的系统可能不支持 + // 如果系统不支持,配置文件中不会出现此参数,将使用默认值 1(强制执行) + Enforcing int + // EnforceForRoot 是否对 root 用户强制执行密码策略(1=强制执行,0=不强制,默认0) + // 注意:此参数在 libpwquality 1.2.0+ 版本中支持,较老的系统可能不支持 + // 在配置文件中,可能以两种形式出现: + // 1. enforce_for_root = 1(key=value 形式) + // 2. enforce_for_root(独立标志形式,无等号,表示启用) + // 如果系统不支持,配置文件中不会出现此参数,将使用默认值 0(不对 root 强制执行) + EnforceForRoot int + Usercheck int // 是否检查密码中包含用户名(1=检查,0=不检查,默认0) + // 以下配置项在 chroot 环境中可能不适用,暂不实现 + // Gecoscheck int // 是否检查密码中包含用户的 GECOS 信息 + // Dictcheck int // 是否检查密码是否包含字典中的单词(需要字典文件) + // Dictpath string // 字典文件路径 +} + +// HasAnyPolicy 检查配置是否有任何非默认的密码策略设置 +// 用于判断配置是否有效(即是否包含任何密码强度要求) +func (c *Config) HasAnyPolicy() bool { + if c == nil { + return false + } + return c.Minlen > 0 || c.Dcredit != 0 || c.Ucredit != 0 || + c.Lcredit != 0 || c.Ocredit != 0 || c.Minclass > 0 || + c.Maxrepeat > 0 || c.Maxclassrepeat > 0 || c.Maxsequence > 0 +} + +// IsEnforcing 检查密码策略是否强制执行 +// 如果 enforcing=0,密码策略不会强制执行(只是警告) +func (c *Config) IsEnforcing() bool { + if c == nil { + return true // 默认强制执行 + } + // enforcing=1 表示强制执行,enforcing=0 表示仅警告 + // 默认值为 1(强制执行) + return c.Enforcing != 0 +} + +// IsEnforcingForRoot 检查是否对 root 用户强制执行密码策略 +func (c *Config) IsEnforcingForRoot() bool { + if c == nil { + return false // 默认不对 root 强制执行 + } + // enforce_for_root=1 表示对 root 强制执行,enforce_for_root=0 表示不强制 + return c.EnforceForRoot == 1 +} + +// ParseConfig 解析 /etc/security/pwquality.conf 配置文件内容 +// +// 兼容性说明: +// - enforcing 和 enforce_for_root 参数在 libpwquality 1.2.0+ 版本中支持 +// - 较老的系统(如 RHEL 6 之前)可能不支持这些参数 +// - enforce_for_root 可能以两种形式出现: +// 1. enforce_for_root = 1(key=value 形式) +// 2. enforce_for_root(独立标志形式,无等号,表示启用) +// +// - 如果配置文件中不存在这些参数,将使用默认值: +// - Enforcing: 1(默认强制执行) +// - EnforceForRoot: 0(默认不对 root 强制执行) +func ParseConfig(content []byte) *Config { + config := &Config{ + Minlen: 0, // 默认值 + Dcredit: 0, // 默认值 + Ucredit: 0, // 默认值 + Lcredit: 0, // 默认值 + Ocredit: 0, // 默认值 + Minclass: 0, // 默认值 + Maxrepeat: 0, // 默认值(0 表示不限制) + Maxclassrepeat: 0, // 默认值(0 表示不限制) + Maxsequence: 0, // 默认值(0 表示不限制) + Enforcing: 1, // 默认值(1 表示强制执行) + EnforceForRoot: 0, // 默认值(0 表示不对 root 强制执行) + Usercheck: 0, // 默认值(0 表示不检查用户名) + } + + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + // 跳过注释和空行 + if len(line) == 0 || strings.HasPrefix(line, "#") { + continue + } + + // 检查是否是 key = value 格式 + if strings.Contains(line, "=") { + // 解析 key = value 格式 + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + switch key { + case "minlen": + if v, err := strconv.Atoi(value); err == nil { + config.Minlen = v + } + case "dcredit": + if v, err := strconv.Atoi(value); err == nil { + config.Dcredit = v + } + case "ucredit": + if v, err := strconv.Atoi(value); err == nil { + config.Ucredit = v + } + case "lcredit": + if v, err := strconv.Atoi(value); err == nil { + config.Lcredit = v + } + case "ocredit": + if v, err := strconv.Atoi(value); err == nil { + config.Ocredit = v + } + case "minclass": + if v, err := strconv.Atoi(value); err == nil { + config.Minclass = v + } + case "maxrepeat": + if v, err := strconv.Atoi(value); err == nil { + config.Maxrepeat = v + } + case "maxclassrepeat": + if v, err := strconv.Atoi(value); err == nil { + config.Maxclassrepeat = v + } + case "maxsequence": + if v, err := strconv.Atoi(value); err == nil { + config.Maxsequence = v + } + case "enforcing": + if v, err := strconv.Atoi(value); err == nil { + config.Enforcing = v + } + case "enforce_for_root": + if v, err := strconv.Atoi(value); err == nil { + config.EnforceForRoot = v + } + case "usercheck": + if v, err := strconv.Atoi(value); err == nil { + config.Usercheck = v + } + } + } else { + // 处理独立标志(无值的参数) + // 例如:enforce_for_root(表示对 root 强制执行密码策略) + key := strings.TrimSpace(line) + switch key { + case "enforce_for_root": + // 如果以独立标志形式出现,设置为 1(强制执行) + config.EnforceForRoot = 1 + } + } + } + + return config +} + +// Validate 根据 pwquality 配置校验密码强度 +// username 为用户名,用于检查密码中是否包含用户名(如果启用了 usercheck) +// 参考 libpwquality 的实现逻辑 +func (c *Config) Validate(password string, username string) error { + if !c.HasAnyPolicy() { + return nil + } + + // 如果 enforcing=0,密码策略不会强制执行(只是警告),直接返回 + //if username != "root" && !c.IsEnforcing() { + // return nil + //} + + // 如果用户是 root 且 enforce_for_root=0,不对 root 强制执行密码策略 + //if username == "root" && !c.IsEnforcingForRoot() { + // return nil + //} + + // 统计各类字符数量 + var digits, uppers, lowers, others int + for _, r := range password { + if unicode.IsDigit(r) { + digits++ + } else if unicode.IsUpper(r) { + uppers++ + } else if unicode.IsLower(r) { + lowers++ + } else { + others++ + } + } + + // 处理 credit 值 + // 根据 libpwquality 的文档: + // - 负数(如 -1):表示至少需要多少个字符(常用) + // - 正数(如 1):表示每个字符可以减少多少长度要求(较少用) + // - 0:不要求 + if c.Dcredit < 0 { + // 负数:至少需要这么多数字字符 + required := -c.Dcredit + if digits < required { + return errors.Wrapf(ErrPasswordTooWeak, "password requires at least %d digit(s), got %d", required, digits) + } + } else if c.Dcredit > 0 { + // 正数:每个数字字符可以减少多少长度要求(较少使用) + // 这里我们简化处理,只检查是否有数字 + if digits == 0 { + return errors.Wrapf(ErrPasswordTooWeak, "password should contain at least one digit") + } + } + + if c.Ucredit < 0 { + required := -c.Ucredit + if uppers < required { + return errors.Wrapf(ErrPasswordTooWeak, "password requires at least %d uppercase letter(s), got %d", required, uppers) + } + } else if c.Ucredit > 0 { + if uppers == 0 { + return errors.Wrapf(ErrPasswordTooWeak, "password should contain at least one uppercase letter") + } + } + + if c.Lcredit < 0 { + required := -c.Lcredit + if lowers < required { + return errors.Wrapf(ErrPasswordTooWeak, "password requires at least %d lowercase letter(s), got %d", required, lowers) + } + } else if c.Lcredit > 0 { + if lowers == 0 { + return errors.Wrapf(ErrPasswordTooWeak, "password should contain at least one lowercase letter") + } + } + + if c.Ocredit < 0 { + required := -c.Ocredit + if others < required { + return errors.Wrapf(ErrPasswordTooWeak, "password requires at least %d special character(s), got %d", required, others) + } + } else if c.Ocredit > 0 { + if others == 0 { + return errors.Wrapf(ErrPasswordTooWeak, "password should contain at least one special character") + } + } + + // 计算有效长度(考虑 credit 的正数值,用于减少长度要求) + effectiveLength := len(password) + if c.Dcredit > 0 { + effectiveLength += digits * c.Dcredit + } + if c.Ucredit > 0 { + effectiveLength += uppers * c.Ucredit + } + if c.Lcredit > 0 { + effectiveLength += lowers * c.Lcredit + } + if c.Ocredit > 0 { + effectiveLength += others * c.Ocredit + } + + // 检查最小长度 + if c.Minlen > 0 && effectiveLength < c.Minlen { + return errors.Wrapf(ErrPasswordTooWeak, "effective length %d is less than required %d", effectiveLength, c.Minlen) + } + + // 检查最小字符类数量 + if c.Minclass > 0 { + classes := 0 + if digits > 0 { + classes++ + } + if uppers > 0 { + classes++ + } + if lowers > 0 { + classes++ + } + if others > 0 { + classes++ + } + if classes < c.Minclass { + return errors.Wrapf(ErrPasswordTooWeak, "requires at least %d character class(es), got %d", c.Minclass, classes) + } + } + + // 检查最大重复字符数 + if c.Maxrepeat > 0 { + maxRepeat := 0 + currentRepeat := 1 + prevChar := rune(0) + for _, r := range password { + if r == prevChar { + currentRepeat++ + if currentRepeat > maxRepeat { + maxRepeat = currentRepeat + } + } else { + currentRepeat = 1 + } + prevChar = r + } + if maxRepeat > c.Maxrepeat { + return errors.Wrapf(ErrPasswordTooWeak, "password contains more than %d consecutive repeated characters", c.Maxrepeat) + } + } + + // 检查最大同类字符重复数 + if c.Maxclassrepeat > 0 { + maxClassRepeat := 0 + currentClassRepeat := 1 + prevClass := -1 // -1: 未设置, 0: 数字, 1: 大写, 2: 小写, 3: 特殊 + for _, r := range password { + var currentClass int + if unicode.IsDigit(r) { + currentClass = 0 + } else if unicode.IsUpper(r) { + currentClass = 1 + } else if unicode.IsLower(r) { + currentClass = 2 + } else { + currentClass = 3 + } + if currentClass == prevClass { + currentClassRepeat++ + if currentClassRepeat > maxClassRepeat { + maxClassRepeat = currentClassRepeat + } + } else { + currentClassRepeat = 1 + } + prevClass = currentClass + } + if maxClassRepeat > c.Maxclassrepeat { + return errors.Wrapf(ErrPasswordTooWeak, "password contains more than %d consecutive characters of the same class", c.Maxclassrepeat) + } + } + + // 检查最大连续字符序列长度 + // maxsequence 检查密码中是否存在超过指定长度的连续字符序列(如 "1234" 或 "abcd") + if c.Maxsequence > 0 { + runes := []rune(password) + for i := 0; i <= len(runes)-c.Maxsequence-1; i++ { + // 检查升序序列(如 "1234", "abcd") + isAscending := true + for j := 1; j <= c.Maxsequence; j++ { + if i+j >= len(runes) || runes[i+j] != runes[i+j-1]+1 { + isAscending = false + break + } + } + // 检查降序序列(如 "4321", "dcba") + isDescending := true + for j := 1; j <= c.Maxsequence; j++ { + if i+j >= len(runes) || runes[i+j] != runes[i+j-1]-1 { + isDescending = false + break + } + } + if isAscending || isDescending { + return errors.Wrapf(ErrPasswordTooWeak, "password contains a sequence of more than %d consecutive characters", c.Maxsequence) + } + } + } + + // 检查密码中是否包含用户名 + if c.Usercheck > 0 && username != "" { + // 将用户名和密码都转换为小写进行比较(不区分大小写) + lowerUsername := strings.ToLower(username) + lowerPassword := strings.ToLower(password) + + // 检查密码中是否包含用户名(包括反向) + if strings.Contains(lowerPassword, lowerUsername) { + return errors.Wrapf(ErrPasswordTooWeak, "password contains the username") + } + + // 检查密码中是否包含用户名的反向(用户名长度至少为3才检查反向) + if len(lowerUsername) >= 3 { + reversedUsername := reverseString(lowerUsername) + if strings.Contains(lowerPassword, reversedUsername) { + return errors.Wrapf(ErrPasswordTooWeak, "password contains the reversed username") + } + } + } + + return nil +} + +// reverseString 反转字符串 +func reverseString(s string) string { + runes := []rune(s) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + return string(runes) +} + +// ParsePAMConfig 解析 PAM 配置文件中的密码强度策略 +// 支持 pam_pwquality 和 pam_cracklib 模块 +// +// 兼容性说明: +// - enforcing 和 enforce_for_root 参数在 libpwquality 1.2.0+ 版本中支持 +// - 较老的系统(如 RHEL 6 之前)可能不支持这些参数 +// - 在 PAM 配置中,enforce_for_root 可能以独立标志形式出现(无值),如: +// password requisite pam_pwquality.so minlen=8 enforce_for_root +// 这种情况下,如果解析到 enforce_for_root 标志(无值),将设置为 1 +// - 如果系统不支持这些参数,配置文件中不会出现,将使用默认值 +func ParsePAMConfig(content []byte, config *Config) *Config { + if config == nil { + config = &Config{ + Minlen: 0, + Dcredit: 0, + Ucredit: 0, + Lcredit: 0, + Ocredit: 0, + Minclass: 0, + Maxrepeat: 0, + Maxclassrepeat: 0, + Maxsequence: 0, + Enforcing: 1, // 默认值(1 表示强制执行) + EnforceForRoot: 0, // 默认值(0 表示不对 root 强制执行) + Usercheck: 0, // 默认值(0 表示不检查用户名) + } + } + + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + // 跳过注释和空行 + if len(line) == 0 || strings.HasPrefix(line, "#") { + continue + } + + // 查找 password 相关的 PAM 配置行 + // 格式: password requisite pam_pwquality.so retry=3 minlen=8 dcredit=-1 ucredit=-1 + // 或: password requisite pam_cracklib.so retry=3 minlen=8 dcredit=-1 ucredit=-1 + if !strings.Contains(line, "password") { + continue + } + + // 检查是否包含 pam_pwquality 或 pam_cracklib + if !strings.Contains(line, "pam_pwquality") && !strings.Contains(line, "pam_cracklib") { + continue + } + + // 解析参数,格式为 key=value 或独立标志(如 enforce_for_root) + // 先找到 .so 后面的参数部分 + parts := strings.Fields(line) + for _, part := range parts { + part = strings.TrimSpace(part) + if len(part) == 0 { + continue + } + + // 检查是否是 key=value 格式 + if strings.Contains(part, "=") { + kv := strings.SplitN(part, "=", 2) + if len(kv) != 2 { + continue + } + + key := strings.TrimSpace(kv[0]) + value := strings.TrimSpace(kv[1]) + + switch key { + case "minlen": + if v, err := strconv.Atoi(value); err == nil { + config.Minlen = v + } + case "dcredit": + if v, err := strconv.Atoi(value); err == nil { + config.Dcredit = v + } + case "ucredit": + if v, err := strconv.Atoi(value); err == nil { + config.Ucredit = v + } + case "lcredit": + if v, err := strconv.Atoi(value); err == nil { + config.Lcredit = v + } + case "ocredit": + if v, err := strconv.Atoi(value); err == nil { + config.Ocredit = v + } + case "minclass": + if v, err := strconv.Atoi(value); err == nil { + config.Minclass = v + } + case "maxrepeat": + if v, err := strconv.Atoi(value); err == nil { + config.Maxrepeat = v + } + case "maxclassrepeat": + if v, err := strconv.Atoi(value); err == nil { + config.Maxclassrepeat = v + } + case "maxsequence": + if v, err := strconv.Atoi(value); err == nil { + config.Maxsequence = v + } + case "enforcing": + if v, err := strconv.Atoi(value); err == nil { + config.Enforcing = v + } + case "enforce_for_root": + if v, err := strconv.Atoi(value); err == nil { + config.EnforceForRoot = v + } + case "usercheck": + if v, err := strconv.Atoi(value); err == nil { + config.Usercheck = v + } + case "difok": // pam_cracklib 特有:至少需要多少个字符与旧密码不同 + // 这个参数不影响密码强度校验,可以忽略 + case "retry": // 重试次数,不影响密码强度校验 + } + } else { + // 处理独立标志(无值的参数) + // 例如:enforce_for_root(表示对 root 强制执行密码策略) + switch part { + case "enforce_for_root": + // 如果以独立标志形式出现,设置为 1(强制执行) + config.EnforceForRoot = 1 + } + } + } + } + + return config +} + +// GeneratePassword 根据配置生成符合强度要求的密码 +// passwordGenerator 是一个函数,接受长度参数并返回密码 +// 如果 passwordGenerator 为 nil,将使用默认的最小长度 12 +func (c *Config) GeneratePassword(passwordGenerator func(int) string) string { + if c == nil || !c.HasAnyPolicy() { + // 如果没有配置或配置为空,使用默认长度生成 + if passwordGenerator != nil { + return passwordGenerator(12) + } + return "" + } + + // 计算所需的最小密码长度 + minLength := c.Minlen + if minLength == 0 { + minLength = 8 // 默认最小长度 + } + + // 根据 credit 要求计算额外需要的字符数 + requiredChars := 0 + if c.Dcredit < 0 { + requiredChars += -c.Dcredit + } + if c.Ucredit < 0 { + requiredChars += -c.Ucredit + } + if c.Lcredit < 0 { + requiredChars += -c.Lcredit + } + if c.Ocredit < 0 { + requiredChars += -c.Ocredit + } + + // 确保长度满足所有要求 + passwordLength := minLength + if requiredChars > 0 { + // 至少需要 minLength 和 requiredChars 中的较大值 + if requiredChars > passwordLength { + passwordLength = requiredChars + } + // 再加上一些缓冲,确保有足够的字符满足 minclass 要求 + if c.Minclass > 0 && c.Minclass > 1 { + passwordLength += c.Minclass + } + } + + if passwordGenerator == nil { + return "" + } + + // 生成密码并验证,直到符合要求 + maxAttempts := 64 + for i := 12; i < maxAttempts; i += 2 { + password := passwordGenerator(passwordLength) + // GeneratePassword 不提供用户名,所以传空字符串 + if c.Validate(password, "") == nil { + return password + } + // 如果不符合要求,增加长度重试 + passwordLength++ + } + + // 如果多次尝试都失败,返回一个较长的密码(应该能满足大部分要求) + return passwordGenerator(passwordLength) +} diff --git a/pkg/util/pwquality/pwquality_test.go b/pkg/util/pwquality/pwquality_test.go new file mode 100644 index 0000000000..9da0c8aa4a --- /dev/null +++ b/pkg/util/pwquality/pwquality_test.go @@ -0,0 +1,1980 @@ +// Copyright 2019 Yunion +// +// Licensed 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 pwquality + +import ( + "strings" + "testing" +) + +func TestParseConfig(t *testing.T) { + tests := []struct { + name string + content string + expected *Config + }{ + { + name: "basic config", + content: `minlen = 8 +dcredit = -1 +ucredit = -1 +lcredit = -1 +ocredit = -1 +minclass = 3`, + expected: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + }, + { + name: "config with comments", + content: `# This is a comment +minlen = 12 +# Another comment +dcredit = -2 +ucredit = 0 +lcredit = -1 +ocredit = -1`, + expected: &Config{ + Minlen: 12, + Dcredit: -2, + Ucredit: 0, + Lcredit: -1, + Ocredit: -1, + Minclass: 0, + }, + }, + { + name: "empty config", + content: `# Empty config file +# No settings`, + expected: &Config{ + Minlen: 0, + Dcredit: 0, + Ucredit: 0, + Lcredit: 0, + Ocredit: 0, + Minclass: 0, + }, + }, + { + name: "config with spaces", + content: ` minlen = 10 +dcredit = -1 +ucredit = -1`, + expected: &Config{ + Minlen: 10, + Dcredit: -1, + Ucredit: -1, + Lcredit: 0, + Ocredit: 0, + Minclass: 0, + }, + }, + { + name: "positive credit values", + content: `minlen = 8 +dcredit = 1 +ucredit = 1 +lcredit = 1`, + expected: &Config{ + Minlen: 8, + Dcredit: 1, + Ucredit: 1, + Lcredit: 1, + Ocredit: 0, + Minclass: 0, + }, + }, + { + name: "config with maxrepeat", + content: `minlen = 8 +maxrepeat = 3`, + expected: &Config{ + Minlen: 8, + Maxrepeat: 3, + }, + }, + { + name: "config with maxclassrepeat", + content: `minlen = 8 +maxclassrepeat = 2`, + expected: &Config{ + Minlen: 8, + Maxclassrepeat: 2, + }, + }, + { + name: "config with maxsequence", + content: `minlen = 8 +maxsequence = 3`, + expected: &Config{ + Minlen: 8, + Maxsequence: 3, + }, + }, + { + name: "config with all new options", + content: `minlen = 8 +maxrepeat = 3 +maxclassrepeat = 2 +maxsequence = 3`, + expected: &Config{ + Minlen: 8, + Maxrepeat: 3, + Maxclassrepeat: 2, + Maxsequence: 3, + }, + }, + { + name: "invalid value - non-numeric", + content: `minlen = abc +dcredit = -1`, + expected: &Config{ + Minlen: 0, // 无效值应该被忽略 + Dcredit: -1, + }, + }, + { + name: "line without equals sign", + content: `minlen 8 +dcredit = -1`, + expected: &Config{ + Minlen: 0, // 没有等号的行应该被忽略 + Dcredit: -1, + }, + }, + { + name: "multiple equals signs", + content: `minlen = 8 = 10 +dcredit = -1`, + expected: &Config{ + Minlen: 0, // " 8 = 10" 不是有效数字,会被忽略 + Dcredit: -1, + }, + }, + { + name: "unknown config key", + content: `minlen = 8 +unknown_key = 10 +dcredit = -1`, + expected: &Config{ + Minlen: 8, + Dcredit: -1, + }, + }, + { + name: "empty value", + content: `minlen = +dcredit = -1`, + expected: &Config{ + Minlen: 0, // 空值应该被忽略 + Dcredit: -1, + }, + }, + { + name: "config with enforcing", + content: `minlen = 8 +enforcing = 1`, + expected: &Config{ + Minlen: 8, + Enforcing: 1, + }, + }, + { + name: "config with enforcing disabled", + content: `minlen = 8 +enforcing = 0`, + expected: &Config{ + Minlen: 8, + Enforcing: 0, + }, + }, + { + name: "config with enforce_for_root", + content: `minlen = 8 +enforce_for_root = 1`, + expected: &Config{ + Minlen: 8, + EnforceForRoot: 1, + }, + }, + { + name: "config with both enforcing parameters", + content: `minlen = 8 +enforcing = 1 +enforce_for_root = 1`, + expected: &Config{ + Minlen: 8, + Enforcing: 1, + EnforceForRoot: 1, + }, + }, + { + name: "config with enforce_for_root as flag (no equals sign)", + content: `minlen = 8 +enforce_for_root`, + expected: &Config{ + Minlen: 8, + EnforceForRoot: 1, // 独立标志形式,应设置为 1 + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := ParseConfig([]byte(tt.content)) + if config.Minlen != tt.expected.Minlen { + t.Errorf("Minlen = %d, want %d", config.Minlen, tt.expected.Minlen) + } + if config.Dcredit != tt.expected.Dcredit { + t.Errorf("Dcredit = %d, want %d", config.Dcredit, tt.expected.Dcredit) + } + if config.Ucredit != tt.expected.Ucredit { + t.Errorf("Ucredit = %d, want %d", config.Ucredit, tt.expected.Ucredit) + } + if config.Lcredit != tt.expected.Lcredit { + t.Errorf("Lcredit = %d, want %d", config.Lcredit, tt.expected.Lcredit) + } + if config.Ocredit != tt.expected.Ocredit { + t.Errorf("Ocredit = %d, want %d", config.Ocredit, tt.expected.Ocredit) + } + if config.Minclass != tt.expected.Minclass { + t.Errorf("Minclass = %d, want %d", config.Minclass, tt.expected.Minclass) + } + if config.Maxrepeat != tt.expected.Maxrepeat { + t.Errorf("Maxrepeat = %d, want %d", config.Maxrepeat, tt.expected.Maxrepeat) + } + if config.Maxclassrepeat != tt.expected.Maxclassrepeat { + t.Errorf("Maxclassrepeat = %d, want %d", config.Maxclassrepeat, tt.expected.Maxclassrepeat) + } + if config.Maxsequence != tt.expected.Maxsequence { + t.Errorf("Maxsequence = %d, want %d", config.Maxsequence, tt.expected.Maxsequence) + } + }) + } +} + +func TestParseConfig_EdgeCases(t *testing.T) { + tests := []struct { + name string + content string + expected *Config + }{ + { + name: "empty content", + content: "", + expected: &Config{}, + }, + { + name: "only newlines", + content: ` + + +`, + expected: &Config{}, + }, + { + name: "mixed valid and invalid", + content: `minlen = 8 +invalid_line +dcredit = -1 +another_invalid = line`, + expected: &Config{ + Minlen: 8, + Dcredit: -1, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := ParseConfig([]byte(tt.content)) + if config.Minlen != tt.expected.Minlen { + t.Errorf("Minlen = %d, want %d", config.Minlen, tt.expected.Minlen) + } + if config.Dcredit != tt.expected.Dcredit { + t.Errorf("Dcredit = %d, want %d", config.Dcredit, tt.expected.Dcredit) + } + }) + } +} + +func TestConfig_Validate(t *testing.T) { + tests := []struct { + name string + config *Config + password string + wantError bool + errorMsg string + }{ + { + name: "nil config should pass", + config: nil, + password: "anypassword", + wantError: false, + }, + { + name: "minlen check - too short", + config: &Config{Minlen: 8}, + password: "short", + wantError: true, + errorMsg: "effective length", + }, + { + name: "minlen check - pass", + config: &Config{Minlen: 8}, + password: "longpassword", + wantError: false, + }, + { + name: "dcredit negative - require at least 1 digit", + config: &Config{Dcredit: -1}, + password: "nodigits", + wantError: true, + errorMsg: "password requires at least 1 digit(s)", + }, + { + name: "dcredit negative - pass with digit", + config: &Config{Dcredit: -1}, + password: "pass1word", + wantError: false, + }, + { + name: "dcredit negative - require 2 digits", + config: &Config{Dcredit: -2}, + password: "pass1word", + wantError: true, + errorMsg: "password requires at least 2 digit(s)", + }, + { + name: "dcredit negative - pass with 2 digits", + config: &Config{Dcredit: -2}, + password: "pass12word", + wantError: false, + }, + { + name: "ucredit negative - require uppercase", + config: &Config{Ucredit: -1}, + password: "nouppercase", + wantError: true, + errorMsg: "password requires at least 1 uppercase letter(s)", + }, + { + name: "ucredit negative - pass with uppercase", + config: &Config{Ucredit: -1}, + password: "passWord", + wantError: false, + }, + { + name: "lcredit negative - require lowercase", + config: &Config{Lcredit: -1}, + password: "NOLOWERCASE", + wantError: true, + errorMsg: "password requires at least 1 lowercase letter(s)", + }, + { + name: "lcredit negative - pass with lowercase", + config: &Config{Lcredit: -1}, + password: "PASSWORDw", + wantError: false, + }, + { + name: "ocredit negative - require special char", + config: &Config{Ocredit: -1}, + password: "nospecialchar", + wantError: true, + errorMsg: "password requires at least 1 special character(s)", + }, + { + name: "ocredit negative - pass with special char", + config: &Config{Ocredit: -1}, + password: "password@", + wantError: false, + }, + { + name: "minclass - require 3 classes", + config: &Config{Minclass: 3}, + password: "onlylowercase", + wantError: true, + errorMsg: "requires at least 3 character class(es)", + }, + { + name: "minclass - pass with 3 classes", + config: &Config{Minclass: 3}, + password: "Pass1word", + wantError: false, + }, + { + name: "minclass - pass with 4 classes", + config: &Config{Minclass: 3}, + password: "Pass1@word", + wantError: false, + }, + { + name: "complex config - all requirements", + config: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + password: "Pass1@word", + wantError: false, + }, + { + name: "complex config - missing digit", + config: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + password: "Pass@word", + wantError: true, + errorMsg: "password requires at least 1 digit(s)", + }, + { + name: "complex config - missing uppercase", + config: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + password: "pass1@word", + wantError: true, + errorMsg: "password requires at least 1 uppercase letter(s)", + }, + { + name: "complex config - missing lowercase", + config: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + password: "PASS1@WORD", + wantError: true, + errorMsg: "password requires at least 1 lowercase letter(s)", + }, + { + name: "complex config - missing special char", + config: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + password: "Pass1word", + wantError: true, + errorMsg: "password requires at least 1 special character(s)", + }, + { + name: "complex config - too short", + config: &Config{ + Minlen: 12, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + password: "Pass1@wor", + wantError: true, + errorMsg: "effective length", + }, + { + name: "positive credit - dcredit", + config: &Config{ + Minlen: 8, + Dcredit: 1, + }, + password: "password", + wantError: true, + errorMsg: "password should contain at least one digit", + }, + { + name: "positive credit - pass with digit", + config: &Config{ + Minlen: 8, + Dcredit: 1, + }, + password: "pass1word", + wantError: false, + }, + { + name: "real world example - strong password", + config: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + password: "MyP@ssw0rd", + wantError: false, + }, + { + name: "real world example - weak password", + config: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + password: "password", + wantError: true, + }, + { + name: "maxrepeat - too many repeated characters", + config: &Config{ + Minlen: 8, + Maxrepeat: 2, + }, + password: "Passaaa1", + wantError: true, + errorMsg: "more than 2 consecutive repeated characters", + }, + { + name: "maxrepeat - pass with repeated characters within limit", + config: &Config{ + Minlen: 8, + Maxrepeat: 3, + }, + password: "Passaa12", + wantError: false, + }, + { + name: "maxclassrepeat - too many consecutive digits", + config: &Config{ + Minlen: 8, + Maxclassrepeat: 2, + }, + password: "Pass1111", + wantError: true, + errorMsg: "more than 2 consecutive characters of the same class", + }, + { + name: "maxclassrepeat - pass with consecutive digits within limit", + config: &Config{ + Minlen: 8, + Maxclassrepeat: 3, + }, + password: "Pass111@", + wantError: false, + }, + { + name: "maxsequence - ascending sequence too long", + config: &Config{ + Minlen: 8, + Maxsequence: 3, + }, + password: "Pass1234", + wantError: true, + errorMsg: "sequence of more than 3 consecutive characters", + }, + { + name: "maxsequence - descending sequence too long", + config: &Config{ + Minlen: 8, + Maxsequence: 3, + }, + password: "Pass4321", + wantError: true, + errorMsg: "sequence of more than 3 consecutive characters", + }, + { + name: "maxsequence - pass with sequence within limit", + config: &Config{ + Minlen: 8, + Maxsequence: 3, + }, + password: "Pass123@", + wantError: false, + }, + { + name: "maxsequence - pass with no sequence", + config: &Config{ + Minlen: 8, + Maxsequence: 3, + }, + password: "Pass1@word", + wantError: false, + }, + { + name: "complex config with all new options", + config: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Maxrepeat: 2, + Maxclassrepeat: 2, + Maxsequence: 3, + }, + password: "P1@w0rD2", + wantError: false, + }, + { + name: "complex config - fails maxrepeat", + config: &Config{ + Minlen: 8, + Maxrepeat: 2, + }, + password: "Passaaa1", + wantError: true, + errorMsg: "more than 2 consecutive repeated characters", + }, + { + name: "empty password", + config: &Config{ + Minlen: 8, + }, + password: "", + wantError: true, + errorMsg: "effective length", + }, + { + name: "maxrepeat boundary - exactly at limit", + config: &Config{ + Minlen: 8, + Maxrepeat: 3, + }, + password: "Passaaa1", + wantError: false, // 3个重复字符,正好在限制内 + }, + { + name: "maxrepeat boundary - one over limit", + config: &Config{ + Minlen: 8, + Maxrepeat: 2, + }, + password: "Passaaa1", + wantError: true, + errorMsg: "more than 2 consecutive repeated characters", + }, + { + name: "maxclassrepeat boundary - exactly at limit", + config: &Config{ + Minlen: 8, + Maxclassrepeat: 3, + }, + password: "Pass111@", + wantError: false, // 3个连续数字,正好在限制内 + }, + { + name: "maxsequence boundary - exactly at limit", + config: &Config{ + Minlen: 8, + Maxsequence: 3, + }, + password: "Pass123@", + wantError: false, // 3个连续字符,正好在限制内 + }, + { + name: "maxsequence boundary - one over limit", + config: &Config{ + Minlen: 8, + Maxsequence: 2, + }, + password: "Pass123@", + wantError: true, + errorMsg: "sequence of more than 2 consecutive characters", + }, + { + name: "positive credit - effective length calculation", + config: &Config{ + Minlen: 10, + Dcredit: 2, // 每个数字可以减少2个长度要求 + }, + password: "Pass123", // 7个字符 + 3个数字*2 = 13,应该通过 + wantError: false, + }, + { + name: "positive credit - effective length too short", + config: &Config{ + Minlen: 10, + Dcredit: 1, // 每个数字可以减少1个长度要求 + }, + password: "Pass12", // 6个字符 + 2个数字*1 = 8,小于10,应该失败 + wantError: true, + errorMsg: "effective length", + }, + { + name: "maxsequence - ascending at start", + config: &Config{ + Minlen: 8, + Maxsequence: 3, + }, + password: "123Pass@", + wantError: false, // 3个连续字符在开头,正好在限制内 + }, + { + name: "maxsequence - descending at end", + config: &Config{ + Minlen: 8, + Maxsequence: 3, + }, + password: "Pass@321", + wantError: false, // 3个连续字符在结尾,正好在限制内 + }, + { + name: "positive credit - ucredit", + config: &Config{ + Minlen: 8, + Ucredit: 1, + }, + password: "password", + wantError: true, + errorMsg: "password should contain at least one uppercase letter", + }, + { + name: "positive credit - ucredit pass", + config: &Config{ + Minlen: 8, + Ucredit: 1, + }, + password: "passWord", + wantError: false, + }, + { + name: "positive credit - lcredit", + config: &Config{ + Minlen: 8, + Lcredit: 1, + }, + password: "PASSWORD", + wantError: true, + errorMsg: "password should contain at least one lowercase letter", + }, + { + name: "positive credit - lcredit pass", + config: &Config{ + Minlen: 8, + Lcredit: 1, + }, + password: "PASSWORDw", + wantError: false, + }, + { + name: "positive credit - ocredit", + config: &Config{ + Minlen: 8, + Ocredit: 1, + }, + password: "password", + wantError: true, + errorMsg: "password should contain at least one special character", + }, + { + name: "positive credit - ocredit pass", + config: &Config{ + Minlen: 8, + Ocredit: 1, + }, + password: "password@", + wantError: false, + }, + { + name: "positive credit - multiple credits effective length", + config: &Config{ + Minlen: 10, + Dcredit: 2, + Ucredit: 1, + Lcredit: 1, + Ocredit: 1, + }, + password: "Pass123@", // 8 + 3*2 + 1*1 + 3*1 + 1*1 = 8+6+1+3+1 = 19,应该通过 + wantError: false, + }, + { + name: "maxrepeat - single character password", + config: &Config{ + Minlen: 1, + Maxrepeat: 2, + }, + password: "a", + wantError: false, // 单个字符,没有重复 + }, + { + name: "maxrepeat - no repeated characters", + config: &Config{ + Minlen: 8, + Maxrepeat: 2, + }, + password: "Passw0rd", + wantError: false, // 没有重复字符 + }, + { + name: "maxclassrepeat - single character", + config: &Config{ + Minlen: 1, + Maxclassrepeat: 2, + }, + password: "1", + wantError: false, // 单个字符,没有同类重复 + }, + { + name: "maxclassrepeat - no consecutive same class", + config: &Config{ + Minlen: 8, + Maxclassrepeat: 2, + }, + password: "P1a2s3w4", + wantError: false, // 没有连续同类字符 + }, + { + name: "maxsequence - short password less than maxsequence", + config: &Config{ + Minlen: 2, + Maxsequence: 3, + }, + password: "12", // 长度小于 maxsequence+1,不会触发检查 + wantError: false, + }, + { + name: "maxsequence - exactly maxsequence+1 length with sequence", + config: &Config{ + Minlen: 4, + Maxsequence: 3, + }, + password: "1234", // 正好4个字符,包含4个连续字符序列 + wantError: true, + errorMsg: "sequence of more than 3 consecutive characters", + }, + { + name: "maxsequence - ascending sequence in middle", + config: &Config{ + Minlen: 8, + Maxsequence: 2, + }, + password: "Pa123ss@", + wantError: true, + errorMsg: "sequence of more than 2 consecutive characters", + }, + { + name: "maxsequence - descending sequence in middle", + config: &Config{ + Minlen: 8, + Maxsequence: 2, + }, + password: "Pa321ss@", + wantError: true, + errorMsg: "sequence of more than 2 consecutive characters", + }, + { + name: "maxsequence - mixed ascending and descending", + config: &Config{ + Minlen: 8, + Maxsequence: 3, + }, + password: "Pass1234@", // 包含4个连续字符序列 + wantError: true, + errorMsg: "sequence of more than 3 consecutive characters", + }, + { + name: "minclass - exactly required classes", + config: &Config{ + Minlen: 8, + Minclass: 2, + }, + password: "Password", // 只有大写和小写,2个类 + wantError: false, + }, + { + name: "minclass - one class short", + config: &Config{ + Minlen: 8, + Minclass: 2, + }, + password: "password", // 只有小写,1个类 + wantError: true, + errorMsg: "requires at least 2 character class(es)", + }, + { + name: "maxrepeat - exactly at limit with multiple repeats", + config: &Config{ + Minlen: 7, + Maxrepeat: 2, + }, + password: "Passaa1", // 2个a重复,正好在限制内 + wantError: false, + }, + { + name: "maxclassrepeat - exactly at limit", + config: &Config{ + Minlen: 7, + Maxclassrepeat: 3, // 允许3个连续同类字符 + }, + password: "Pass111@", // 3个连续数字,正好在限制内 + wantError: false, + }, + { + name: "maxclassrepeat - different classes", + config: &Config{ + Minlen: 8, + Maxclassrepeat: 3, // 允许3个连续同类字符 + }, + password: "PassAA11", // AA和11都是2个连续同类字符,都在限制内 + wantError: false, + }, + { + name: "maxclassrepeat - uppercase consecutive", + config: &Config{ + Minlen: 8, + Maxclassrepeat: 2, + }, + password: "PassAAA1", + wantError: true, + errorMsg: "more than 2 consecutive characters of the same class", + }, + { + name: "maxclassrepeat - lowercase consecutive", + config: &Config{ + Minlen: 8, + Maxclassrepeat: 2, + }, + password: "Paaass1@", + wantError: true, + errorMsg: "more than 2 consecutive characters of the same class", + }, + { + name: "maxclassrepeat - special consecutive", + config: &Config{ + Minlen: 8, + Maxclassrepeat: 2, + }, + password: "Pass1@@@", + wantError: true, + errorMsg: "more than 2 consecutive characters of the same class", + }, + { + name: "usercheck - password contains username", + config: &Config{ + Minlen: 8, + Usercheck: 1, + }, + password: "user1234", // 密码包含用户名 "user" + wantError: true, + errorMsg: "password contains the username", + }, + { + name: "usercheck - password contains reversed username", + config: &Config{ + Minlen: 8, + Usercheck: 1, + }, + password: "resu1234", // 密码包含反向用户名 "resu" (user 的反向) + wantError: true, + errorMsg: "password contains the reversed username", + }, + { + name: "usercheck - password does not contain username", + config: &Config{ + Minlen: 8, + Usercheck: 1, + }, + password: "Pass1234", // 密码不包含用户名 + wantError: false, + }, + { + name: "usercheck - case insensitive", + config: &Config{ + Minlen: 8, + Usercheck: 1, + }, + password: "USER1234", // 密码包含大写用户名 + wantError: true, + errorMsg: "password contains the username", + }, + { + name: "usercheck - disabled", + config: &Config{ + Minlen: 8, + Usercheck: 0, // 不检查用户名 + }, + password: "user1234", // 即使密码包含用户名,也不应该报错 + wantError: false, + }, + { + name: "usercheck - empty username", + config: &Config{ + Minlen: 8, + Usercheck: 1, + }, + password: "anypassword", // 用户名为空,不应该检查 + wantError: false, + }, + { + name: "root user with enforce_for_root = 1 - should validate", + config: &Config{ + Minlen: 8, + EnforceForRoot: 1, // 对 root 强制执行 + Enforcing: 1, // 强制执行 + }, + password: "short", // root 用户也需要验证密码强度 + wantError: true, + errorMsg: "effective length", + }, + { + name: "non-root user with enforce_for_root = 0 - should validate", + config: &Config{ + Minlen: 8, + EnforceForRoot: 0, // 不对 root 强制执行,但普通用户需要验证 + Enforcing: 1, // 强制执行(对普通用户) + }, + password: "short", // 普通用户需要验证密码强度 + wantError: true, + errorMsg: "effective length", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 对于 usercheck 测试,使用 "user" 作为用户名 + // 对于 root 用户测试,使用 "root" 作为用户名 + // 对于 non-root 用户测试,使用 "testuser" 作为用户名 + // 注意:需要先检查 "non-root user",因为 "non-root user" 包含 "root user" + username := "" + if strings.Contains(tt.name, "usercheck") { + username = "user" + } else if strings.Contains(tt.name, "non-root user") { + username = "testuser" + } else if strings.Contains(tt.name, "root user") { + username = "root" + } + + // 如果 config 不为 nil 且未设置 Enforcing,默认设置为 1(强制执行) + // 但如果是 enforcing=0 的测试用例,不要修改 + // 对于 root 用户测试,如果 enforce_for_root=0,enforcing 可能为 0,不要修改 + // 对于 non-root 用户测试,如果已经设置了 Enforcing=1,不要修改 + // 注意:只对 Enforcing=0 的测试用例进行修改,且排除特殊测试用例 + if tt.config != nil && tt.config.Enforcing == 0 && tt.name != "nil config should pass" && + !strings.Contains(tt.name, "enforcing = 0") && + !strings.Contains(tt.name, "root user with enforce_for_root = 0") && + !strings.Contains(tt.name, "non-root user") { + // 只对非特殊测试用例且 Enforcing=0 的情况设置为 1 + tt.config.Enforcing = 1 + } + + err := tt.config.Validate(tt.password, username) + if tt.wantError { + if err == nil { + t.Errorf("Validate() expected error but got nil") + } else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Validate() error = %v, want error containing %q", err, tt.errorMsg) + } + } else { + if err != nil { + t.Errorf("Validate() unexpected error = %v", err) + } + } + }) + } +} + +func TestConfig_Validate_CharacterClasses(t *testing.T) { + config := &Config{ + Minclass: 4, + Enforcing: 1, // 强制执行 + } + + tests := []struct { + name string + password string + wantError bool + }{ + {"all 4 classes", "Pass1@word", false}, + {"3 classes - missing special", "Pass1word", true}, + {"3 classes - missing digit", "Pass@word", true}, + {"3 classes - missing uppercase", "pass1@word", true}, + {"3 classes - missing lowercase", "PASS1@WORD", true}, + {"2 classes", "password", true}, + {"1 class", "PASSWORD", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := config.Validate(tt.password, "") + if tt.wantError { + if err == nil { + t.Errorf("Validate() expected error but got nil for password %q", tt.password) + } + } else { + if err != nil { + t.Errorf("Validate() unexpected error = %v for password %q", err, tt.password) + } + } + }) + } +} + +func TestParsePAMConfig(t *testing.T) { + tests := []struct { + name string + content string + expected *Config + }{ + { + name: "pam_pwquality config", + content: `# PAM configuration +auth required pam_unix.so +password requisite pam_pwquality.so retry=3 minlen=8 dcredit=-1 ucredit=-1 lcredit=-1 ocredit=-1 minclass=3 +password required pam_unix.so sha512 shadow nullok try_first_pass use_authtok`, + expected: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + }, + { + name: "pam_cracklib config", + content: `# PAM configuration +password requisite pam_cracklib.so retry=3 minlen=12 dcredit=-2 ucredit=-1 lcredit=-1 ocredit=-1 +password required pam_unix.so`, + expected: &Config{ + Minlen: 12, + Dcredit: -2, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 0, + }, + }, + { + name: "PAM config with comments", + content: `# This is a comment +password requisite pam_pwquality.so minlen=10 dcredit=-1 +# Another comment`, + expected: &Config{ + Minlen: 10, + Dcredit: -1, + Ucredit: 0, + Lcredit: 0, + Ocredit: 0, + Minclass: 0, + }, + }, + { + name: "PAM config without password module", + content: `# No password module +auth required pam_unix.so`, + expected: &Config{ + Minlen: 0, + Dcredit: 0, + Ucredit: 0, + Lcredit: 0, + Ocredit: 0, + Minclass: 0, + }, + }, + { + name: "multiple password lines - use first", + content: `password requisite pam_pwquality.so minlen=8 dcredit=-1 +password required pam_unix.so +password optional pam_gnome_keyring.so`, + expected: &Config{ + Minlen: 8, + Dcredit: -1, + Ucredit: 0, + Lcredit: 0, + Ocredit: 0, + Minclass: 0, + }, + }, + { + name: "PAM config with new options", + content: `password requisite pam_pwquality.so minlen=8 maxrepeat=3 maxclassrepeat=2 maxsequence=3`, + expected: &Config{ + Minlen: 8, + Maxrepeat: 3, + Maxclassrepeat: 2, + Maxsequence: 3, + }, + }, + { + name: "PAM config with invalid value", + content: `password requisite pam_pwquality.so minlen=abc dcredit=-1`, + expected: &Config{ + Minlen: 0, // 无效值应该被忽略 + Dcredit: -1, + }, + }, + { + name: "PAM config - password line without pam module", + content: `password required pam_unix.so +auth required pam_unix.so`, + expected: &Config{ + Minlen: 0, + Dcredit: 0, + Ucredit: 0, + Lcredit: 0, + Ocredit: 0, + Minclass: 0, + }, + }, + { + name: "PAM config - pam module without password", + content: `auth required pam_pwquality.so minlen=8 +account required pam_unix.so`, + expected: &Config{ + Minlen: 0, // 不是 password 行,应该被忽略 + Dcredit: 0, + }, + }, + { + name: "PAM config - parameter without equals", + content: `password requisite pam_pwquality.so minlen dcredit=-1`, + expected: &Config{ + Minlen: 0, // 没有等号的参数应该被忽略 + Dcredit: -1, + }, + }, + { + name: "PAM config - multiple equals in parameter", + content: `password requisite pam_pwquality.so minlen=8=10 dcredit=-1`, + expected: &Config{ + Minlen: 0, // "8=10" 不是有效数字,会被忽略 + Dcredit: -1, + }, + }, + { + name: "PAM config with enforcing", + content: `password requisite pam_pwquality.so minlen=8 enforcing=1`, + expected: &Config{ + Minlen: 8, + Enforcing: 1, + }, + }, + { + name: "PAM config with enforce_for_root", + content: `password requisite pam_pwquality.so minlen=8 enforce_for_root=1`, + expected: &Config{ + Minlen: 8, + EnforceForRoot: 1, + }, + }, + { + name: "PAM config with both enforcing parameters", + content: `password requisite pam_pwquality.so minlen=8 enforcing=0 enforce_for_root=1`, + expected: &Config{ + Minlen: 8, + Enforcing: 0, + EnforceForRoot: 1, + }, + }, + { + name: "PAM config with enforce_for_root as flag (no value)", + content: `password requisite pam_pwquality.so minlen=8 enforce_for_root`, + expected: &Config{ + Minlen: 8, + EnforceForRoot: 1, // 独立标志形式,应设置为 1 + }, + }, + { + name: "PAM config with usercheck", + content: `password requisite pam_pwquality.so minlen=8 usercheck=1`, + expected: &Config{ + Minlen: 8, + Usercheck: 1, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := ParsePAMConfig([]byte(tt.content), nil) + if config.Minlen != tt.expected.Minlen { + t.Errorf("Minlen = %d, want %d", config.Minlen, tt.expected.Minlen) + } + if config.Dcredit != tt.expected.Dcredit { + t.Errorf("Dcredit = %d, want %d", config.Dcredit, tt.expected.Dcredit) + } + if config.Ucredit != tt.expected.Ucredit { + t.Errorf("Ucredit = %d, want %d", config.Ucredit, tt.expected.Ucredit) + } + if config.Lcredit != tt.expected.Lcredit { + t.Errorf("Lcredit = %d, want %d", config.Lcredit, tt.expected.Lcredit) + } + if config.Ocredit != tt.expected.Ocredit { + t.Errorf("Ocredit = %d, want %d", config.Ocredit, tt.expected.Ocredit) + } + if config.Minclass != tt.expected.Minclass { + t.Errorf("Minclass = %d, want %d", config.Minclass, tt.expected.Minclass) + } + if config.Maxrepeat != tt.expected.Maxrepeat { + t.Errorf("Maxrepeat = %d, want %d", config.Maxrepeat, tt.expected.Maxrepeat) + } + if config.Maxclassrepeat != tt.expected.Maxclassrepeat { + t.Errorf("Maxclassrepeat = %d, want %d", config.Maxclassrepeat, tt.expected.Maxclassrepeat) + } + if config.Maxsequence != tt.expected.Maxsequence { + t.Errorf("Maxsequence = %d, want %d", config.Maxsequence, tt.expected.Maxsequence) + } + }) + } +} + +func TestConfig_HasAnyPolicy(t *testing.T) { + tests := []struct { + name string + config *Config + expected bool + }{ + { + name: "nil config", + config: nil, + expected: false, + }, + { + name: "empty config", + config: &Config{}, + expected: false, + }, + { + name: "only minlen", + config: &Config{Minlen: 8}, + expected: true, + }, + { + name: "only dcredit", + config: &Config{Dcredit: -1}, + expected: true, + }, + { + name: "only ucredit", + config: &Config{Ucredit: -1}, + expected: true, + }, + { + name: "only lcredit", + config: &Config{Lcredit: -1}, + expected: true, + }, + { + name: "only ocredit", + config: &Config{Ocredit: -1}, + expected: true, + }, + { + name: "only minclass", + config: &Config{Minclass: 3}, + expected: true, + }, + { + name: "all fields set", + config: &Config{Minlen: 8, Dcredit: -1, Ucredit: -1, Lcredit: -1, Ocredit: -1, Minclass: 3}, + expected: true, + }, + { + name: "only lcredit and ocredit", + config: &Config{Lcredit: -1, Ocredit: -1}, + expected: true, + }, + { + name: "only maxrepeat", + config: &Config{Maxrepeat: 3}, + expected: true, + }, + { + name: "only maxclassrepeat", + config: &Config{Maxclassrepeat: 2}, + expected: true, + }, + { + name: "only maxsequence", + config: &Config{Maxsequence: 3}, + expected: true, + }, + { + name: "all new options", + config: &Config{Maxrepeat: 3, Maxclassrepeat: 2, Maxsequence: 3}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.config.HasAnyPolicy() + if got != tt.expected { + t.Errorf("HasAnyPolicy() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestConfig_IsEnforcing(t *testing.T) { + tests := []struct { + name string + config *Config + expected bool + }{ + { + name: "nil config - default enforcing", + config: nil, + expected: true, // 默认强制执行 + }, + { + name: "enforcing = 1", + config: &Config{Enforcing: 1}, + expected: true, + }, + { + name: "enforcing = 0", + config: &Config{Enforcing: 0}, + expected: false, + }, + { + name: "enforcing = 2", + config: &Config{Enforcing: 2}, + expected: true, // 非0值都视为强制执行 + }, + { + name: "default config - enforcing not set", + config: &Config{Enforcing: 1}, // 默认值为1 + expected: true, // 默认值为1,强制执行 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.config.IsEnforcing() + if got != tt.expected { + t.Errorf("IsEnforcing() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestConfig_IsEnforcingForRoot(t *testing.T) { + tests := []struct { + name string + config *Config + expected bool + }{ + { + name: "nil config - default not enforcing for root", + config: nil, + expected: false, // 默认不对 root 强制执行 + }, + { + name: "enforce_for_root = 1", + config: &Config{EnforceForRoot: 1}, + expected: true, + }, + { + name: "enforce_for_root = 0", + config: &Config{EnforceForRoot: 0}, + expected: false, + }, + { + name: "enforce_for_root = 2", + config: &Config{EnforceForRoot: 2}, + expected: false, // 只有1才表示强制执行 + }, + { + name: "default config - enforce_for_root not set", + config: &Config{}, + expected: false, // 默认值为0,不对 root 强制执行 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.config.IsEnforcingForRoot() + if got != tt.expected { + t.Errorf("IsEnforcingForRoot() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestConfig_GeneratePassword(t *testing.T) { + tests := []struct { + name string + config *Config + passwordGenerator func(int) string + wantError bool // 生成的密码是否应该通过验证 + description string + }{ + { + name: "nil config - should return empty or default password", + config: nil, + passwordGenerator: func(length int) string { + return "defaultpassword" + }, + wantError: false, + description: "nil 配置应该返回默认长度的密码", + }, + { + name: "no policy config - should return default password", + config: &Config{}, + passwordGenerator: func(length int) string { + return "defaultpassword" + }, + wantError: false, + description: "没有策略的配置应该返回默认长度的密码", + }, + { + name: "minlen only - generate valid password", + config: &Config{ + Minlen: 8, + }, + passwordGenerator: func(length int) string { + // 第一次返回不符合要求的密码,第二次返回符合要求的密码 + if length == 8 { + return "short" // 太短 + } + return "longpassword" // 符合要求 + }, + wantError: false, + description: "只有最小长度要求,应该生成符合要求的密码", + }, + { + name: "dcredit requirement - generate password with digits", + config: &Config{ + Minlen: 8, + Dcredit: -1, // 至少需要1个数字 + }, + passwordGenerator: func(length int) string { + // 第一次返回没有数字的密码,第二次返回有数字的密码 + if length == 8 { + return "nodigits" // 没有数字 + } + return "pass1word" // 有数字,符合要求 + }, + wantError: false, + description: "需要数字字符,应该生成包含数字的密码", + }, + { + name: "ucredit requirement - generate password with uppercase", + config: &Config{ + Minlen: 8, + Ucredit: -1, // 至少需要1个大写字母 + }, + passwordGenerator: func(length int) string { + // 第一次返回没有大写字母的密码,第二次返回有大写字母的密码 + if length == 8 { + return "nouppercase" // 没有大写字母 + } + return "passWord" // 有大写字母,符合要求 + }, + wantError: false, + description: "需要大写字母,应该生成包含大写字母的密码", + }, + { + name: "lcredit requirement - generate password with lowercase", + config: &Config{ + Minlen: 8, + Lcredit: -1, // 至少需要1个小写字母 + }, + passwordGenerator: func(length int) string { + // 第一次返回没有小写字母的密码,第二次返回有小写字母的密码 + if length == 8 { + return "NOLOWERCASE" // 没有小写字母 + } + return "PASSWORDw" // 有小写字母,符合要求 + }, + wantError: false, + description: "需要小写字母,应该生成包含小写字母的密码", + }, + { + name: "ocredit requirement - generate password with special char", + config: &Config{ + Minlen: 8, + Ocredit: -1, // 至少需要1个特殊字符 + }, + passwordGenerator: func(length int) string { + // 第一次返回没有特殊字符的密码,第二次返回有特殊字符的密码 + if length == 8 { + return "nospecial" // 没有特殊字符 + } + return "pass@word" // 有特殊字符,符合要求 + }, + wantError: false, + description: "需要特殊字符,应该生成包含特殊字符的密码", + }, + { + name: "complex requirements - multiple retries", + config: &Config{ + Minlen: 12, + Dcredit: -1, + Ucredit: -1, + Lcredit: -1, + Ocredit: -1, + Minclass: 3, + }, + passwordGenerator: func(length int) string { + // 模拟多次尝试:第一次太短,第二次缺少数字,第三次缺少大写,第四次符合要求 + switch length { + case 12: + return "short" // 太短 + case 13: + return "nouppercase1@" // 缺少大写字母 + case 14: + return "nolowercase1@A" // 缺少小写字母 + default: + return "ValidPass12@" // 符合所有要求:12个字符,包含数字、大写、小写、特殊字符 + } + }, + wantError: false, + description: "复杂要求,需要多次重试才能生成符合要求的密码", + }, + { + name: "nil passwordGenerator - should return empty", + config: &Config{ + Minlen: 8, + }, + passwordGenerator: nil, + wantError: false, + description: "passwordGenerator 为 nil 时应该返回空字符串", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + password := tt.config.GeneratePassword(tt.passwordGenerator) + + if tt.passwordGenerator == nil { + // 如果 passwordGenerator 为 nil,应该返回空字符串 + if password != "" { + t.Errorf("GeneratePassword() with nil generator = %q, want empty string", password) + } + return + } + + if password == "" { + t.Errorf("GeneratePassword() returned empty string, want non-empty password") + return + } + + // 验证生成的密码是否符合配置要求 + if tt.config != nil && tt.config.HasAnyPolicy() { + err := tt.config.Validate(password, "") + if tt.wantError { + if err == nil { + t.Errorf("GeneratePassword() generated password %q should fail validation but passed", password) + } + } else { + if err != nil { + t.Errorf("GeneratePassword() generated password %q failed validation: %v", password, err) + } + } + } + }) + } +} + +func TestConfig_GeneratePassword_RetryMechanism(t *testing.T) { + // 测试重试机制:确保在多次尝试后能生成符合要求的密码 + attemptCount := 0 + config := &Config{ + Minlen: 10, + Dcredit: -2, // 需要至少2个数字 + Ucredit: -1, // 需要至少1个大写字母 + Lcredit: -1, // 需要至少1个小写字母 + Ocredit: -1, // 需要至少1个特殊字符 + Enforcing: 1, // 强制执行 + } + + passwordGenerator := func(length int) string { + attemptCount++ + // 前几次生成不符合要求的密码 + switch attemptCount { + case 1: + return "short" // 太短 + case 2: + return "nouppercase12@" // 缺少大写字母 + case 3: + return "NOLOWERCASE12@" // 缺少小写字母 + case 4: + return "NoSpecial12" // 缺少特殊字符 + case 5: + return "ValidPass12@" // 符合所有要求 + default: + return "ValidPass12@" // 后续都返回符合要求的密码 + } + } + + password := config.GeneratePassword(passwordGenerator) + + if password == "" { + t.Fatal("GeneratePassword() returned empty string") + } + + // 验证密码符合要求 + err := config.Validate(password, "") + if err != nil { + t.Errorf("GeneratePassword() generated password %q failed validation: %v", password, err) + } + + // 验证确实进行了多次尝试(至少尝试了4次) + if attemptCount < 4 { + t.Errorf("Expected at least 4 attempts, got %d", attemptCount) + } +} + +func TestConfig_GeneratePassword_LengthCalculation(t *testing.T) { + // 测试密码长度计算逻辑 + tests := []struct { + name string + config *Config + expectedMinLen int + description string + }{ + { + name: "minlen only", + config: &Config{ + Minlen: 8, + }, + expectedMinLen: 8, + description: "只有最小长度要求", + }, + { + name: "minlen with credit requirements", + config: &Config{ + Minlen: 8, + Dcredit: -2, // 需要2个数字 + Ucredit: -1, // 需要1个大写字母 + }, + expectedMinLen: 8, // 至少是 minlen 和 requiredChars 的较大值 + description: "最小长度和 credit 要求", + }, + { + name: "minlen with minclass", + config: &Config{ + Minlen: 8, + Minclass: 3, + }, + expectedMinLen: 8, + description: "最小长度和最小字符类要求", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + callCount := 0 + passwordGenerator := func(length int) string { + callCount++ + // 第一次调用时记录长度 + if callCount == 1 { + if length < tt.expectedMinLen { + t.Errorf("First password generation length = %d, want at least %d", length, tt.expectedMinLen) + } + } + // 根据配置返回符合要求的密码 + if tt.config.Dcredit < 0 && -tt.config.Dcredit >= 2 { + // 需要至少2个数字 + return "ValidPass12@" + } + return "ValidPass1@" + } + + password := tt.config.GeneratePassword(passwordGenerator) + if password == "" { + t.Errorf("GeneratePassword() returned empty string") + } + + err := tt.config.Validate(password, "") + if err != nil { + t.Errorf("Generated password failed validation: %v", err) + } + }) + } +} + +func TestConfig_GeneratePassword_EdgeCases(t *testing.T) { + // 测试 GeneratePassword 的边界情况 + tests := []struct { + name string + config *Config + passwordGenerator func(int) string + description string + }{ + { + name: "minclass = 1 should not add buffer", + config: &Config{ + Minlen: 8, + Minclass: 1, // minclass = 1,不应该添加缓冲 + Dcredit: -1, + }, + passwordGenerator: func(length int) string { + return "Pass1word" + }, + description: "minclass = 1 时不应该添加额外的长度缓冲", + }, + { + name: "requiredChars > minLength", + config: &Config{ + Minlen: 5, + Dcredit: -3, // 需要3个数字 + Ucredit: -2, // 需要2个大写字母 + }, + passwordGenerator: func(length int) string { + // requiredChars = 5,应该使用5而不是minLength + if length < 5 { + t.Errorf("Expected length >= 5, got %d", length) + } + return "PASS123" + }, + description: "当 requiredChars > minLength 时,应该使用 requiredChars", + }, + { + name: "minLength = 0 with credit requirements", + config: &Config{ + Minlen: 0, // 没有设置最小长度 + Dcredit: -2, // 需要2个数字 + }, + passwordGenerator: func(length int) string { + // 应该使用默认的8,或者 requiredChars 的较大值 + if length < 2 { + t.Errorf("Expected length >= 2, got %d", length) + } + return "Pass12" + }, + description: "minLength = 0 时应该使用默认值8", + }, + { + name: "maxAttempts reached - should return longer password", + config: &Config{ + Minlen: 8, + Maxrepeat: 1, // 非常严格的限制 + }, + passwordGenerator: func(length int) string { + // 总是返回不符合要求的密码(包含重复字符) + return strings.Repeat("a", length) // 全部是重复字符 + }, + description: "当达到最大尝试次数时,应该返回一个较长的密码", + }, + { + name: "minclass > 1 should add buffer", + config: &Config{ + Minlen: 8, + Minclass: 3, // minclass > 1,应该添加缓冲 + Dcredit: -1, + }, + passwordGenerator: func(length int) string { + // 应该包含 minclass 的缓冲 + if length < 8+3 { + t.Errorf("Expected length >= 11 (8 + 3), got %d", length) + } + return "Pass1@word" + }, + description: "minclass > 1 时应该添加额外的长度缓冲", + }, + { + name: "requiredChars = 0", + config: &Config{ + Minlen: 8, + // 没有 credit 要求 + }, + passwordGenerator: func(length int) string { + if length != 8 { + t.Errorf("Expected length = 8, got %d", length) + } + return "password" + }, + description: "没有 credit 要求时,应该使用 minLength", + }, + { + name: "all positive credits", + config: &Config{ + Minlen: 8, + Dcredit: 1, + Ucredit: 1, + Lcredit: 1, + Ocredit: 1, + }, + passwordGenerator: func(length int) string { + return "Pass1@word" + }, + description: "所有 credit 都是正数时,应该正常生成密码", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + password := tt.config.GeneratePassword(tt.passwordGenerator) + if password == "" && tt.passwordGenerator != nil { + t.Errorf("GeneratePassword() returned empty string") + } + // 验证生成的密码(如果可能) + if password != "" && tt.config.HasAnyPolicy() { + err := tt.config.Validate(password, "") + // 对于 maxAttempts 测试,密码可能不符合要求 + if tt.name != "maxAttempts reached - should return longer password" { + if err != nil { + t.Errorf("Generated password failed validation: %v", err) + } + } + } + }) + } +} + +func TestConfig_GeneratePassword_MaxAttempts(t *testing.T) { + // 专门测试达到最大尝试次数的情况 + attemptCount := 0 + config := &Config{ + Minlen: 8, + Maxrepeat: 1, // 非常严格的限制,几乎不可能满足 + Enforcing: 1, // 强制执行 + } + + passwordGenerator := func(length int) string { + attemptCount++ + // 总是返回不符合要求的密码(全部是重复字符) + return strings.Repeat("a", length) + } + + password := config.GeneratePassword(passwordGenerator) + + // 应该返回一个密码(即使不符合要求) + if password == "" { + t.Error("GeneratePassword() should return a password even after max attempts") + } + + // 应该进行了多次尝试 + if attemptCount < 10 { + t.Errorf("Expected at least 10 attempts, got %d", attemptCount) + } + + // 密码长度应该增加了 + if len(password) < 8 { + t.Errorf("Expected password length >= 8 after retries, got %d", len(password)) + } +}