From d836d9903c813dbe057b7797bf643e43d3245c87 Mon Sep 17 00:00:00 2001 From: tamal Date: Mon, 12 Mar 2018 14:31:15 -0700 Subject: [PATCH] Refactor token command --- appscode/kubectl.go | 39 +++++++++ cli/utils.go | 57 ++++++++++++ cmds/token.go | 207 +++----------------------------------------- github/kubectl.go | 12 +++ gitlab/kubectl.go | 12 +++ google/kubectl.go | 116 +++++++++++++++++++++++++ 6 files changed, 246 insertions(+), 197 deletions(-) create mode 100644 appscode/kubectl.go create mode 100644 cli/utils.go create mode 100644 github/kubectl.go create mode 100644 gitlab/kubectl.go create mode 100644 google/kubectl.go diff --git a/appscode/kubectl.go b/appscode/kubectl.go new file mode 100644 index 000000000..2495669e9 --- /dev/null +++ b/appscode/kubectl.go @@ -0,0 +1,39 @@ +package appscode + +import ( + "fmt" + "strings" + + "github.com/appscode/go/term" + "github.com/howeyc/gopass" + "github.com/skratchdot/open-golang/open" +) + +func IssueToken() error { + teamId := term.Read("Team Id:") + endpoint := fmt.Sprintf("https://%v.appscode.io", teamId) + err := open.Start(strings.Join([]string{endpoint, "conduit", "login"}, "/")) + + term.Print("Paste the token here: ") + tokenBytes, err := gopass.GetPasswdMasked() + if err != nil { + term.Fatalln("Failed to retrieve token", err) + } + + token := string(tokenBytes) + client := &ConduitClient{ + Url: strings.Join([]string{endpoint, "api", "user.whoami"}, "/"), + Token: token, + } + result := &WhoAmIResponse{} + err = client.Call().Into(result) + if err != nil { + term.Fatalln("Failed to validate token", err) + } + if result.ErrorCode != nil { + term.Fatalln("Failed to validate token") + } + term.Successln("Token successfully generated") + + return err +} diff --git a/cli/utils.go b/cli/utils.go new file mode 100644 index 000000000..416f0b761 --- /dev/null +++ b/cli/utils.go @@ -0,0 +1,57 @@ +package cli + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/appscode/go/ioutil" + "github.com/appscode/go/term" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/util/homedir" +) + +func AddAuthInfoToKubeConfig(email string, authInfo *clientcmdapi.AuthInfo) error { + var konfig *clientcmdapi.Config + if _, err := os.Stat(KubeConfigPath()); err == nil { + // ~/.kube/config exists + konfig, err = clientcmd.LoadFromFile(KubeConfigPath()) + if err != nil { + return err + } + + bakFile := KubeConfigPath() + ".bak." + time.Now().Format("2006-01-02T15-04") + err = ioutil.CopyFile(bakFile, KubeConfigPath()) + if err != nil { + return err + } + term.Infoln(fmt.Sprintf("Current Kubeconfig is backed up as %s.", bakFile)) + } else { + konfig = &clientcmdapi.Config{ + APIVersion: "v1", + Kind: "Config", + Preferences: clientcmdapi.Preferences{ + Colors: true, + }, + } + } + + konfig.AuthInfos[email] = authInfo + + err := os.MkdirAll(filepath.Dir(KubeConfigPath()), 0755) + if err != nil { + return err + } + err = clientcmd.WriteToFile(*konfig, KubeConfigPath()) + if err != nil { + return err + } + term.Successln("Configuration has been written to", KubeConfigPath()) + return nil +} + +func KubeConfigPath() string { + return homedir.HomeDir() + "/.kube/config" +} diff --git a/cmds/token.go b/cmds/token.go index d1bc707c0..d66062ca5 100644 --- a/cmds/token.go +++ b/cmds/token.go @@ -1,31 +1,16 @@ package cmds import ( - "encoding/base64" "fmt" - "net" - "net/http" - "os" - "path/filepath" "strings" - "time" - "github.com/appscode/go/ioutil" "github.com/appscode/go/log" - "github.com/appscode/go/term" "github.com/appscode/guard/appscode" + "github.com/appscode/guard/github" + "github.com/appscode/guard/gitlab" "github.com/appscode/guard/google" "github.com/appscode/guard/server" - "github.com/howeyc/gopass" - "github.com/pkg/errors" - "github.com/skratchdot/open-golang/open" "github.com/spf13/cobra" - "golang.org/x/net/context" - "golang.org/x/oauth2" - goauth "golang.org/x/oauth2/google" - "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - "k8s.io/client-go/util/homedir" ) func NewCmdGetToken() *cobra.Command { @@ -37,21 +22,14 @@ func NewCmdGetToken() *cobra.Command { Run: func(cmd *cobra.Command, args []string) { org = strings.ToLower(org) switch org { - case "github": - codeURurl := "https://github.com/settings/tokens/new" - log.Infoln("Github url for personal access tokens:", codeURurl) - open.Start(codeURurl) - return - case "gitlab": - codeURurl := "https://gitlab.com/profile/personal_access_tokens" - log.Infoln("Gitlab url for personal access tokens:", codeURurl) - open.Start(codeURurl) - return - case "google": - getGoogleToken() - return - case "appscode": - getAppscodeToken() + case github.OrgType: + github.IssueToken() + case gitlab.OrgType: + gitlab.IssueToken() + case google.OrgType: + google.IssueToken() + case appscode.OrgType: + appscode.IssueToken() case "": log.Fatalln("Missing organization name. Set flag -o Google|Github.") default: @@ -63,168 +41,3 @@ func NewCmdGetToken() *cobra.Command { cmd.Flags().StringVarP(&org, "organization", "o", org, fmt.Sprintf("Name of Organization (%v).", server.SupportedOrgPrintForm())) return cmd } - -var gauthConfig oauth2.Config - -func getGoogleToken() error { - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return err - } - defer listener.Close() - log.Infoln("Oauth2 callback receiver listening on", listener.Addr()) - - // https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken - gauthConfig = oauth2.Config{ - Endpoint: goauth.Endpoint, - ClientID: google.GoogleOauth2ClientID, - ClientSecret: google.GoogleOauth2ClientSecret, - Scopes: []string{"openid", "profile", "email"}, - RedirectURL: "http://" + listener.Addr().String(), - } - // PromptSelectAccount allows a user who has multiple accounts at the authorization server - // to select amongst the multiple accounts that they may have current sessions for. - // eg: https://developers.google.com/identity/protocols/OpenIDConnect - promptSelectAccount := oauth2.SetAuthURLParam("prompt", "select_account") - codeURL := gauthConfig.AuthCodeURL("/", promptSelectAccount) - - log.Infoln("Auhtorization code URL:", codeURL) - open.Start(codeURL) - - http.HandleFunc("/", handleGoogleAuth) - return http.Serve(listener, nil) -} - -// https://developers.google.com/identity/protocols/OAuth2InstalledApp -func handleGoogleAuth(w http.ResponseWriter, r *http.Request) { - code := r.URL.Query().Get("code") - if code == "" { - return - } - token, err := gauthConfig.Exchange(context.Background(), code) - if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - w.Header().Set("Content-Type", "application/json") - w.Header().Set("x-content-type-options", "nosniff") - w.WriteHeader(http.StatusOK) - - err = addUserInKubeConfig(token.Extra("id_token").(string), token.RefreshToken) - if err != nil { - w.Write([]byte(fmt.Sprintf("Error: %v", err))) - return - } else { - w.Write([]byte("Configuration has been written to " + KubeConfigPath())) - return - } -} - -func getAppscodeToken() error { - teamId := term.Read("Team Id:") - endpoint := fmt.Sprintf("https://%v.appscode.io", teamId) - err := open.Start(strings.Join([]string{endpoint, "conduit", "login"}, "/")) - - term.Print("Paste the token here: ") - tokenBytes, err := gopass.GetPasswdMasked() - if err != nil { - term.Fatalln("Failed to retrieve token", err) - } - - token := string(tokenBytes) - client := &appscode.ConduitClient{ - Url: strings.Join([]string{endpoint, "api", "user.whoami"}, "/"), - Token: token, - } - result := &appscode.WhoAmIResponse{} - err = client.Call().Into(result) - if err != nil { - term.Fatalln("Failed to validate token", err) - } - if result.ErrorCode != nil { - term.Fatalln("Failed to validate token") - } - term.Successln("Token successfully generated") - - return err -} - -func addUserInKubeConfig(idToken, refreshToken string) error { - var konfig *clientcmdapi.Config - if _, err := os.Stat(KubeConfigPath()); err == nil { - // ~/.kube/config exists - konfig, err = clientcmd.LoadFromFile(KubeConfigPath()) - if err != nil { - return err - } - - bakFile := KubeConfigPath() + ".bak." + time.Now().Format("2006-01-02T15-04") - err = ioutil.CopyFile(bakFile, KubeConfigPath()) - if err != nil { - return err - } - term.Infoln(fmt.Sprintf("Current Kubeconfig is backed up as %s.", bakFile)) - } else { - konfig = &clientcmdapi.Config{ - APIVersion: "v1", - Kind: "Config", - Preferences: clientcmdapi.Preferences{ - Colors: true, - }, - } - } - - authInfo := &clientcmdapi.AuthInfo{ - AuthProvider: &clientcmdapi.AuthProviderConfig{ - Name: "oidc", - Config: map[string]string{ - "client-id": google.GoogleOauth2ClientID, - "client-secret": google.GoogleOauth2ClientSecret, - "id-token": idToken, - "idp-issuer-url": "https://accounts.google.com", - "refresh-token": refreshToken, - }, - }, - } - email, err := getEmailFromIdToken(idToken) - if err != nil { - return errors.Wrap(err, "failed to retrive emial from idToken") - } - konfig.AuthInfos[email] = authInfo - - err = os.MkdirAll(filepath.Dir(KubeConfigPath()), 0755) - if err != nil { - return err - } - err = clientcmd.WriteToFile(*konfig, KubeConfigPath()) - if err != nil { - return err - } - term.Successln("Configuration has been written to", KubeConfigPath()) - return nil -} - -func getEmailFromIdToken(idToken string) (string, error) { - parts := strings.Split(idToken, ".") - if len(parts) < 2 { - return "", fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts)) - } - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return "", fmt.Errorf("oidc: malformed jwt payload: %v", err) - } - - c := struct { - Email string `json:"email"` - }{} - - err = json.Unmarshal(payload, &c) - if err != nil { - return "", err - } - return c.Email, nil -} - -func KubeConfigPath() string { - return homedir.HomeDir() + "/.kube/config" -} diff --git a/github/kubectl.go b/github/kubectl.go new file mode 100644 index 000000000..be4401b07 --- /dev/null +++ b/github/kubectl.go @@ -0,0 +1,12 @@ +package github + +import ( + "github.com/appscode/go/term" + "github.com/skratchdot/open-golang/open" +) + +func IssueToken() { + codeURurl := "https://github.com/settings/tokens/new" + term.Infoln("Github url for personal access tokens:", codeURurl) + open.Start(codeURurl) +} diff --git a/gitlab/kubectl.go b/gitlab/kubectl.go new file mode 100644 index 000000000..c95572b06 --- /dev/null +++ b/gitlab/kubectl.go @@ -0,0 +1,12 @@ +package gitlab + +import ( + "github.com/appscode/go/term" + "github.com/skratchdot/open-golang/open" +) + +func IssueToken() { + codeURurl := "https://gitlab.com/profile/personal_access_tokens" + term.Infoln("Gitlab url for personal access tokens:", codeURurl) + open.Start(codeURurl) +} diff --git a/google/kubectl.go b/google/kubectl.go new file mode 100644 index 000000000..a8558fced --- /dev/null +++ b/google/kubectl.go @@ -0,0 +1,116 @@ +package google + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + + "github.com/appscode/go/log" + "github.com/appscode/guard/cli" + "github.com/pkg/errors" + "github.com/skratchdot/open-golang/open" + "golang.org/x/net/context" + "golang.org/x/oauth2" + goauth "golang.org/x/oauth2/google" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +var gauthConfig oauth2.Config + +func IssueToken() error { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return err + } + defer listener.Close() + log.Infoln("Oauth2 callback receiver listening on", listener.Addr()) + + // https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken + gauthConfig = oauth2.Config{ + Endpoint: goauth.Endpoint, + ClientID: GoogleOauth2ClientID, + ClientSecret: GoogleOauth2ClientSecret, + Scopes: []string{"openid", "profile", "email"}, + RedirectURL: "http://" + listener.Addr().String(), + } + // PromptSelectAccount allows a user who has multiple accounts at the authorization server + // to select amongst the multiple accounts that they may have current sessions for. + // eg: https://developers.google.com/identity/protocols/OpenIDConnect + promptSelectAccount := oauth2.SetAuthURLParam("prompt", "select_account") + codeURL := gauthConfig.AuthCodeURL("/", promptSelectAccount) + + log.Infoln("Auhtorization code URL:", codeURL) + open.Start(codeURL) + + http.HandleFunc("/", handleGoogleAuth) + return http.Serve(listener, nil) +} + +// https://developers.google.com/identity/protocols/OAuth2InstalledApp +func handleGoogleAuth(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + if code == "" { + return + } + token, err := gauthConfig.Exchange(context.Background(), code) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("x-content-type-options", "nosniff") + w.WriteHeader(http.StatusOK) + + err = addAuthInfo(token.Extra("id_token").(string), token.RefreshToken) + if err != nil { + w.Write([]byte(fmt.Sprintf("Error: %v", err))) + return + } else { + w.Write([]byte("Configuration has been written to " + cli.KubeConfigPath())) + return + } +} + +func addAuthInfo(idToken, refreshToken string) error { + authInfo := &clientcmdapi.AuthInfo{ + AuthProvider: &clientcmdapi.AuthProviderConfig{ + Name: "oidc", + Config: map[string]string{ + "client-id": GoogleOauth2ClientID, + "client-secret": GoogleOauth2ClientSecret, + "id-token": idToken, + "idp-issuer-url": "https://accounts.google.com", + "refresh-token": refreshToken, + }, + }, + } + email, err := getEmailFromIdToken(idToken) + if err != nil { + return errors.Wrap(err, "failed to retrieve email from idToken") + } + return cli.AddAuthInfoToKubeConfig(email, authInfo) +} + +func getEmailFromIdToken(idToken string) (string, error) { + parts := strings.Split(idToken, ".") + if len(parts) < 2 { + return "", fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts)) + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "", fmt.Errorf("oidc: malformed jwt payload: %v", err) + } + + c := struct { + Email string `json:"email"` + }{} + + err = json.Unmarshal(payload, &c) + if err != nil { + return "", err + } + return c.Email, nil +}