diff --git a/Makefile b/Makefile index f2fec5a3..b71af095 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,7 @@ all: koko-ui $(call make_artifact_full,darwin,arm64) $(call make_artifact_full,linux,amd64) $(call make_artifact_full,linux,arm64) + $(call make_artifact_full,linux,mips64le) $(call make_artifact_full,linux,ppc64le) $(call make_artifact_full,linux,s390x) $(call make_artifact_full,linux,riscv64) @@ -83,6 +84,9 @@ linux-arm64: koko-ui linux-loong64: koko-ui $(call make_artifact_full,linux,loong64) +linux-mips64le: koko-ui + $(call make_artifact_full,linux,mips64le) + linux-ppc64le: koko-ui $(call make_artifact_full,linux,ppc64le) diff --git a/go.mod b/go.mod index a6ef023e..63a9f768 100644 --- a/go.mod +++ b/go.mod @@ -33,9 +33,9 @@ require ( github.com/spf13/viper v1.12.0 github.com/xlab/treeprint v1.1.0 go.mongodb.org/mongo-driver v1.8.3 - golang.org/x/crypto v0.14.0 - golang.org/x/term v0.13.0 - golang.org/x/text v0.13.0 + golang.org/x/crypto v0.19.0 + golang.org/x/term v0.17.0 + golang.org/x/text v0.14.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 k8s.io/api v0.23.1 k8s.io/apimachinery v0.23.1 @@ -106,7 +106,7 @@ require ( golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect google.golang.org/appengine v1.6.7 // indirect @@ -125,6 +125,6 @@ require ( ) replace ( - github.com/gliderlabs/ssh => github.com/LeeEirc/ssh v0.1.2-0.20231007053448-a6110c0dfc4a - golang.org/x/crypto => github.com/LeeEirc/crypto v0.0.0-20230919154755-059031d26b68 + github.com/gliderlabs/ssh => github.com/jumpserver-dev/ssh v0.1.2-0.20240209035326-4f0f6365ccae + golang.org/x/crypto => github.com/jumpserver-dev/crypto v0.0.0-20240209025851-4b55dc4c3463 ) diff --git a/go.sum b/go.sum index 585365b7..6204a0db 100644 --- a/go.sum +++ b/go.sum @@ -59,14 +59,10 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/clickhouse-go v1.5.4 h1:cKjXeYLNWVJIx2J1K6H2CqyRmfwVJVY1OV1coaaFcI0= github.com/ClickHouse/clickhouse-go v1.5.4/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/LeeEirc/crypto v0.0.0-20230919154755-059031d26b68 h1:bkeuW/ujHp3Rr1K3Ah4/Onw9Ero9uMKX0rPxbzV5mvw= -github.com/LeeEirc/crypto v0.0.0-20230919154755-059031d26b68/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= github.com/LeeEirc/elfinder v0.0.14 h1:6ObxwIoC5zmrnKArUU5Mz++/T3lzgl1Ja0pS1Smd3j4= github.com/LeeEirc/elfinder v0.0.14/go.mod h1:d1bMAAydkZSBxSN/EuQjBg6B0xcPP3boHuYEpzEHYTs= github.com/LeeEirc/httpsig v1.2.1 h1:GGmCc2Bug3KeCchlZHwrfyjyAnw+JlzMjKDobPypirs= github.com/LeeEirc/httpsig v1.2.1/go.mod h1:aoLZLXCSNDgkzsH2sGLWn3hlVbF+Voe8fCArxLt9nWA= -github.com/LeeEirc/ssh v0.1.2-0.20231007053448-a6110c0dfc4a h1:/EdJeCK6cTaKNgftQLP9uyBL4Q86MFawU0WsK22yn2A= -github.com/LeeEirc/ssh v0.1.2-0.20231007053448-a6110c0dfc4a/go.mod h1:O9BMs9PYwCJbftRP9O2Ig5Wd3hbLSpzhvP0bqU9EONg= github.com/LeeEirc/tclientlib v0.0.3-0.20230803101925-fb52a90cb08d h1:4qUSGc/34IALiDs2kBrjbCKfx7zvAt16K+gTRzNN8Fo= github.com/LeeEirc/tclientlib v0.0.3-0.20230803101925-fb52a90cb08d/go.mod h1:TF2v0XZYyRcZfx4NmA/EEFRkdKZLsQd8YnlhGKl1KUA= github.com/LeeEirc/terminalparser v0.0.0-20220328021224-de16b7643ea4 h1:Gk7m4Nu2jqVqJAJqNlTYqkiq96WkANAtB4fVi+t7Xv8= @@ -279,6 +275,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jumpserver-dev/crypto v0.0.0-20240209025851-4b55dc4c3463 h1:ZQTX/4gO/G8SFtzn9K8M2mac83TSIyEHX9/2bnBKf1Y= +github.com/jumpserver-dev/crypto v0.0.0-20240209025851-4b55dc4c3463/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +github.com/jumpserver-dev/ssh v0.1.2-0.20240209035326-4f0f6365ccae h1:zND4fMpJoOf90TILA9CY9KTub+YLK+pdiG6LM/+9UBo= +github.com/jumpserver-dev/ssh v0.1.2-0.20240209035326-4f0f6365ccae/go.mod h1:KdoSNwfOgcFXlcr2OQ34eeKfIl7K8l6cAQ6twkYaLcU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= @@ -522,7 +522,7 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -612,16 +612,17 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -633,9 +634,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/auth/ssh.go b/pkg/auth/ssh.go index be31acee..210551cd 100644 --- a/pkg/auth/ssh.go +++ b/pkg/auth/ssh.go @@ -18,6 +18,10 @@ type SSHAuthFunc func(ctx ssh.Context, password, publicKey string) (res ssh.Auth func SSHPasswordAndPublicKeyAuth(jmsService *service.JMService) SSHAuthFunc { return func(ctx ssh.Context, password, publicKey string) (res ssh.AuthResult) { + if password == "" && publicKey == "" { + logger.Errorf("SSH conn[%s] no password and publickey", ctx.SessionID()) + return ssh.AuthFailed + } remoteAddr, _, _ := net.SplitHostPort(ctx.RemoteAddr().String()) username := ctx.User() if req, ok := parseDirectLoginReq(jmsService, ctx); ok { @@ -35,25 +39,22 @@ func SSHPasswordAndPublicKeyAuth(jmsService *service.JMService) SSHAuthFunc { if password != "" { authMethod = "password" } - userAuthClient, ok := ctx.Value(ContextKeyClient).(*UserAuthClient) - if !ok { - newClient := jmsService.CloneClient() - var accessKey model.AccessKey - conf := config.GetConf() - _ = accessKey.LoadFromFile(conf.AccessKeyFilePath) - userClient := service.NewUserClient( - service.UserClientUsername(username), - service.UserClientRemoteAddr(remoteAddr), - service.UserClientLoginType("T"), - service.UserClientHttpClient(&newClient), - service.UserClientSvcSignKey(accessKey), - ) - userAuthClient = &UserAuthClient{ - UserClient: userClient, - authOptions: make(map[string]authOptions), - } - ctx.SetValue(ContextKeyClient, userAuthClient) + newClient := jmsService.CloneClient() + var accessKey model.AccessKey + conf := config.GetConf() + _ = accessKey.LoadFromFile(conf.AccessKeyFilePath) + userClient := service.NewUserClient( + service.UserClientUsername(username), + service.UserClientRemoteAddr(remoteAddr), + service.UserClientLoginType("T"), + service.UserClientHttpClient(&newClient), + service.UserClientSvcSignKey(accessKey), + ) + userAuthClient := &UserAuthClient{ + UserClient: userClient, + authOptions: make(map[string]authOptions), } + ctx.SetValue(ContextKeyClient, userAuthClient) userAuthClient.SetOption(service.UserClientPassword(password), service.UserClientPublicKey(publicKey)) logger.Infof("SSH conn[%s] authenticating user %s %s", ctx.SessionID(), username, authMethod) diff --git a/pkg/handler/select_handler.go b/pkg/handler/select_handler.go index 861e6fe4..08652fe4 100644 --- a/pkg/handler/select_handler.go +++ b/pkg/handler/select_handler.go @@ -5,7 +5,6 @@ import ( "sort" "strconv" "strings" - "time" "github.com/jumpserver/koko/pkg/i18n" "github.com/jumpserver/koko/pkg/jms-sdk-go/model" @@ -218,8 +217,6 @@ func (u *UserSelectHandler) DisplayCurrentResult() { func (u *UserSelectHandler) Proxy(target model.PermAsset) { u.proxyAsset(target) - time.Sleep(time.Second * 2) - u.DisplayCurrentResult() } func (u *UserSelectHandler) Retrieve(pageSize, offset int, searches ...string) []model.PermAsset { diff --git a/pkg/handler/server_ssh.go b/pkg/handler/server_ssh.go index 866b29f3..5d0c64cd 100644 --- a/pkg/handler/server_ssh.go +++ b/pkg/handler/server_ssh.go @@ -321,10 +321,28 @@ func (s *Server) proxyTokenInfo(sess ssh.Session, tokeInfo *model.ConnectToken) } } +func IsScpCommand(rawStr string) bool { + rawCommands := strings.Split(rawStr, ";") + for _, cmd := range rawCommands { + cmd = strings.TrimSpace(cmd) + if strings.HasPrefix(cmd, "scp") { + return true + } + } + return false +} + +func (s *Server) recordSessionLifecycle(sid string, event model.LifecycleEvent, reason string) { + logObj := model.SessionLifecycleLog{Reason: reason} + if err2 := s.jmsService.RecordSessionLifecycleLog(sid, event, logObj); err2 != nil { + logger.Errorf("Record session %s lifecycle %s failed: %s", sid, event, err2) + } +} + func (s *Server) proxyAssetCommand(sess ssh.Session, sshClient *srvconn.SSHClient, tokeInfo *model.ConnectToken) { rawStr := sess.RawCommand() - if strings.HasPrefix(rawStr, "scp") && !config.GetConf().EnableVscodeSupport { + if IsScpCommand(rawStr) && !config.GetConf().EnableVscodeSupport { logger.Errorf("Not support scp command: %s", rawStr) utils.IgnoreErrWriteString(sess, "Not support scp command") return @@ -360,6 +378,7 @@ func (s *Server) proxyAssetCommand(sess ssh.Session, sshClient *srvconn.SSHClien } ctx, cancel := context.WithCancel(sess.Context()) defer cancel() + traceSession := session.NewSession(&respSession, func(task *model.TerminalTask) error { switch task.Name { case model.TaskKillSession: @@ -372,7 +391,7 @@ func (s *Server) proxyAssetCommand(sess ssh.Session, sshClient *srvconn.SSHClien defer func() { if err2 := s.jmsService.SessionFinished(respSession.ID, modelCommon.NewNowUTCTime()); err2 != nil { - logger.Errorf("Create tunnel session err: %s", err) + logger.Errorf("Create tunnel session err: %s", err2) } session.RemoveSession(traceSession) }() @@ -382,6 +401,7 @@ func (s *Server) proxyAssetCommand(sess ssh.Session, sshClient *srvconn.SSHClien logger.Errorf("Get SSH session failed: %s", err) return } + s.recordSessionLifecycle(respSession.ID, model.AssetConnectSuccess, "") defer goSess.Close() defer sshClient.ReleaseSession(goSess) go func() { @@ -444,6 +464,8 @@ func (s *Server) proxyAssetCommand(sess ssh.Session, sshClient *srvconn.SSHClien logger.Errorf("User %s Run command %s failed: %s", tokeInfo.User.String(), rawStr, err) } + reason := string(model.ReasonErrConnectDisconnect) + s.recordSessionLifecycle(respSession.ID, model.AssetConnectFinished, reason) } func (s *Server) proxyVscodeShell(sess ssh.Session, vsReq *vscodeReq, sshClient *srvconn.SSHClient, @@ -469,7 +491,7 @@ func (s *Server) proxyVscodeShell(sess ssh.Session, vsReq *vscodeReq, sshClient session.AddSession(traceSession) defer func() { if err2 := s.jmsService.SessionFinished(respSession.ID, modelCommon.NewNowUTCTime()); err2 != nil { - logger.Errorf("Create tunnel session err: %s", err) + logger.Errorf("Create tunnel session err: %s", err2) } session.RemoveSession(traceSession) }() @@ -479,7 +501,7 @@ func (s *Server) proxyVscodeShell(sess ssh.Session, vsReq *vscodeReq, sshClient logger.Errorf("Get SSH session failed: %s", err) return err } - + s.recordSessionLifecycle(respSession.ID, model.AssetConnectSuccess, "") defer goSess.Close() defer sshClient.ReleaseSession(goSess) stdOut, err := goSess.StdoutPipe() @@ -495,6 +517,7 @@ func (s *Server) proxyVscodeShell(sess ssh.Session, vsReq *vscodeReq, sshClient err = goSess.Shell() if err != nil { logger.Errorf("Get SSH session shell failed: %s", err) + s.recordSessionLifecycle(respSession.ID, model.AssetConnectFinished, err.Error()) return err } logger.Infof("User %s start vscode request to %s", vsReq.user, sshClient) @@ -514,11 +537,15 @@ func (s *Server) proxyVscodeShell(sess ssh.Session, vsReq *vscodeReq, sshClient case <-ctx.Done(): logger.Infof("SSH conn[%s] User %s end vscode request %s as session done", vsReq.reqId, vsReq.user, sshClient) + reason := string(model.ReasonErrConnectDisconnect) + s.recordSessionLifecycle(respSession.ID, model.AssetConnectFinished, reason) return nil case now := <-ticker.C: if vsReq.expireInfo.IsExpired(now) { logger.Infof("SSH conn[%s] User %s end vscode request %s as permission has expired", vsReq.reqId, vsReq.user, sshClient) + reason := string(model.ReasonErrPermissionExpired) + s.recordSessionLifecycle(respSession.ID, model.AssetConnectFinished, reason) return nil } logger.Debugf("SSH conn[%s] user %s vscode request still alive", vsReq.reqId, vsReq.user) diff --git a/pkg/httpd/tty.go b/pkg/httpd/tty.go index 7b9ff088..c0ba06e4 100644 --- a/pkg/httpd/tty.go +++ b/pkg/httpd/tty.go @@ -433,6 +433,8 @@ func (h *tty) JoinRoom(c *Client, roomID string) { Body: nil, Meta: meta, }) + logObj := model.SessionLifecycleLog{User: h.ws.user.String()} + h.ws.RecordLifecycleLog(roomID, model.UserJoinSession, logObj) for { buf := make([]byte, 1024) nr, err := c.Read(buf) @@ -451,6 +453,7 @@ func (h *tty) JoinRoom(c *Client, roomID string) { Body: nil, Meta: meta, }) + h.ws.RecordLifecycleLog(roomID, model.UserLeaveSession, logObj) logger.Infof("Conn[%s] user read end", c.ID()) if err := h.ws.apiClient.FinishShareRoom(h.shareInfo.Record.ID); err != nil { logger.Infof("Conn[%s] finish share room err: %s", c.ID(), err) @@ -463,6 +466,8 @@ func (h *tty) Monitor(c *Client, roomID string) { conn := exchange.WrapperUserCon(c) room.Subscribe(conn) defer room.UnSubscribe(conn) + logObj := model.SessionLifecycleLog{User: h.ws.user.String()} + h.ws.RecordLifecycleLog(roomID, model.AdminJoinMonitor, logObj) for { buf := make([]byte, 1024) _, err := c.Read(buf) @@ -473,5 +478,6 @@ func (h *tty) Monitor(c *Client, roomID string) { logger.Debugf("Conn[%s] user monitor", c.ID()) } logger.Infof("Conn[%s] user read end", c.ID()) + h.ws.RecordLifecycleLog(roomID, model.AdminExitMonitor, logObj) } } diff --git a/pkg/httpd/userwebsocket.go b/pkg/httpd/userwebsocket.go index 4c30a29e..d6c2d7d6 100644 --- a/pkg/httpd/userwebsocket.go +++ b/pkg/httpd/userwebsocket.go @@ -241,3 +241,10 @@ var ( ErrDisableShare = errors.New("disable share") ErrPermissionDenied = errors.New("permission denied") ) + +func (userCon *UserWebsocket) RecordLifecycleLog(sid string, event model.LifecycleEvent, + logObj model.SessionLifecycleLog) { + if err := userCon.apiClient.RecordSessionLifecycleLog(sid, event, logObj); err != nil { + logger.Errorf("Record session lifecycle log err: %s", err) + } +} diff --git a/pkg/httpd/webserver.go b/pkg/httpd/webserver.go index bbce9e2d..944f4387 100644 --- a/pkg/httpd/webserver.go +++ b/pkg/httpd/webserver.go @@ -196,7 +196,7 @@ func (s *Server) GenerateViewMeta(targetId string) (meta ViewPageMata) { if err != nil { logger.Errorf("Get core api public setting err: %s", err) } - meta.IconURL = setting.LogoURLS.Favicon + meta.IconURL = setting.Interface.Favicon return } diff --git a/pkg/jms-sdk-go/model/session.go b/pkg/jms-sdk-go/model/session.go index 5d3763b9..e56b7e82 100644 --- a/pkg/jms-sdk-go/model/session.go +++ b/pkg/jms-sdk-go/model/session.go @@ -100,3 +100,41 @@ const ( SessionReplayErrUploadFailed ReplayError = "replay_upload_failed" SessionReplayErrUnsupported ReplayError = "replay_unsupported" ) + +type LifecycleEvent string + +const ( + AssetConnectSuccess LifecycleEvent = "asset_connect_success" + AssetConnectFinished LifecycleEvent = "asset_connect_finished" + CreateShareLink LifecycleEvent = "create_share_link" + UserJoinSession LifecycleEvent = "user_join_session" + UserLeaveSession LifecycleEvent = "user_leave_session" + AdminJoinMonitor LifecycleEvent = "admin_join_monitor" + AdminExitMonitor LifecycleEvent = "admin_exit_monitor" + ReplayConvertStart LifecycleEvent = "replay_convert_start" + ReplayConvertSuccess LifecycleEvent = "replay_convert_success" + ReplayConvertFailure LifecycleEvent = "replay_convert_failure" + ReplayUploadStart LifecycleEvent = "replay_upload_start" + ReplayUploadSuccess LifecycleEvent = "replay_upload_success" + ReplayUploadFailure LifecycleEvent = "replay_upload_failure" +) + +type SessionLifecycleLog struct { + Reason string `json:"reason"` + User string `json:"user"` +} + +var EmptyLifecycleLog = SessionLifecycleLog{} + +type SessionLifecycleReasonErr string + +const ( + ReasonErrConnectFailed SessionLifecycleReasonErr = "connect_failed" + ReasonErrConnectDisconnect SessionLifecycleReasonErr = "connect_disconnect" + ReasonErrUserClose SessionLifecycleReasonErr = "user_close" + ReasonErrIdleDisconnect SessionLifecycleReasonErr = "idle_disconnect" + ReasonErrAdminTerminate SessionLifecycleReasonErr = "admin_terminate" + ReasonErrMaxSessionTimeout SessionLifecycleReasonErr = "max_session_timeout" + ReasonErrPermissionExpired SessionLifecycleReasonErr = "permission_expired" + ReasonErrNullStorage SessionLifecycleReasonErr = "null_storage" +) diff --git a/pkg/jms-sdk-go/model/setting.go b/pkg/jms-sdk-go/model/setting.go index 4faaabdb..3e2fbdb1 100644 --- a/pkg/jms-sdk-go/model/setting.go +++ b/pkg/jms-sdk-go/model/setting.go @@ -1,13 +1,13 @@ package model type PublicSetting struct { - LoginTitle string `json:"LOGIN_TITLE"` - LogoURLS struct { - LogOut string `json:"logo_logout"` - Index string `json:"logo_index"` - Image string `json:"login_image"` - Favicon string `json:"favicon"` - } `json:"LOGO_URLS"` + Interface struct { + LoginTitle string `json:"login_title"` + LogOut string `json:"logo_logout"` + Index string `json:"logo_index"` + Image string `json:"login_image"` + Favicon string `json:"favicon"` + } `json:"INTERFACE"` EnableWatermark bool `json:"SECURITY_WATERMARK_ENABLED"` EnableSessionShare bool `json:"SECURITY_SESSION_SHARE"` EnableAnnouncement bool `json:"ANNOUNCEMENT_ENABLED"` @@ -32,13 +32,6 @@ type PublicSetting struct { "SECURITY_PASSWORD_EXPIRATION_TIME": 10000, "SECURITY_LUNA_REMEMBER_AUTH": true, "XPACK_LICENSE_IS_VALID": true, - "LOGIN_TITLE": "欢迎使用JumpServer开源堡垒机", - "LOGO_URLS": { - "logo_logout": "/static/img/logo.png", - "logo_index": "/static/img/logo_text.png", - "login_image": "/static/img/login_image.jpg", - "favicon": "/static/img/facio.ico" - }, "TICKETS_ENABLED": true, "PASSWORD_RULE": { "SECURITY_PASSWORD_MIN_LENGTH": 6, @@ -53,6 +46,17 @@ type PublicSetting struct { "AUTH_FEISHU": true, "SECURITY_WATERMARK_ENABLED": true, "SECURITY_SESSION_SHARE": true, - "XRDP_ENABLED": true + "XRDP_ENABLED": true, + INTERFACE: { + logo_logout: "/static/img/logo.png", + logo_index: "/static/img/logo_text_white.png", + login_image: "/static/img/login_image.png", + favicon: "/static/img/facio.ico", + login_title: "JumpServer 开源堡垒机", + theme: "classic_green", + theme_info: { }, + beian_link: "", + beian_text: "" + } } */ diff --git a/pkg/jms-sdk-go/service/jms_session_record.go b/pkg/jms-sdk-go/service/jms_session_record.go new file mode 100644 index 00000000..ac877402 --- /dev/null +++ b/pkg/jms-sdk-go/service/jms_session_record.go @@ -0,0 +1,24 @@ +package service + +import ( + "fmt" + + "github.com/jumpserver/koko/pkg/jms-sdk-go/model" +) + +func (s *JMService) RecordSessionLifecycleLog(sid string, event model.LifecycleEvent, logObj model.SessionLifecycleLog) (err error) { + data := map[string]interface{}{ + "event": event, + } + if logObj.Reason != "" { + data["reason"] = logObj.Reason + } + if logObj.User != "" { + data["user"] = logObj.User + } + + reqURL := fmt.Sprintf(SessionLifecycleLogURL, sid) + var resp map[string]interface{} + _, err = s.authClient.Post(reqURL, data, &resp) + return +} diff --git a/pkg/jms-sdk-go/service/url.go b/pkg/jms-sdk-go/service/url.go index 5feb6255..acbb69d2 100644 --- a/pkg/jms-sdk-go/service/url.go +++ b/pkg/jms-sdk-go/service/url.go @@ -27,6 +27,8 @@ const ( FTPLogListURL = "/api/v1/audits/ftp-logs/" // 上传 ftp日志 FTPLogUpdateURL = "/api/v1/audits/ftp-logs/%s/" FTPLogFileURL = "/api/v1/audits/ftp-logs/%s/upload/" + + SessionLifecycleLogURL = "/api/v1/terminal/sessions/%s/lifecycle_log/" ) // 授权相关API diff --git a/pkg/koko/task.go b/pkg/koko/task.go index dd21178d..428a0fb6 100644 --- a/pkg/koko/task.go +++ b/pkg/koko/task.go @@ -43,6 +43,13 @@ func uploadRemainReplay(jmsService *service.JMService) { return nil }) + recordLifecycleLog := func(id string, event model.LifecycleEvent, reason string) { + logObj := model.SessionLifecycleLog{Reason: reason} + if err1 := jmsService.RecordSessionLifecycleLog(id, event, logObj); err1 != nil { + logger.Errorf("Update session %s activity log failed: %s", id, err1) + } + } + for absPath, remainReplay := range allRemainFiles { absGzPath := absPath if !remainReplay.IsGzip { @@ -64,18 +71,22 @@ func uploadRemainReplay(jmsService *service.JMService) { } _ = os.Remove(absPath) } - Target, _ := filepath.Rel(replayDir, absGzPath) + target, _ := filepath.Rel(replayDir, absGzPath) + + recordLifecycleLog(remainReplay.Id, model.ReplayUploadStart, "") logger.Infof("Upload replay file: %s, type: %s", absGzPath, replayStorage.TypeName()) - if err2 := replayStorage.Upload(absGzPath, Target); err2 != nil { + if err2 := replayStorage.Upload(absGzPath, target); err2 != nil { logger.Errorf("Upload remain replay file %s failed: %s", absGzPath, err2) reason := model.SessionReplayErrUploadFailed if err3 := jmsService.SessionReplayFailed(remainReplay.Id, reason); err3 != nil { logger.Errorf("Update session %s status %s failed: %s", remainReplay.Id, reason, err3) } + recordLifecycleLog(remainReplay.Id, model.ReplayUploadFailure, err2.Error()) continue } - if err := jmsService.FinishReply(remainReplay.Id); err != nil { - logger.Errorf("Notify session %s upload failed: %s", remainReplay.Id, err) + recordLifecycleLog(remainReplay.Id, model.ReplayUploadSuccess, "") + if err1 := jmsService.FinishReply(remainReplay.Id); err1 != nil { + logger.Errorf("Notify session %s upload failed: %s", remainReplay.Id, err1) continue } _ = os.Remove(absGzPath) diff --git a/pkg/proxy/recorder.go b/pkg/proxy/recorder.go index 31e18700..09b3fc88 100644 --- a/pkg/proxy/recorder.go +++ b/pkg/proxy/recorder.go @@ -201,6 +201,7 @@ func (r *ReplyRecorder) Record(p []byte) { func (r *ReplyRecorder) End() { if r.isNullStorage() { + r.recordLifecycleLog(model.ReplayUploadFailure, string(model.ReasonErrNullStorage)) return } _ = r.file.Close() @@ -230,8 +231,10 @@ func (r *ReplyRecorder) uploadReplay() { func (r *ReplyRecorder) UploadGzipFile(maxRetry int) { if r.isNullStorage() { _ = os.Remove(r.absGzipFilePath) + r.recordLifecycleLog(model.ReplayUploadFailure, string(model.ReasonErrNullStorage)) return } + r.recordLifecycleLog(model.ReplayUploadStart, "") for i := 0; i <= maxRetry; i++ { logger.Infof("Upload replay file: %s, type: %s", r.absGzipFilePath, r.storage.TypeName()) err := r.storage.Upload(r.absGzipFilePath, r.Target) @@ -240,8 +243,10 @@ func (r *ReplyRecorder) UploadGzipFile(maxRetry int) { if err = r.jmsService.FinishReply(r.SessionID); err != nil { logger.Errorf("Session[%s] finish replay err: %s", r.SessionID, err) } + r.recordLifecycleLog(model.ReplayUploadSuccess, "") break } + r.recordLifecycleLog(model.ReplayUploadFailure, err.Error()) logger.Errorf("Upload replay file err: %s", err) // 如果还是失败,上传 server 再传一次 if i == maxRetry { @@ -260,6 +265,13 @@ func (r *ReplyRecorder) UploadGzipFile(maxRetry int) { } } +func (r *ReplyRecorder) recordLifecycleLog(event model.LifecycleEvent, reason string) { + eventLog := model.SessionLifecycleLog{Reason: reason} + if err := r.jmsService.RecordSessionLifecycleLog(r.SessionID, event, eventLog); err != nil { + logger.Errorf("Update session %s activity log %s failed: %s", r.SessionID, event, err) + } +} + type ReplyInfo struct { Width int Height int diff --git a/pkg/proxy/server.go b/pkg/proxy/server.go index dc012acf..72d2ec8c 100644 --- a/pkg/proxy/server.go +++ b/pkg/proxy/server.go @@ -1074,9 +1074,18 @@ func (s *Server) Proxy() { if err2 := s.ConnectedFailedCallback(err); err2 != nil { logger.Errorf("Conn[%s] update session err: %s", s.UserConn.ID(), err2) } + errLog := model.SessionLifecycleLog{Reason: err.Error()} + if err1 := s.jmsService.RecordSessionLifecycleLog(s.sessionInfo.ID, model.AssetConnectFinished, + errLog); err1 != nil { + logger.Errorf("Conn[%s] record session activity log err: %s", s.UserConn.ID(), err1) + } return } defer srvCon.Close() + if err1 := s.jmsService.RecordSessionLifecycleLog(s.sessionInfo.ID, model.AssetConnectSuccess, + model.EmptyLifecycleLog); err1 != nil { + logger.Errorf("Conn[%s] record session activity log err: %s", s.UserConn.ID(), err1) + } logger.Infof("Conn[%s] create session %s success", s.UserConn.ID(), s.ID) if err2 := s.ConnectedSuccessCallback(); err2 != nil { diff --git a/pkg/proxy/switch.go b/pkg/proxy/switch.go index f7e6eaaf..350cdc50 100644 --- a/pkg/proxy/switch.go +++ b/pkg/proxy/switch.go @@ -292,6 +292,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo msg = utils.WrapperWarn(msg) replayRecorder.Record([]byte(msg)) room.Broadcast(&exchange.RoomMessage{Event: exchange.DataEvent, Body: []byte("\n\r" + msg)}) + s.recordSessionFinished(model.ReasonErrMaxSessionTimeout) return } @@ -302,6 +303,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo msg = utils.WrapperWarn(msg) replayRecorder.Record([]byte(msg)) room.Broadcast(&exchange.RoomMessage{Event: exchange.DataEvent, Body: []byte("\n\r" + msg)}) + s.recordSessionFinished(model.ReasonErrIdleDisconnect) return } if s.p.CheckPermissionExpired(now) { @@ -310,6 +312,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo msg = utils.WrapperWarn(msg) replayRecorder.Record([]byte(msg)) room.Broadcast(&exchange.RoomMessage{Event: exchange.DataEvent, Body: []byte("\n\r" + msg)}) + s.recordSessionFinished(model.ReasonErrPermissionExpired) return } continue @@ -321,6 +324,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo replayRecorder.Record([]byte(msg)) logger.Infof("Session[%s]: %s", s.ID, msg) room.Broadcast(&exchange.RoomMessage{Event: exchange.DataEvent, Body: []byte("\n\r" + msg)}) + s.recordSessionFinished(model.ReasonErrAdminTerminate) return // 监控窗口大小变化 case win, ok := <-winCh: @@ -339,6 +343,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo // 经过parse处理的server数据,发给user case p, ok := <-srvOutChan: if !ok { + s.recordSessionFinished(model.ReasonErrConnectDisconnect) return } if parser.NeedRecord() { @@ -352,6 +357,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo // 经过parse处理的user数据,发给server case p, ok := <-userOutChan: if !ok { + s.recordSessionFinished(model.ReasonErrUserClose) return } if _, err1 := srvConn.Write(p); err1 != nil { @@ -367,9 +373,11 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo continue case <-userConn.Context().Done(): logger.Infof("Session[%s]: user conn context done", s.ID) + s.recordSessionFinished(model.ReasonErrUserClose) return nil case <-exitSignal: logger.Debugf("Session[%s] end by exit signal", s.ID) + s.recordSessionFinished(model.ReasonErrConnectDisconnect) return case notifyMsg := <-s.notifyMsgChan: logger.Infof("Session[%s] notify event: %s", s.ID, notifyMsg.Event) @@ -379,3 +387,10 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo lastActiveTime = time.Now() } } + +func (s *SwitchSession) recordSessionFinished(reason model.SessionLifecycleReasonErr) { + logObj := model.SessionLifecycleLog{Reason: string(reason)} + if err := s.p.jmsService.RecordSessionLifecycleLog(s.ID, model.AssetConnectFinished, logObj); err != nil { + logger.Errorf("Session[%s] record session asset_connect_finished failed: %s", s.ID, err) + } +} diff --git a/pkg/srvconn/conn_clickhouse.go b/pkg/srvconn/conn_clickhouse.go index b5987bf8..ee0df55f 100644 --- a/pkg/srvconn/conn_clickhouse.go +++ b/pkg/srvconn/conn_clickhouse.go @@ -2,6 +2,7 @@ package srvconn import ( "fmt" + "net/url" "os" "strconv" @@ -106,8 +107,8 @@ func (opt *sqlOption) ClickHouseDataSourceName() string { opt.Host, opt.Port, opt.DBName, - opt.Username, - opt.Password, + url.QueryEscape(opt.Username), + url.QueryEscape(opt.Password), ) } diff --git a/pkg/srvconn/conn_mysql.go b/pkg/srvconn/conn_mysql.go index 184a3056..2ce500a6 100644 --- a/pkg/srvconn/conn_mysql.go +++ b/pkg/srvconn/conn_mysql.go @@ -215,6 +215,12 @@ func (opt *sqlOption) Envs() []string { } envs := make([]string, 0, 6) + // 设置下系统环境的语言, 中文输入问题 + envLang := os.Getenv("LANG") + if envLang == "" { + envLang = "zh_CN.UTF-8" + } + envs = append(envs, fmt.Sprintf("LANG=%s", envLang)) envs = append(envs, fmt.Sprintf("USERNAME=%s", opt.Username)) envs = append(envs, fmt.Sprintf("HOSTNAME=%s", opt.Host)) envs = append(envs, fmt.Sprintf("PORT=%d", opt.Port)) diff --git a/pkg/srvconn/conn_redis.go b/pkg/srvconn/conn_redis.go index 3bd37c3f..227cdb6d 100644 --- a/pkg/srvconn/conn_redis.go +++ b/pkg/srvconn/conn_redis.go @@ -154,6 +154,10 @@ func checkRedisAccount(args *sqlOption) error { if args.UseSSL { tlsConfig := tls.Config{} + // 连接使用的是内部地址或者localhost时,跳过证书验证 + if args.Host == "127.0.0.1" || args.Host == "localhost" { + tlsConfig.InsecureSkipVerify = true + } if args.CaCert != "" { rootCAs := x509.NewCertPool() rootCAs.AppendCertsFromPEM([]byte(args.CaCert)) diff --git a/pkg/srvconn/sftp_asset.go b/pkg/srvconn/sftp_asset.go index fff2a7d3..bc97a005 100644 --- a/pkg/srvconn/sftp_asset.go +++ b/pkg/srvconn/sftp_asset.go @@ -535,6 +535,7 @@ func (ad *AssetDir) GetSFTPAndRealPath(su *model.PermAccount, path string) (conn traceSession := session.NewSession(&respSession, terminalFunc) session.AddSession(traceSession) ad.sftpTraceSessions[su.String()] = traceSession + ad.recordSessionLifecycle(traceSession.ID, model.AssetConnectSuccess, "") } if conn.rootDirPath == "" { platform := conn.token.Platform @@ -581,7 +582,7 @@ func (ad *AssetDir) GetSftpClient(su *model.PermAccount) (conn *SftpConn, err er if err != nil { return nil, fmt.Errorf("get connect token account err: %s", err2) } - return ad.getNewSftpConn(&connectToken) + return ad.getNewSftpConn(&connectToken, su) } func (ad *AssetDir) createConnectToken(su *model.PermAccount) (model.ConnectToken, error) { @@ -609,7 +610,8 @@ func (ad *AssetDir) createConnectToken(su *model.PermAccount) (model.ConnectToke return ad.jmsService.GetConnectTokenInfo(tokenInfo.ID) } -func (ad *AssetDir) getNewSftpConn(connectToken *model.ConnectToken) (conn *SftpConn, err error) { +func (ad *AssetDir) getNewSftpConn(connectToken *model.ConnectToken, + su *model.PermAccount) (conn *SftpConn, err error) { if ad.detailAsset == nil { return nil, errNoSelectAsset } @@ -680,6 +682,11 @@ func (ad *AssetDir) getNewSftpConn(connectToken *model.ConnectToken) (conn *Sftp sshClient.ReleaseSession(sess) _ = sshClient.Close() logger.Infof("User %s SSH client(%s) for SFTP release", user.String(), sshClient) + if sftpSession, ok := ad.sftpTraceSessions[su.String()]; ok { + sid := sftpSession.ID + reason := string(model.ReasonErrConnectDisconnect) + ad.recordSessionLifecycle(sid, model.AssetConnectFinished, reason) + } }() homeDirPath, err := sftpClient.Getwd() if err != nil { @@ -741,6 +748,13 @@ func (ad *AssetDir) CreateFTPLog(su *model.PermAccount, operate, filename string return &data } +func (ad *AssetDir) recordSessionLifecycle(sid string, event model.LifecycleEvent, reason string) { + logObj := model.SessionLifecycleLog{Reason: reason} + if err := ad.jmsService.RecordSessionLifecycleLog(sid, event, logObj); err != nil { + logger.Errorf("Update session %s lifecycle %s failed: %s", sid, event, err) + } +} + func IsExistPath(client *sftp.Client, path string) bool { _, err := client.Stat(path) return err == nil diff --git a/ui/src/components/RightPanel.vue b/ui/src/components/RightPanel.vue index 8568b5d8..1f3056d0 100644 --- a/ui/src/components/RightPanel.vue +++ b/ui/src/components/RightPanel.vue @@ -6,9 +6,6 @@ >
-
- -
@@ -45,7 +42,7 @@ export default { } }, mounted() { - this.init() + // this.init() this.insertToBody() }, beforeDestroy() { @@ -107,6 +104,9 @@ export default { const element = this.$refs.container const body = document.querySelector('body') body.insertBefore(element, body.firstChild) + }, + toggle() { + this.show = !this.show } } } diff --git a/ui/src/components/Terminal.vue b/ui/src/components/Terminal.vue index 2730ddfd..13838187 100644 --- a/ui/src/components/Terminal.vue +++ b/ui/src/components/Terminal.vue @@ -185,6 +185,9 @@ export default { this.term.focus() } break + case 'OPEN': + this.$emit("event", "open", this.terminalId) + break } console.log('KoKo got post message: ', msg) }, @@ -258,6 +261,8 @@ export default { this.lastSendTime = new Date(); this.$log.debug("term on data event") data = this.preprocessInput(data) + + this.sendEventToLuna('KEYBOARDEVENT', '') this.ws.send(this.message(this.terminalId, 'TERMINAL_DATA', data)); }); diff --git a/ui/src/views/Connection.vue b/ui/src/views/Connection.vue index 48816a4f..36392f24 100644 --- a/ui/src/views/Connection.vue +++ b/ui/src/views/Connection.vue @@ -8,7 +8,7 @@ v-on:event="onEvent" v-on:ws-data="onWsData"> - + @@ -386,6 +386,10 @@ export default { }) this.$log.debug("reconnect: ", data); break + case 'open': + this.$log.debug("open: ", data); + this.$refs.panel.toggle() + break } }, getMinuteLabel(item) { diff --git a/ui/src/views/ShareTerminal.vue b/ui/src/views/ShareTerminal.vue index 098dcd60..02b60389 100644 --- a/ui/src/views/ShareTerminal.vue +++ b/ui/src/views/ShareTerminal.vue @@ -3,10 +3,11 @@ - + @@ -197,6 +198,14 @@ export default { this.themeBackground = themeColors.background; } this.$log.debug(val); + }, + onEvent(event, data) { + switch (event) { + case 'open': + this.$log.debug("open: ", data); + this.$refs.panel.toggle() + break + } } },