diff --git a/cli/auth/authProvider.go b/cli/auth/authProvider.go new file mode 100644 index 000000000..f56f3bbb0 --- /dev/null +++ b/cli/auth/authProvider.go @@ -0,0 +1,123 @@ +package auth + +import ( + "context" + "errors" + "fmt" + "github.com/google/uuid" + "github.com/up9inc/mizu/cli/logger" + "github.com/up9inc/mizu/cli/uiUtils" + "golang.org/x/oauth2" + "net" + "net/http" + "time" +) + +const loginTimeoutInMin = 2 + +// Ports are configured in keycloak "cli" client as valid redirect URIs. A change here must be reflected there as well. +var listenPorts = []int{3141, 4001, 5002, 6003, 7004, 8005, 9006, 10007} + +func LoginInteractively(envName string) (*oauth2.Token, error) { + tokenChannel := make(chan *oauth2.Token) + errorChannel := make(chan error) + + server := http.Server{} + go startLoginServer(tokenChannel, errorChannel, envName, &server) + + defer func() { + if err := server.Shutdown(context.Background()); err != nil { + logger.Log.Debugf("Error shutting down server, err: %v", err) + } + }() + + select { + case <-time.After(loginTimeoutInMin * time.Minute): + return nil, errors.New("auth timed out") + case err := <-errorChannel: + return nil, err + case token := <-tokenChannel: + return token, nil + } +} + +func startLoginServer(tokenChannel chan *oauth2.Token, errorChannel chan error, envName string, server *http.Server) { + for _, port := range listenPorts { + var config = &oauth2.Config{ + ClientID: "cli", + RedirectURL: fmt.Sprintf("http://localhost:%v/callback", port), + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("https://auth.%s/auth/realms/testr/protocol/openid-connect/auth", envName), + TokenURL: fmt.Sprintf("https://auth.%s/auth/realms/testr/protocol/openid-connect/token", envName), + }, + } + + state := uuid.New() + + mux := http.NewServeMux() + server.Handler = mux + mux.Handle("/callback", loginCallbackHandler(tokenChannel, errorChannel, config, envName, state)) + + listener, listenErr := net.Listen("tcp", fmt.Sprintf("%s:%d", "127.0.0.1", port)) + if listenErr != nil { + logger.Log.Debugf("failed to start listening on port %v, err: %v", port, listenErr) + continue + } + + authorizationUrl := config.AuthCodeURL(state.String()) + uiUtils.OpenBrowser(authorizationUrl) + + serveErr := server.Serve(listener) + if serveErr == http.ErrServerClosed { + logger.Log.Debugf("received server shutdown, server on port %v is closed", port) + return + } else if serveErr != nil { + logger.Log.Debugf("failed to start serving on port %v, err: %v", port, serveErr) + continue + } + + logger.Log.Debugf("didn't receive server closed on port %v", port) + return + } + + errorChannel <- fmt.Errorf("failed to start serving on all listen ports, ports: %v", listenPorts) +} + +func loginCallbackHandler(tokenChannel chan *oauth2.Token, errorChannel chan error, config *oauth2.Config, envName string, state uuid.UUID) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if err := request.ParseForm(); err != nil { + errorMsg := fmt.Sprintf("failed to parse form, err: %v", err) + http.Error(writer, errorMsg, http.StatusBadRequest) + errorChannel <- fmt.Errorf(errorMsg) + return + } + + requestState := request.Form.Get("state") + if requestState != state.String() { + errorMsg := fmt.Sprintf("state invalid, requestState: %v, authState:%v", requestState, state.String()) + http.Error(writer, errorMsg, http.StatusBadRequest) + errorChannel <- fmt.Errorf(errorMsg) + return + } + + code := request.Form.Get("code") + if code == "" { + errorMsg := "code not found" + http.Error(writer, errorMsg, http.StatusBadRequest) + errorChannel <- fmt.Errorf(errorMsg) + return + } + + token, err := config.Exchange(context.Background(), code) + if err != nil { + errorMsg := fmt.Sprintf("failed to create token, err: %v", err) + http.Error(writer, errorMsg, http.StatusInternalServerError) + errorChannel <- fmt.Errorf(errorMsg) + return + } + + tokenChannel <- token + + http.Redirect(writer, request, fmt.Sprintf("https://%s/CliLogin", envName), http.StatusFound) + }) +} diff --git a/cli/cmd/auth.go b/cli/cmd/auth.go new file mode 100644 index 000000000..97a8c7bcb --- /dev/null +++ b/cli/cmd/auth.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var authCmd = &cobra.Command{ + Use: "auth", + Short: "Authenticate to up9 application", +} + +func init() { + rootCmd.AddCommand(authCmd) +} + diff --git a/cli/cmd/authLogin.go b/cli/cmd/authLogin.go new file mode 100644 index 000000000..fca09c402 --- /dev/null +++ b/cli/cmd/authLogin.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/up9inc/mizu/cli/auth" + "github.com/up9inc/mizu/cli/config" + "github.com/up9inc/mizu/cli/config/configStructs" + "github.com/up9inc/mizu/cli/logger" + "github.com/up9inc/mizu/cli/telemetry" +) + +var authLoginCmd = &cobra.Command{ + Use: "login", + Short: "Login to up9 application", + RunE: func(cmd *cobra.Command, args []string) error { + go telemetry.ReportRun("authLogin", config.Config.Auth) + + token, err := auth.LoginInteractively(config.Config.Auth.EnvName) + if err != nil { + logger.Log.Errorf("Failed login interactively, err: %v", err) + return nil + } + + authConfig := configStructs.AuthConfig{ + EnvName: config.Config.Auth.EnvName, + Token: token.AccessToken, + } + + config.Config.Auth = authConfig + + if err := config.WriteConfig(&config.Config); err != nil { + logger.Log.Errorf("Failed writing config with auth, err: %v", err) + return nil + } + + logger.Log.Infof("Login successfully, token stored in config") + + return nil + }, +} + +func init() { + authCmd.AddCommand(authLoginCmd) +} diff --git a/cli/cmd/authLogout.go b/cli/cmd/authLogout.go new file mode 100644 index 000000000..96906f860 --- /dev/null +++ b/cli/cmd/authLogout.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/up9inc/mizu/cli/config" + "github.com/up9inc/mizu/cli/logger" + "github.com/up9inc/mizu/cli/telemetry" +) + +var authLogoutCmd = &cobra.Command{ + Use: "logout", + Short: "Logout from up9 application", + RunE: func(cmd *cobra.Command, args []string) error { + go telemetry.ReportRun("authLogout", config.Config.Auth) + + config.Config.Auth.Token = "" + + if err := config.WriteConfig(&config.Config); err != nil { + logger.Log.Errorf("Failed writing config with default auth, err: %v", err) + return nil + } + + logger.Log.Infof("Logout successfully, token removed from config") + + return nil + }, +} + +func init() { + authCmd.AddCommand(authLogoutCmd) +} diff --git a/cli/cmd/common.go b/cli/cmd/common.go index f4160db58..b60de5a8d 100644 --- a/cli/cmd/common.go +++ b/cli/cmd/common.go @@ -4,9 +4,7 @@ import ( "context" "fmt" "os" - "os/exec" "os/signal" - "runtime" "syscall" "github.com/up9inc/mizu/cli/config" @@ -49,21 +47,3 @@ func waitForFinish(ctx context.Context, cancel context.CancelFunc) { } } -func openBrowser(url string) { - var err error - - switch runtime.GOOS { - case "linux": - err = exec.Command("xdg-open", url).Start() - case "windows": - err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() - case "darwin": - err = exec.Command("open", url).Start() - default: - err = fmt.Errorf("unsupported platform") - } - - if err != nil { - logger.Log.Errorf("error while opening browser, %v", err) - } -} diff --git a/cli/cmd/config.go b/cli/cmd/config.go index 0a7815d9a..41e12930a 100644 --- a/cli/cmd/config.go +++ b/cli/cmd/config.go @@ -9,7 +9,6 @@ import ( "github.com/up9inc/mizu/cli/logger" "github.com/up9inc/mizu/cli/telemetry" "github.com/up9inc/mizu/cli/uiUtils" - "io/ioutil" ) var configCmd = &cobra.Command{ @@ -18,22 +17,30 @@ var configCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { go telemetry.ReportRun("config", config.Config.Config) - template, err := config.GetConfigWithDefaults() + configWithDefaults, err := config.GetConfigWithDefaults() if err != nil { - logger.Log.Errorf("Failed generating config with defaults %v", err) + logger.Log.Errorf("Failed generating config with defaults, err: %v", err) return nil } + if config.Config.Config.Regenerate { - data := []byte(template) - if err := ioutil.WriteFile(config.Config.ConfigFilePath, data, 0644); err != nil { - logger.Log.Errorf("Failed writing config %v", err) + if err := config.WriteConfig(configWithDefaults); err != nil { + logger.Log.Errorf("Failed writing config with defaults, err: %v", err) return nil } + logger.Log.Infof(fmt.Sprintf("Template File written to %s", fmt.Sprintf(uiUtils.Purple, config.Config.ConfigFilePath))) } else { + template, err := uiUtils.PrettyYaml(configWithDefaults) + if err != nil { + logger.Log.Errorf("Failed converting config with defaults to yaml, err: %v", err) + return nil + } + logger.Log.Debugf("Writing template config.\n%v", template) fmt.Printf("%v", template) } + return nil }, } diff --git a/cli/cmd/tapRunner.go b/cli/cmd/tapRunner.go index 0839498c7..a3bfd76fc 100644 --- a/cli/cmd/tapRunner.go +++ b/cli/cmd/tapRunner.go @@ -585,7 +585,7 @@ func watchApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provi } logger.Log.Infof("Mizu is available at %s\n", url) - openBrowser(url) + uiUtils.OpenBrowser(url) requestForAnalysisIfNeeded() if err := apiserver.Provider.ReportTappedPods(state.currentlyTappedPods); err != nil { logger.Log.Debugf("[Error] failed update tapped pods %v", err) diff --git a/cli/cmd/viewRunner.go b/cli/cmd/viewRunner.go index 63ce6d695..499e6baf9 100644 --- a/cli/cmd/viewRunner.go +++ b/cli/cmd/viewRunner.go @@ -57,7 +57,8 @@ func runMizuView() { logger.Log.Infof("Mizu is available at %s\n", url) - openBrowser(url) + uiUtils.OpenBrowser(url) + if isCompatible, err := version.CheckVersionCompatibility(); err != nil { logger.Log.Errorf("Failed to check versions compatibility %v", err) cancel() diff --git a/cli/config/config.go b/cli/config/config.go index 8a65bdf28..4a1580aee 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -54,16 +54,30 @@ func InitConfig(cmd *cobra.Command) error { return nil } -func GetConfigWithDefaults() (string, error) { +func GetConfigWithDefaults() (*ConfigStruct, error) { defaultConf := ConfigStruct{} if err := defaults.Set(&defaultConf); err != nil { - return "", err + return nil, err } configElem := reflect.ValueOf(&defaultConf).Elem() setZeroForReadonlyFields(configElem) - return uiUtils.PrettyYaml(defaultConf) + return &defaultConf, nil +} + +func WriteConfig(config *ConfigStruct) error { + template, err := uiUtils.PrettyYaml(config) + if err != nil { + return fmt.Errorf("failed converting config to yaml, err: %v", err) + } + + data := []byte(template) + if err := ioutil.WriteFile(Config.ConfigFilePath, data, 0644); err != nil { + return fmt.Errorf("failed writing config, err: %v", err) + } + + return nil } func mergeConfigFile(configFilePath string) error { diff --git a/cli/config/configStruct.go b/cli/config/configStruct.go index 42987010b..50e1049a7 100644 --- a/cli/config/configStruct.go +++ b/cli/config/configStruct.go @@ -21,6 +21,7 @@ type ConfigStruct struct { Version configStructs.VersionConfig `yaml:"version"` View configStructs.ViewConfig `yaml:"view"` Logs configStructs.LogsConfig `yaml:"logs"` + Auth configStructs.AuthConfig `yaml:"auth"` Config configStructs.ConfigConfig `yaml:"config,omitempty"` AgentImage string `yaml:"agent-image,omitempty" readonly:""` ImagePullPolicyStr string `yaml:"image-pull-policy" default:"Always"` diff --git a/cli/config/configStructs/authConfig.go b/cli/config/configStructs/authConfig.go new file mode 100644 index 000000000..550364528 --- /dev/null +++ b/cli/config/configStructs/authConfig.go @@ -0,0 +1,6 @@ +package configStructs + +type AuthConfig struct { + EnvName string `yaml:"env-name" default:"up9.app"` + Token string `yaml:"token"` +} diff --git a/cli/config/config_test.go b/cli/config/config_test.go index 7f0751f0c..35d63b519 100644 --- a/cli/config/config_test.go +++ b/cli/config/config_test.go @@ -3,6 +3,7 @@ package config_test import ( "fmt" "github.com/up9inc/mizu/cli/config" + "gopkg.in/yaml.v3" "reflect" "strings" "testing" @@ -15,10 +16,11 @@ func TestConfigWriteIgnoresReadonlyFields(t *testing.T) { getFieldsWithReadonlyTag(configElem, &readonlyFields) configWithDefaults, _ := config.GetConfigWithDefaults() + configWithDefaultsBytes, _ := yaml.Marshal(configWithDefaults) for _, readonlyField := range readonlyFields { t.Run(readonlyField, func(t *testing.T) { - readonlyFieldToCheck := fmt.Sprintf("\n%s:", readonlyField) - if strings.Contains(configWithDefaults, readonlyFieldToCheck) { + readonlyFieldToCheck := fmt.Sprintf(" %s:", readonlyField) + if strings.Contains(string(configWithDefaultsBytes), readonlyFieldToCheck) { t.Errorf("unexpected result - readonly field: %v, config: %v", readonlyField, configWithDefaults) } }) diff --git a/cli/go.mod b/cli/go.mod index 577df798c..11c0f116a 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -6,12 +6,13 @@ require ( github.com/creasty/defaults v1.5.1 github.com/denisbrodbeck/machineid v1.0.1 github.com/google/go-github/v37 v37.0.0 - github.com/gorilla/websocket v1.4.2 + github.com/google/uuid v1.1.2 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 github.com/up9inc/mizu/shared v0.0.0 github.com/up9inc/mizu/tap/api v0.0.0 + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b k8s.io/api v0.21.2 k8s.io/apimachinery v0.21.2 diff --git a/cli/go.sum b/cli/go.sum index f19b35b91..f274a030f 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -237,7 +237,6 @@ github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyyc github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= diff --git a/cli/uiUtils/openBrowser.go b/cli/uiUtils/openBrowser.go new file mode 100644 index 000000000..0f2998cf9 --- /dev/null +++ b/cli/uiUtils/openBrowser.go @@ -0,0 +1,27 @@ +package uiUtils + +import ( + "fmt" + "github.com/up9inc/mizu/cli/logger" + "os/exec" + "runtime" +) + +func OpenBrowser(url string) { + var err error + + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", url).Start() + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + default: + err = fmt.Errorf("unsupported platform") + } + + if err != nil { + logger.Log.Errorf("error while opening browser, %v", err) + } +}