// GetAndTestEtcdClient creates an etcd client based on the provided config. It will attempt to // connect to the etcd server and block until the server responds at least once, or return an // error if the server never responded. func GetAndTestEtcdClient(etcdClientInfo configapi.EtcdConnectionInfo) (*etcdclient.Client, error) { // etcd does a poor job of setting up the transport - use the Kube client stack transport, err := client.TransportFor(&client.Config{ TLSClientConfig: client.TLSClientConfig{ CertFile: etcdClientInfo.ClientCert.CertFile, KeyFile: etcdClientInfo.ClientCert.KeyFile, CAFile: etcdClientInfo.CA, }, WrapTransport: DefaultEtcdClientTransport, }) if err != nil { return nil, err } etcdClient := etcdclient.NewClient(etcdClientInfo.URLs) etcdClient.SetTransport(transport.(*http.Transport)) for i := 0; ; i++ { _, err := etcdClient.Get("/", false, false) if err == nil || etcdstorage.IsEtcdNotFound(err) { break } if i > 100 { return nil, fmt.Errorf("could not reach etcd: %v", err) } time.Sleep(50 * time.Millisecond) } return etcdClient, nil }
// NewProxyServer creates and installs a new ProxyServer. // It automatically registers the created ProxyServer to http.DefaultServeMux. // 'filter', if non-nil, protects requests to the api only. func NewProxyServer(port int, filebase string, apiProxyPrefix string, staticPrefix string, filter *FilterServer, cfg *client.Config) (*ProxyServer, error) { host := cfg.Host if !strings.HasSuffix(host, "/") { host = host + "/" } target, err := url.Parse(host) if err != nil { return nil, err } proxy := newProxy(target) if proxy.Transport, err = client.TransportFor(cfg); err != nil { return nil, err } proxyServer := http.Handler(proxy) if filter != nil { proxyServer = filter.HandlerFor(proxyServer) } if !strings.HasPrefix(apiProxyPrefix, "/api") { proxyServer = stripLeaveSlash(apiProxyPrefix, proxyServer) } mux := http.NewServeMux() mux.Handle(apiProxyPrefix, proxyServer) if filebase != "" { // Require user to explicitly request this behavior rather than // serving their working directory by default. mux.Handle(staticPrefix, newFileHandler(staticPrefix, filebase)) } return &ProxyServer{handler: mux, port: port}, nil }
// NewUpgradeAwareSingleHostReverseProxy creates a new UpgradeAwareSingleHostReverseProxy. func NewUpgradeAwareSingleHostReverseProxy(clientConfig *kclient.Config, backendAddr *url.URL) (*UpgradeAwareSingleHostReverseProxy, error) { transport, err := kclient.TransportFor(clientConfig) if err != nil { return nil, err } reverseProxy := httputil.NewSingleHostReverseProxy(backendAddr) reverseProxy.FlushInterval = 200 * time.Millisecond p := &UpgradeAwareSingleHostReverseProxy{ clientConfig: clientConfig, backendAddr: backendAddr, transport: transport, reverseProxy: reverseProxy, } p.reverseProxy.Transport = p return p, nil }
// RunStartBuildWebHook tries to trigger the provided webhook. It will attempt to utilize the current client // configuration if the webhook has the same URL. func RunStartBuildWebHook(f *clientcmd.Factory, out io.Writer, webhook string, path, postReceivePath string, repo git.Repository) error { hook, err := url.Parse(webhook) if err != nil { return err } event, err := hookEventFromPostReceive(repo, path, postReceivePath) if err != nil { return err } // TODO: should be a versioned struct data, err := json.Marshal(event) if err != nil { return err } httpClient := http.DefaultClient // when using HTTPS, try to reuse the local config transport if possible to get a client cert // TODO: search all configs if hook.Scheme == "https" { config, err := f.OpenShiftClientConfig.ClientConfig() if err == nil { if url, err := client.DefaultServerURL(config.Host, "", "test", true); err == nil { if url.Host == hook.Host && url.Scheme == hook.Scheme { if rt, err := client.TransportFor(config); err == nil { httpClient = &http.Client{Transport: rt} } } } } } glog.V(4).Infof("Triggering hook %s\n%s", hook, string(data)) resp, err := httpClient.Post(hook.String(), "application/json", bytes.NewBuffer(data)) if err != nil { return err } switch { case resp.StatusCode == 301 || resp.StatusCode == 302: // TODO: follow redirect and display output case resp.StatusCode < 200 || resp.StatusCode >= 300: body, _ := ioutil.ReadAll(resp.Body) return fmt.Errorf("server rejected our request %d\nremote: %s", resp.StatusCode, string(body)) } return nil }
// EtcdClient creates an etcd client based on the provided config. func EtcdClient(etcdClientInfo configapi.EtcdConnectionInfo) (*etcdclient.Client, error) { // etcd does a poor job of setting up the transport - use the Kube client stack transport, err := client.TransportFor(&client.Config{ TLSClientConfig: client.TLSClientConfig{ CertFile: etcdClientInfo.ClientCert.CertFile, KeyFile: etcdClientInfo.ClientCert.KeyFile, CAFile: etcdClientInfo.CA, }, WrapTransport: DefaultEtcdClientTransport, }) if err != nil { return nil, err } etcdClient := etcdclient.NewClient(etcdClientInfo.URLs) etcdClient.SetTransport(transport.(*http.Transport)) return etcdClient, nil }
// etcdClient creates an etcd client based on the provided config. func etcdClient(cfg *Config) (*etcd.Client, error) { // etcd does a poor job of setting up the transport - use the Kube client stack transport, err := client.TransportFor(&client.Config{ TLSClientConfig: client.TLSClientConfig{ CertFile: cfg.CertFile, KeyFile: cfg.KeyFile, CAFile: cfg.CAFile, }, WrapTransport: defaultEtcdClientTransport, }) if err != nil { return nil, err } etcdClient := etcd.NewClient([]string{cfg.EtcdAddr.URL.String()}) etcdClient.SetTransport(transport.(*http.Transport)) return etcdClient, nil }
// RequestToken uses the cmd arguments to locate an openshift oauth server and attempts to authenticate // it returns the access token if it gets one. An error if it does not func RequestToken(clientCfg *kclient.Config, reader io.Reader, defaultUsername string, defaultPassword string) (string, error) { tokenGetter := &tokenGetterInfo{} osClient, err := client.New(clientCfg) if err != nil { return "", err } // get the transport, so that we can use it to build our own client that wraps it // our client understands certain challenges and can respond to them clientTransport, err := kclient.TransportFor(clientCfg) if err != nil { return "", err } httpClient := &http.Client{ Transport: clientTransport, CheckRedirect: tokenGetter.checkRedirect, } osClient.Client = &challengingClient{httpClient, reader, defaultUsername, defaultPassword} result := osClient.Get().AbsPath("/oauth", osinserver.AuthorizePath). Param("response_type", "token"). Param("client_id", "openshift-challenging-client"). Do() if err := result.Error(); err != nil && !isRedirectError(err) { return "", err } if len(tokenGetter.accessToken) == 0 { r, _ := result.Raw() if description, ok := rawOAuthJSONErrorDescription(r); ok { return "", fmt.Errorf("cannot retrieve a token: %s", description) } glog.V(4).Infof("A request token could not be created, server returned: %s", string(r)) return "", fmt.Errorf("the server did not return a token (possible server error)") } return tokenGetter.accessToken, nil }
func TestOAuthRequestHeader(t *testing.T) { // Write cert we're going to use to verify OAuth requestheader requests caFile, err := ioutil.TempFile("", "test.crt") if err != nil { t.Fatalf("unexpected error: %v", err) } defer os.Remove(caFile.Name()) if err := ioutil.WriteFile(caFile.Name(), rootCACert, os.FileMode(0600)); err != nil { t.Fatalf("unexpected error: %v", err) } masterOptions, err := testutil.DefaultMasterOptions() if err != nil { t.Fatalf("unexpected error: %v", err) } masterOptions.OAuthConfig.IdentityProviders[0] = configapi.IdentityProvider{ Name: "requestheader", UseAsChallenger: false, UseAsLogin: false, Provider: runtime.EmbeddedObject{ &configapi.RequestHeaderIdentityProvider{ ClientCA: caFile.Name(), Headers: []string{"My-Remote-User", "SSO-User"}, }, }, } // Start server clusterAdminKubeConfig, err := testutil.StartConfiguredMaster(masterOptions) if err != nil { t.Fatalf("unexpected error: %v", err) } clientConfig, err := testutil.GetClusterAdminClientConfig(clusterAdminKubeConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } // Use the server and CA info, but no client cert info anonConfig := kclient.Config{} anonConfig.Host = clientConfig.Host anonConfig.CAFile = clientConfig.CAFile anonConfig.CAData = clientConfig.CAData // Build the authorize request with the My-Remote-User header authorizeURL := clientConfig.Host + "/oauth/authorize?client_id=openshift-challenging-client&response_type=token" req, err := http.NewRequest("GET", authorizeURL, nil) req.Header.Set("My-Remote-User", "myuser") // Make the request without cert auth transport, err := kclient.TransportFor(&anonConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } resp, err := transport.RoundTrip(req) if err != nil { t.Fatalf("unexpected error: %v", err) } redirect, err := resp.Location() if err != nil { t.Fatalf("expected 302 redirect, got error: %v", err) } if redirect.Query().Get("error") == "" { t.Fatalf("expected unsuccessful token request, got redirected to %v", redirect.String()) } // Use the server and CA info, with cert info authProxyConfig := anonConfig authProxyConfig.CertData = clientCert authProxyConfig.KeyData = clientKey // Make the request with cert info transport, err = kclient.TransportFor(&authProxyConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } resp, err = transport.RoundTrip(req) if err != nil { t.Fatalf("unexpected error: %v", err) } redirect, err = resp.Location() if err != nil { t.Fatalf("expected 302 redirect, got error: %v", err) } if redirect.Query().Get("error") != "" { t.Fatalf("expected successful token request, got error %v", redirect.String()) } // Extract the access_token // group #0 is everything. #1 #2 #3 accessTokenRedirectRegex := regexp.MustCompile(`(^|&)access_token=([^&]+)($|&)`) accessToken := "" if matches := accessTokenRedirectRegex.FindStringSubmatch(redirect.Fragment); matches != nil { accessToken = matches[2] } if accessToken == "" { t.Fatalf("Expected access token, got %s", redirect.String()) } // Make sure we can use the token, and it represents who we expect userConfig := anonConfig userConfig.BearerToken = accessToken userClient, err := client.New(&userConfig) if err != nil { t.Fatalf("Unexpected error: %v", err) } user, err := userClient.Users().Get("~") if err != nil { t.Fatalf("Unexpected error: %v", err) } if user.Name != "myuser" { t.Fatalf("Expected myuser as the user, got %v", user) } }
// RequestToken uses the cmd arguments to locate an openshift oauth server and attempts to authenticate // it returns the access token if it gets one. An error if it does not func RequestToken(clientCfg *kclient.Config, reader io.Reader, defaultUsername string, defaultPassword string) (string, error) { challengeHandler := &BasicChallengeHandler{ Host: clientCfg.Host, Reader: reader, Username: defaultUsername, Password: defaultPassword, } rt, err := kclient.TransportFor(clientCfg) if err != nil { return "", err } // requestURL holds the current URL to make requests to. This can change if the server responds with a redirect requestURL := clientCfg.Host + "/oauth/authorize?response_type=token&client_id=openshift-challenging-client" // requestHeaders holds additional headers to add to the request. This can be changed by challengeHandlers requestHeaders := http.Header{} // requestedURLSet/requestedURLList hold the URLs we have requested, to prevent redirect loops. Gets reset when a challenge is handled. requestedURLSet := util.NewStringSet() requestedURLList := []string{} for { // Make the request resp, err := request(rt, requestURL, requestHeaders) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode == http.StatusUnauthorized { if resp.Header.Get("WWW-Authenticate") != "" { if !challengeHandler.CanHandle(resp.Header) { return "", apierrs.NewUnauthorized("unhandled challenge") } // Handle a challenge newRequestHeaders, shouldRetry, err := challengeHandler.HandleChallenge(resp.Header) if err != nil { return "", err } if !shouldRetry { return "", apierrs.NewUnauthorized("challenger chose not to retry the request") } // Reset request set/list. Since we're setting different headers, it is legitimate to request the same urls requestedURLSet = util.NewStringSet() requestedURLList = []string{} // Use the response to the challenge as the new headers requestHeaders = newRequestHeaders continue } // Unauthorized with no challenge unauthorizedError := apierrs.NewUnauthorized("") // Attempt to read body content and include as an error detail if details, err := ioutil.ReadAll(resp.Body); err == nil && len(details) > 0 { unauthorizedError.(*apierrs.StatusError).ErrStatus.Details = &api.StatusDetails{ Causes: []api.StatusCause{ {Message: string(details)}, }, } } return "", unauthorizedError } if resp.StatusCode == http.StatusFound { redirectURL := resp.Header.Get("Location") // OAuth response case (access_token or error parameter) accessToken, err := oauthAuthorizeResult(redirectURL) if err != nil { return "", err } if len(accessToken) > 0 { return accessToken, err } // Non-OAuth response, just follow the URL // add to our list of redirects requestedURLList = append(requestedURLList, redirectURL) // detect loops if !requestedURLSet.Has(redirectURL) { requestedURLSet.Insert(redirectURL) requestURL = redirectURL continue } return "", apierrs.NewInternalError(fmt.Errorf("redirect loop: %s", strings.Join(requestedURLList, " -> "))) } // Unknown response return "", apierrs.NewInternalError(fmt.Errorf("unexpected response: %d", resp.StatusCode)) } }
// TestOAuthRequestHeader checks the following scenarios: // * request containing remote user header is ignored if it doesn't have client cert auth // * request containing remote user header is honored if it has client cert auth // * unauthenticated requests are redirected to an auth proxy // * login command succeeds against a request-header identity provider via redirection to an auth proxy func TestOAuthRequestHeader(t *testing.T) { // Test data used by auth proxy users := map[string]string{ "myusername": "******", } // Write cert we're going to use to verify OAuth requestheader requests caFile, err := ioutil.TempFile("", "test.crt") if err != nil { t.Fatalf("unexpected error: %v", err) } defer os.Remove(caFile.Name()) if err := ioutil.WriteFile(caFile.Name(), rootCACert, os.FileMode(0600)); err != nil { t.Fatalf("unexpected error: %v", err) } // Get master config masterOptions, err := testserver.DefaultMasterOptions() if err != nil { t.Fatalf("unexpected error: %v", err) } masterURL, _ := url.Parse(masterOptions.OAuthConfig.MasterPublicURL) // Set up an auth proxy var proxyTransport http.RoundTripper proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Decide whether to challenge username, password, hasBasicAuth := r.BasicAuth() if correctPassword, hasUser := users[username]; !hasBasicAuth || !hasUser || password != correctPassword { w.Header().Set("WWW-Authenticate", "Basic realm=Protected Area") w.WriteHeader(401) return } // Swap the scheme and host to the master, keeping path and params the same proxyURL := r.URL proxyURL.Scheme = masterURL.Scheme proxyURL.Host = masterURL.Host // Build a request, copying the original method, body, and headers, overriding the remote user headers proxyRequest, _ := http.NewRequest(r.Method, proxyURL.String(), r.Body) proxyRequest.Header = r.Header proxyRequest.Header.Set("My-Remote-User", username) proxyRequest.Header.Set("SSO-User", "") // Round trip to the back end response, err := proxyTransport.RoundTrip(r) if err != nil { t.Fatalf("Unexpected error: %v", err) } defer response.Body.Close() // Copy response back to originator for k, v := range response.Header { w.Header()[k] = v } w.WriteHeader(response.StatusCode) if _, err := io.Copy(w, response.Body); err != nil { t.Fatalf("Unexpected error: %v", err) } })) defer proxyServer.Close() masterOptions.OAuthConfig.IdentityProviders[0] = configapi.IdentityProvider{ Name: "requestheader", UseAsChallenger: true, UseAsLogin: true, Provider: runtime.EmbeddedObject{ Object: &configapi.RequestHeaderIdentityProvider{ ChallengeURL: proxyServer.URL + "/oauth/authorize?${query}", LoginURL: "http://www.example.com/login?then=${url}", ClientCA: caFile.Name(), Headers: []string{"My-Remote-User", "SSO-User"}, }, }, } // Start server clusterAdminKubeConfig, err := testserver.StartConfiguredMaster(masterOptions) if err != nil { t.Fatalf("unexpected error: %v", err) } clientConfig, err := testutil.GetClusterAdminClientConfig(clusterAdminKubeConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } // Use the server and CA info, but no client cert info anonConfig := kclient.Config{} anonConfig.Host = clientConfig.Host anonConfig.CAFile = clientConfig.CAFile anonConfig.CAData = clientConfig.CAData anonTransport, err := kclient.TransportFor(&anonConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } // Use the server and CA info, with cert info proxyConfig := anonConfig proxyConfig.CertData = clientCert proxyConfig.KeyData = clientKey proxyTransport, err = kclient.TransportFor(&proxyConfig) if err != nil { t.Fatalf("unexpected error: %v", err) } // Build the authorize request, spoofing a remote user header authorizeURL := clientConfig.Host + "/oauth/authorize?client_id=openshift-challenging-client&response_type=token" req, err := http.NewRequest("GET", authorizeURL, nil) req.Header.Set("My-Remote-User", "myuser") // Make the request without cert auth resp, err := anonTransport.RoundTrip(req) if err != nil { t.Fatalf("unexpected error: %v", err) } proxyRedirect, err := resp.Location() if err != nil { t.Fatalf("expected spoofed remote user header to get 302 redirect, got error: %v", err) } if proxyRedirect.String() != proxyServer.URL+"/oauth/authorize?client_id=openshift-challenging-client&response_type=token" { t.Fatalf("expected redirect to proxy endpoint, got redirected to %v", proxyRedirect.String()) } // Request the redirected URL, which should cause the proxy to make the same request with cert auth req, err = http.NewRequest("GET", proxyRedirect.String(), nil) req.Header.Set("My-Remote-User", "myuser") req.SetBasicAuth("myusername", "mypassword") resp, err = proxyTransport.RoundTrip(req) if err != nil { t.Fatalf("unexpected error: %v", err) } tokenRedirect, err := resp.Location() if err != nil { t.Fatalf("expected 302 redirect, got error: %v", err) } if tokenRedirect.Query().Get("error") != "" { t.Fatalf("expected successful token request, got error %v", tokenRedirect.String()) } // Extract the access_token // group #0 is everything. #1 #2 #3 accessTokenRedirectRegex := regexp.MustCompile(`(^|&)access_token=([^&]+)($|&)`) accessToken := "" if matches := accessTokenRedirectRegex.FindStringSubmatch(tokenRedirect.Fragment); matches != nil { accessToken = matches[2] } if accessToken == "" { t.Fatalf("Expected access token, got %s", tokenRedirect.String()) } // Make sure we can use the token, and it represents who we expect userConfig := anonConfig userConfig.BearerToken = accessToken userClient, err := client.New(&userConfig) if err != nil { t.Fatalf("Unexpected error: %v", err) } user, err := userClient.Users().Get("~") if err != nil { t.Fatalf("Unexpected error: %v", err) } if user.Name != "myusername" { t.Fatalf("Expected myusername as the user, got %v", user) } // Get the master CA data for the login command masterCAFile := userConfig.CAFile if masterCAFile == "" { // Write master ca data tmpFile, err := ioutil.TempFile("", "ca.crt") if err != nil { t.Fatalf("unexpected error: %v", err) } defer os.Remove(tmpFile.Name()) if err := ioutil.WriteFile(tmpFile.Name(), userConfig.CAData, os.FileMode(0600)); err != nil { t.Fatalf("unexpected error: %v", err) } masterCAFile = tmpFile.Name() } // Attempt a login using a redirecting auth proxy loginOutput := &bytes.Buffer{} loginOptions := &cmd.LoginOptions{ Server: anonConfig.Host, CAFile: masterCAFile, StartingKubeConfig: &clientcmdapi.Config{}, Reader: bytes.NewBufferString("myusername\nmypassword\n"), Out: loginOutput, } if err := loginOptions.GatherInfo(); err != nil { t.Fatalf("Error trying to determine server info: %v\n%v", err, loginOutput.String()) } if loginOptions.Username != "myusername" { t.Fatalf("Unexpected user after authentication: %#v", loginOptions) } if len(loginOptions.Config.BearerToken) == 0 { t.Fatalf("Expected token after authentication: %#v", loginOptions.Config) } }