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 }