github.com/spinnaker/spin@v1.30.0/cmd/gateclient/client.go (about)

     1  // Copyright (c) 2018, Google, Inc.
     2  // Copyright (c) 2019, Noel Cower.
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  //   http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  //   Unless required by applicable law or agreed to in writing, software
    11  //   distributed under the License is distributed on an "AS IS" BASIS,
    12  //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  //   See the License for the specific language governing permissions and
    14  //   limitations under the License.
    15  
    16  package gateclient
    17  
    18  import (
    19  	"bufio"
    20  	"context"
    21  	"crypto/rand"
    22  	"crypto/sha256"
    23  	"crypto/tls"
    24  	"crypto/x509"
    25  	"encoding/base64"
    26  	"fmt"
    27  	"io/ioutil"
    28  	"net/http"
    29  	"net/http/cookiejar"
    30  	_ "net/http/pprof"
    31  	"net/url"
    32  	"os"
    33  	"path/filepath"
    34  	"strings"
    35  	"syscall"
    36  
    37  	"github.com/pkg/errors"
    38  	"golang.org/x/crypto/ssh/terminal"
    39  	"golang.org/x/oauth2"
    40  	"golang.org/x/oauth2/google"
    41  	"sigs.k8s.io/yaml"
    42  
    43  	"github.com/spinnaker/spin/cmd/output"
    44  	"github.com/spinnaker/spin/config"
    45  	"github.com/spinnaker/spin/config/auth"
    46  	iap "github.com/spinnaker/spin/config/auth/iap"
    47  	gate "github.com/spinnaker/spin/gateapi"
    48  	"github.com/spinnaker/spin/util"
    49  	"github.com/spinnaker/spin/version"
    50  )
    51  
    52  const (
    53  	// defaultConfigFileMode is the default file mode used for config files. This corresponds to
    54  	// the Unix file permissions u=rw,g=,o= so that config files with cached tokens, at least by
    55  	// default, are only readable by the user that owns the config file.
    56  	defaultConfigFileMode os.FileMode = 0600 // u=rw,g=,o=
    57  )
    58  
    59  // GatewayClient is the wrapper with authentication
    60  type GatewayClient struct {
    61  	// The exported fields below should be set by anyone using a command
    62  	// with an GatewayClient field. These are expected to be set externally
    63  	// (not from within the command itself).
    64  
    65  	// Generate Gate Api client.
    66  	*gate.APIClient
    67  
    68  	// Spin CLI configuration.
    69  	Config config.Config
    70  
    71  	// Context for OAuth2 access token.
    72  	Context context.Context
    73  
    74  	// This is the set of flags global to the command parser.
    75  	gateEndpoint string
    76  
    77  	ignoreCertErrors bool
    78  
    79  	ignoreRedirects bool
    80  
    81  	// Location of the spin config.
    82  	configLocation string
    83  
    84  	// Raw Http Client to do OAuth2 login.
    85  	httpClient *http.Client
    86  
    87  	ui output.Ui
    88  
    89  	// Maximum time to wait (when polling) for a task to become completed.
    90  	retryTimeout int
    91  }
    92  
    93  func (m *GatewayClient) GateEndpoint() string {
    94  	if m.Config.Gate.Endpoint == "" && m.gateEndpoint == "" {
    95  		return "http://localhost:8084"
    96  	}
    97  	if m.gateEndpoint != "" {
    98  		return m.gateEndpoint
    99  	}
   100  	return m.Config.Gate.Endpoint
   101  }
   102  
   103  func (m *GatewayClient) RetryTimeout() int {
   104  	if m.Config.Gate.RetryTimeout == 0 && m.retryTimeout == 0 {
   105  		return 60
   106  	}
   107  	if m.retryTimeout != 0 {
   108  		return m.retryTimeout
   109  	}
   110  	return m.Config.Gate.RetryTimeout
   111  }
   112  
   113  // Create new spinnaker gateway client with flag
   114  func NewGateClient(ui output.Ui, gateEndpoint, defaultHeaders, configLocation string, ignoreCertErrors bool, ignoreRedirects bool, retryTimeout int) (*GatewayClient, error) {
   115  	gateClient := &GatewayClient{
   116  		gateEndpoint:     gateEndpoint,
   117  		ignoreCertErrors: ignoreCertErrors,
   118  		ignoreRedirects:  ignoreRedirects,
   119  		ui:               ui,
   120  		retryTimeout:     retryTimeout,
   121  		Context:          context.Background(),
   122  	}
   123  
   124  	err := userConfig(gateClient, configLocation)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	// Api client initialization.
   130  	httpClient, err := InitializeHTTPClient(gateClient.Config.Auth)
   131  	if err != nil {
   132  		ui.Error("Could not initialize http client, failing.")
   133  		return nil, unwrapErr(ui, err)
   134  	}
   135  
   136  	// If IgnoreRedirects is set to true, CheckRedirect will return a special error type
   137  	// 'ErrUseLastResponse', telling the client not to follow redirects
   138  	if ignoreRedirects || (gateClient.Config.Auth != nil && gateClient.Config.Auth.IgnoreRedirects) {
   139  		httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
   140  			return http.ErrUseLastResponse
   141  		}
   142  	}
   143  
   144  	gateClient.Context, err = ContextWithAuth(gateClient.Context, gateClient.Config.Auth)
   145  
   146  	if ignoreCertErrors {
   147  		if httpClient.Transport.(*http.Transport).TLSClientConfig == nil {
   148  			httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
   149  				InsecureSkipVerify: true,
   150  			}
   151  		} else {
   152  			httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
   153  		}
   154  	}
   155  
   156  	gateClient.httpClient = httpClient
   157  	updatedConfig := false
   158  	updatedMessage := ""
   159  
   160  	if gateClient.Config.Auth != nil && gateClient.Config.Auth.OAuth2 != nil {
   161  		// The below will fail if the token is expired and there is no refresh token.
   162  		// This may happen if refresh tokens are not supported on the identity provider
   163  		if gateClient.Config.Auth.OAuth2.CachedToken != nil {
   164  			token := gateClient.Config.Auth.OAuth2.CachedToken
   165  
   166  			// The valid method below will return true if the token is set and not expired
   167  			// So, to check if it is expired. The token has an internal method to do this, and it is done
   168  			// as a part of the "Valid" method. So just use that, but ensure we are only checking if there
   169  			// is indeed an access token set
   170  			if token.AccessToken != "" && !token.Valid() && token.RefreshToken == "" {
   171  				gateClient.Config.Auth.OAuth2.CachedToken = nil
   172  			}
   173  		}
   174  
   175  		updatedConfig, err = authenticateOAuth2(ui.Output, httpClient, gateClient.GateEndpoint(), gateClient.Config.Auth)
   176  		if err != nil {
   177  			ui.Error(fmt.Sprintf("OAuth2 Authentication failed: %v", err))
   178  			return nil, unwrapErr(ui, err)
   179  		}
   180  
   181  		updatedMessage = "Caching oauth2 token."
   182  	}
   183  
   184  	if gateClient.Config.Auth != nil && gateClient.Config.Auth.GoogleServiceAccount != nil {
   185  		updatedConfig, err = authenticateGoogleServiceAccount(httpClient, gateClient.GateEndpoint(), gateClient.Config.Auth)
   186  		if err != nil {
   187  			ui.Error(fmt.Sprintf("Google service account authentication failed: %v", err))
   188  			return nil, unwrapErr(ui, err)
   189  		}
   190  		updatedMessage = "Caching gsa token."
   191  	}
   192  
   193  	if updatedConfig {
   194  		ui.Info(updatedMessage)
   195  		_ = gateClient.writeYAMLConfig()
   196  	}
   197  
   198  	if gateClient.Config.Auth != nil && gateClient.Config.Auth.Ldap != nil {
   199  		if err = authenticateLdap(ui.Output, httpClient, gateClient.GateEndpoint(), gateClient.Config.Auth); err != nil {
   200  			ui.Error(fmt.Sprintf("LDAP Authentication failed: %v", err))
   201  			return nil, unwrapErr(ui, err)
   202  		}
   203  	}
   204  
   205  	m := make(map[string]string)
   206  
   207  	if defaultHeaders != "" {
   208  		headers := strings.Split(defaultHeaders, ",")
   209  		for _, element := range headers {
   210  			header := strings.SplitN(element, "=", 2)
   211  			if len(header) != 2 {
   212  				return nil, fmt.Errorf("Bad default-header value, use key=value form: %s", element)
   213  			}
   214  			m[strings.TrimSpace(header[0])] = strings.TrimSpace(header[1])
   215  		}
   216  	}
   217  
   218  	cfg := &gate.Configuration{
   219  		BasePath:      gateClient.GateEndpoint(),
   220  		DefaultHeader: m,
   221  		UserAgent:     fmt.Sprintf("%s/%s", version.UserAgent, version.String()),
   222  		HTTPClient:    httpClient,
   223  	}
   224  	gateClient.APIClient = gate.NewAPIClient(cfg)
   225  
   226  	// TODO: Verify version compatibility between Spin CLI and Gate.
   227  	_, _, err = gateClient.VersionControllerApi.GetVersionUsingGET(gateClient.Context)
   228  	if err != nil {
   229  		ui.Error("Could not reach Gate, please ensure it is running. Failing.")
   230  		return nil, err
   231  	}
   232  
   233  	return gateClient, nil
   234  }
   235  
   236  // unwrapErr will convert any errors made with `errors.Wrap` into ui.Error calls
   237  // and return the wrapped error. This allows for some error handling inside
   238  // functions that do not have access to a `ui` object.
   239  func unwrapErr(ui output.Ui, err error) error {
   240  	if e := errors.Unwrap(err); e != nil {
   241  		ui.Error(e.Error())
   242  		return e
   243  	}
   244  	return err
   245  }
   246  
   247  func userConfig(gateClient *GatewayClient, configLocation string) error {
   248  	if configLocation != "" {
   249  		gateClient.configLocation = configLocation
   250  	} else {
   251  		userHome, err := os.UserHomeDir()
   252  		if err != nil {
   253  			gateClient.ui.Error("Could not read current user home directory from environment, failing.")
   254  			return err
   255  		}
   256  		gateClient.configLocation = filepath.Join(userHome, ".spin", "config")
   257  	}
   258  
   259  	yamlFile, err := ioutil.ReadFile(gateClient.configLocation)
   260  	// Please note that https://github.com/spinnaker/spin/pull/243 introduced better coding standards and
   261  	// as a result, your auth config needs to match the struct tags through all the config structs
   262  	// e.g. the struct tags for oauth in the config are set in the local oauth package here
   263  	// but unmarshal to an upstream oauth package, so the cached token needs to match
   264  	// https://godoc.org/golang.org/x/oauth2#Token
   265  	if yamlFile != nil {
   266  		err = yaml.UnmarshalStrict([]byte(os.ExpandEnv(string(yamlFile))), &gateClient.Config)
   267  		if err != nil {
   268  			gateClient.ui.Error(fmt.Sprintf("Could not deserialize config file with contents: %s, failing.", yamlFile))
   269  			return err
   270  		}
   271  	} else {
   272  		gateClient.Config = config.Config{}
   273  	}
   274  	return nil
   275  }
   276  
   277  // InitializeHTTPClient will return an *http.Client configured with
   278  // optional TLS keys as specified in the auth.Config
   279  func InitializeHTTPClient(auth *auth.Config) (*http.Client, error) {
   280  	cookieJar, _ := cookiejar.New(nil)
   281  	client := http.Client{
   282  		Jar:       cookieJar,
   283  		Transport: http.DefaultTransport.(*http.Transport).Clone(),
   284  	}
   285  
   286  	if auth == nil || !auth.Enabled || auth.X509 == nil {
   287  		return &client, nil
   288  	}
   289  
   290  	X509 := auth.X509
   291  	client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
   292  		InsecureSkipVerify: auth.IgnoreCertErrors,
   293  	}
   294  
   295  	if !X509.IsValid() {
   296  		// Misconfigured.
   297  		return nil, errors.New("Incorrect x509 auth configuration.\nMust specify certPath/keyPath or cert/key pair.")
   298  	}
   299  
   300  	if X509.CertPath != "" && X509.KeyPath != "" {
   301  		certPath, err := util.ExpandHomeDir(X509.CertPath)
   302  		if err != nil {
   303  			return nil, err
   304  		}
   305  		keyPath, err := util.ExpandHomeDir(X509.KeyPath)
   306  		if err != nil {
   307  			return nil, err
   308  		}
   309  
   310  		cert, err := tls.LoadX509KeyPair(certPath, keyPath)
   311  		if err != nil {
   312  			return nil, err
   313  		}
   314  
   315  		clientCA, err := ioutil.ReadFile(certPath)
   316  		if err != nil {
   317  			return nil, err
   318  		}
   319  
   320  		return initializeX509Config(client, clientCA, cert), nil
   321  	}
   322  
   323  	if X509.Cert != "" && X509.Key != "" {
   324  		certBytes := []byte(X509.Cert)
   325  		keyBytes := []byte(X509.Key)
   326  		cert, err := tls.X509KeyPair(certBytes, keyBytes)
   327  		if err != nil {
   328  			return nil, err
   329  		}
   330  
   331  		return initializeX509Config(client, certBytes, cert), nil
   332  	}
   333  
   334  	// Misconfigured.
   335  	return nil, errors.New("Incorrect x509 auth configuration.\nMust specify certPath/keyPath or cert/key pair.")
   336  }
   337  
   338  // Authenticate is helper function to attempt to authenticate with OAuth2,
   339  // Google Service Account or LDAP as configured in the auth.Config.
   340  func Authenticate(output func(string), httpClient *http.Client, endpoint string, auth *auth.Config) (updatedConfig bool, err error) {
   341  	updatedConfig, err = authenticateOAuth2(output, httpClient, endpoint, auth)
   342  	if updatedConfig || err != nil {
   343  		return updatedConfig, err
   344  	}
   345  
   346  	updatedConfig, err = authenticateGoogleServiceAccount(httpClient, endpoint, auth)
   347  	if updatedConfig || err != nil {
   348  		return updatedConfig, err
   349  	}
   350  
   351  	if err = authenticateLdap(output, httpClient, endpoint, auth); err != nil {
   352  		return false, err
   353  	}
   354  	return false, nil
   355  }
   356  
   357  // ContextWithAuth will set context variables that maybe necessary for IAP or Basic
   358  // authentication per-request.  This can be used in conjunction with AddAuthHeaders
   359  // to ensure auth headers from the context are added to all requests.
   360  func ContextWithAuth(ctx context.Context, auth *auth.Config) (context.Context, error) {
   361  	if auth != nil && auth.Enabled && auth.Iap != nil {
   362  		accessToken, err := authenticateIAP(auth)
   363  		ctx = context.WithValue(ctx, gate.ContextAccessToken, accessToken)
   364  		return ctx, err
   365  	} else if auth != nil && auth.Enabled && auth.Basic != nil {
   366  		if !auth.Basic.IsValid() {
   367  			return nil, errors.New("Incorrect Basic auth configuration. Must include username and password.")
   368  		}
   369  		ctx = context.WithValue(ctx, gate.ContextBasicAuth, gate.BasicAuth{
   370  			UserName: auth.Basic.Username,
   371  			Password: auth.Basic.Password,
   372  		})
   373  		return ctx, nil
   374  	}
   375  	return ctx, nil
   376  }
   377  
   378  // AddAuthHeaders will use the context variables to set via ContextWithAuth
   379  // to add any necessary authentication headers to the request.
   380  func AddAuthHeaders(ctx context.Context, req *http.Request) error {
   381  	if ctx != nil {
   382  		return nil
   383  	}
   384  
   385  	// add context to the request
   386  	req = req.WithContext(ctx)
   387  
   388  	// Walk through any authentication.
   389  
   390  	// OAuth2 authentication
   391  	if tok, ok := ctx.Value(gate.ContextOAuth2).(oauth2.TokenSource); ok {
   392  		// We were able to grab an oauth2 token from the context
   393  		latestToken, err := tok.Token()
   394  		if err != nil {
   395  			return err
   396  		}
   397  		latestToken.SetAuthHeader(req)
   398  	}
   399  
   400  	// Basic HTTP Authentication
   401  	if auth, ok := ctx.Value(gate.ContextBasicAuth).(gate.BasicAuth); ok {
   402  		req.SetBasicAuth(auth.UserName, auth.Password)
   403  	}
   404  
   405  	// AccessToken Authentication
   406  	if auth, ok := ctx.Value(gate.ContextAccessToken).(string); ok {
   407  		req.Header.Add("Authorization", "Bearer "+auth)
   408  	}
   409  	return nil
   410  }
   411  
   412  func initializeX509Config(client http.Client, clientCA []byte, cert tls.Certificate) *http.Client {
   413  	clientCertPool := x509.NewCertPool()
   414  	clientCertPool.AppendCertsFromPEM(clientCA)
   415  
   416  	client.Transport.(*http.Transport).TLSClientConfig.MinVersion = tls.VersionTLS12
   417  	client.Transport.(*http.Transport).TLSClientConfig.PreferServerCipherSuites = true
   418  	client.Transport.(*http.Transport).TLSClientConfig.Certificates = []tls.Certificate{cert}
   419  	return &client
   420  }
   421  
   422  func authenticateOAuth2(output func(string), httpClient *http.Client, endpoint string, auth *auth.Config) (configUpdated bool, err error) {
   423  	if auth != nil && auth.Enabled && auth.OAuth2 != nil {
   424  		OAuth2 := auth.OAuth2
   425  		if !OAuth2.IsValid() {
   426  			// TODO(jacobkiefer): Improve this error message.
   427  			return false, errors.New("incorrect OAuth2 auth configuration")
   428  		}
   429  
   430  		config := &oauth2.Config{
   431  			ClientID:     OAuth2.ClientId,
   432  			ClientSecret: OAuth2.ClientSecret,
   433  			RedirectURL:  "http://localhost:8085",
   434  			Scopes:       OAuth2.Scopes,
   435  			Endpoint: oauth2.Endpoint{
   436  				AuthURL:  OAuth2.AuthUrl,
   437  				TokenURL: OAuth2.TokenUrl,
   438  			},
   439  		}
   440  		var newToken *oauth2.Token
   441  
   442  		if auth.OAuth2.CachedToken != nil {
   443  			// Look up cached credentials to save oauth2 roundtrip.
   444  			token := auth.OAuth2.CachedToken
   445  			tokenSource := config.TokenSource(context.Background(), token)
   446  			newToken, err = tokenSource.Token()
   447  			if err != nil {
   448  				return false, errors.Wrapf(err, "Could not refresh token from source: %v", tokenSource)
   449  			}
   450  		} else {
   451  			// Do roundtrip.
   452  			http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   453  				code := r.FormValue("code")
   454  				fmt.Fprintln(w, code)
   455  			}))
   456  			go http.ListenAndServe(":8085", nil)
   457  			// Note: leaving server connection open for scope of request, will be reaped on exit.
   458  
   459  			verifier, verifierCode, err := generateCodeVerifier()
   460  			if err != nil {
   461  				return false, err
   462  			}
   463  
   464  			codeVerifier := oauth2.SetAuthURLParam("code_verifier", verifier)
   465  			codeChallenge := oauth2.SetAuthURLParam("code_challenge", verifierCode)
   466  			challengeMethod := oauth2.SetAuthURLParam("code_challenge_method", "S256")
   467  
   468  			authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.ApprovalForce, challengeMethod, codeChallenge)
   469  			output(fmt.Sprintf("Navigate to %s and authenticate", authURL))
   470  			code := prompt(output, "Paste authorization code:")
   471  
   472  			newToken, err = config.Exchange(context.Background(), code, codeVerifier)
   473  			if err != nil {
   474  				return false, err
   475  			}
   476  		}
   477  		OAuth2.CachedToken = newToken
   478  		err = login(httpClient, endpoint, newToken.AccessToken)
   479  		if err != nil {
   480  			return false, err
   481  		}
   482  		return true, nil
   483  	}
   484  	return false, nil
   485  }
   486  
   487  func authenticateIAP(auth *auth.Config) (string, error) {
   488  	iapConfig := auth.Iap
   489  	token, err := iap.GetIapToken(*iapConfig)
   490  	return token, err
   491  }
   492  
   493  func authenticateGoogleServiceAccount(httpClient *http.Client, endpoint string, auth *auth.Config) (updatedConfig bool, err error) {
   494  	if auth == nil {
   495  		return false, nil
   496  	}
   497  
   498  	gsa := auth.GoogleServiceAccount
   499  	if !gsa.IsEnabled() {
   500  		return false, nil
   501  	}
   502  
   503  	if gsa.CachedToken != nil && gsa.CachedToken.Valid() {
   504  		return false, login(httpClient, endpoint, gsa.CachedToken.AccessToken)
   505  	}
   506  	gsa.CachedToken = nil
   507  
   508  	var source oauth2.TokenSource
   509  	if gsa.File == "" {
   510  		source, err = google.DefaultTokenSource(context.Background(), "profile", "email")
   511  	} else {
   512  		serviceAccountJSON, ferr := ioutil.ReadFile(gsa.File)
   513  		if ferr != nil {
   514  			return false, ferr
   515  		}
   516  		source, err = google.JWTAccessTokenSourceFromJSON(serviceAccountJSON, "https://accounts.google.com/o/oauth2/v2/auth")
   517  	}
   518  	if err != nil {
   519  		return false, err
   520  	}
   521  
   522  	token, err := source.Token()
   523  	if err != nil {
   524  		return false, err
   525  	}
   526  
   527  	if err := login(httpClient, endpoint, token.AccessToken); err != nil {
   528  		return false, err
   529  	}
   530  
   531  	gsa.CachedToken = token
   532  	return true, nil
   533  }
   534  
   535  func login(httpClient *http.Client, endpoint string, accessToken string) error {
   536  	loginReq, err := http.NewRequest("GET", endpoint+"/login", nil)
   537  	if err != nil {
   538  		return err
   539  	}
   540  	loginReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
   541  	_, err = httpClient.Do(loginReq) // Login to establish session.
   542  	if err != nil {
   543  		return errors.New(fmt.Sprintf("login failed: %s", err))
   544  	}
   545  	return nil
   546  }
   547  
   548  func authenticateLdap(output func(string), httpClient *http.Client, endpoint string, auth *auth.Config) error {
   549  	if auth != nil && auth.Enabled && auth.Ldap != nil {
   550  		if auth.Ldap.Username == "" {
   551  			auth.Ldap.Username = prompt(output, "Username:")
   552  		}
   553  
   554  		if auth.Ldap.Password == "" {
   555  			auth.Ldap.Password = securePrompt(output, "Password:")
   556  		}
   557  
   558  		if !auth.Ldap.IsValid() {
   559  			return errors.New("Incorrect LDAP auth configuration. Must include username and password.")
   560  		}
   561  
   562  		form := url.Values{}
   563  		form.Add("username", auth.Ldap.Username)
   564  		form.Add("password", auth.Ldap.Password)
   565  
   566  		loginReq, err := http.NewRequest("POST", endpoint+"/login", strings.NewReader(form.Encode()))
   567  		loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   568  		if err != nil {
   569  			return err
   570  		}
   571  
   572  		_, err = httpClient.Do(loginReq) // Login to establish session.
   573  
   574  		if err != nil {
   575  			return errors.New("ldap authentication failed")
   576  		}
   577  	}
   578  
   579  	return nil
   580  }
   581  
   582  // writeYAMLConfig writes an updated YAML configuration file to the receiver's config file location.
   583  // It returns an error, but the error may be ignored.
   584  func (m *GatewayClient) writeYAMLConfig() error {
   585  	// Write updated config file with u=rw,g=,o= permissions by default.
   586  	// The default permissions should only be used if the file no longer exists.
   587  	err := writeYAML(&m.Config, m.configLocation, defaultConfigFileMode)
   588  	if err != nil {
   589  		m.ui.Warn(fmt.Sprintf("Error caching oauth2 token: %v", err))
   590  	}
   591  	return err
   592  }
   593  
   594  func writeYAML(v interface{}, dest string, defaultMode os.FileMode) error {
   595  	// Write config with cached token
   596  	buf, err := yaml.Marshal(v)
   597  	if err != nil {
   598  		return err
   599  	}
   600  
   601  	mode := defaultMode
   602  	info, err := os.Stat(dest)
   603  	if err != nil && !os.IsNotExist(err) {
   604  		return nil
   605  	} else {
   606  		// Preserve existing file mode
   607  		mode = info.Mode()
   608  	}
   609  
   610  	return ioutil.WriteFile(dest, buf, mode)
   611  }
   612  
   613  // generateCodeVerifier generates an OAuth2 code verifier
   614  // in accordance to https://www.oauth.com/oauth2-servers/pkce/authorization-request and
   615  // https://tools.ietf.org/html/rfc7636#section-4.1.
   616  func generateCodeVerifier() (verifier string, code string, err error) {
   617  	randomBytes := make([]byte, 64)
   618  	if _, err := rand.Read(randomBytes); err != nil {
   619  		return "", "", errors.Wrap(err, "Could not generate random string for code_verifier")
   620  	}
   621  	verifier = base64.RawURLEncoding.EncodeToString(randomBytes)
   622  	verifierHash := sha256.Sum256([]byte(verifier))
   623  	code = base64.RawURLEncoding.EncodeToString(verifierHash[:]) // Slice for type conversion
   624  	return verifier, code, nil
   625  }
   626  
   627  func prompt(output func(string), inputMsg string) string {
   628  	reader := bufio.NewReader(os.Stdin)
   629  	output(inputMsg)
   630  	text, _ := reader.ReadString('\n')
   631  	return strings.TrimSpace(text)
   632  }
   633  
   634  func securePrompt(output func(string), inputMsg string) string {
   635  	output(inputMsg)
   636  	byteSecret, _ := terminal.ReadPassword(int(syscall.Stdin))
   637  	secret := string(byteSecret)
   638  	return strings.TrimSpace(secret)
   639  }