github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/ovirt/credentials.go (about)

     1  package ovirt
     2  
     3  import (
     4  	"crypto/tls"
     5  	"crypto/x509"
     6  	"encoding/pem"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"os"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/AlecAivazis/survey/v2"
    15  	"github.com/pkg/errors"
    16  	"github.com/sirupsen/logrus"
    17  )
    18  
    19  var errHTTPNotFound = errors.New("http response 404")
    20  
    21  // readFile reads a file provided in the args and return
    22  // the content or in case of failure return an error
    23  func readFile(pathFile string) ([]byte, error) {
    24  	content, err := os.ReadFile(pathFile)
    25  	if err != nil {
    26  		return content, errors.Wrapf(err, "failed to read file: %s", pathFile)
    27  	}
    28  	return content, nil
    29  }
    30  
    31  // Add PEM into the System Pool
    32  func (c *clientHTTP) addTrustBundle(pemContent string, engineConfig *Config) error {
    33  	c.certPool, _ = x509.SystemCertPool()
    34  	if c.certPool == nil {
    35  		logrus.Debug("failed to load cert pool.... Creating new cert pool")
    36  		c.certPool = x509.NewCertPool()
    37  	}
    38  
    39  	if len(pemContent) != 0 {
    40  		if !c.certPool.AppendCertsFromPEM([]byte(pemContent)) {
    41  			return errors.New("unable to load certificate")
    42  		}
    43  		logrus.Debugf("loaded %s into the system pool: ", engineConfig.CAFile)
    44  		engineConfig.CABundle = strings.TrimSpace(string(pemContent))
    45  	}
    46  	return nil
    47  }
    48  
    49  // downloadFile from specificed URL and store via filepath
    50  // Return error in case of failure
    51  func (c *clientHTTP) downloadFile() error {
    52  	tr := &http.Transport{
    53  		TLSClientConfig: &tls.Config{
    54  			InsecureSkipVerify: c.skipVerify,
    55  			RootCAs:            c.certPool,
    56  		},
    57  	}
    58  
    59  	if c.saveFilePath == "" {
    60  		return errors.New("saveFilePath must be specified")
    61  	}
    62  
    63  	client := &http.Client{Transport: tr}
    64  	resp, err := client.Get(c.urlAddr)
    65  
    66  	switch resp.StatusCode {
    67  	case http.StatusNotFound:
    68  		return fmt.Errorf("%s: %w", c.urlAddr, errHTTPNotFound)
    69  	}
    70  
    71  	if err != nil {
    72  		return err
    73  	}
    74  	defer resp.Body.Close()
    75  
    76  	out, err := os.Create(c.saveFilePath)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	defer out.Close()
    81  
    82  	_, err = io.Copy(out, resp.Body)
    83  	return err
    84  }
    85  
    86  // checkURLResponse performs a GET on the provided urlAddr to ensure that
    87  // the url actually exists. Users can set skipVerify as true or false to
    88  // avoid cert validation. In case of failure, returns error.
    89  func (c *clientHTTP) checkURLResponse() error {
    90  
    91  	logrus.Debugf("checking URL response... urlAddr: %s skipVerify: %s", c.urlAddr, strconv.FormatBool(c.skipVerify))
    92  
    93  	tr := &http.Transport{
    94  		TLSClientConfig: &tls.Config{
    95  			InsecureSkipVerify: c.skipVerify,
    96  			RootCAs:            c.certPool,
    97  		},
    98  	}
    99  
   100  	client := &http.Client{Transport: tr}
   101  	resp, err := client.Get(c.urlAddr)
   102  	if err != nil {
   103  		return errors.Wrapf(err, "error checking URL response")
   104  	}
   105  	defer resp.Body.Close()
   106  
   107  	return nil
   108  }
   109  
   110  // askPassword will ask the password to connect to the Engine API.
   111  // The password provided will be added in the Config struct.
   112  // If an error happens, it will ask again username for users.
   113  func askPassword(c *Config) error {
   114  	err := survey.Ask([]*survey.Question{
   115  		{
   116  			Prompt: &survey.Password{
   117  				Message: "Engine password",
   118  				Help:    "Password for the chosen username, Press Ctrl+C to change username",
   119  			},
   120  			Validate: survey.ComposeValidators(survey.Required, authenticated(c)),
   121  		},
   122  	}, &c.Password)
   123  
   124  	if err != nil {
   125  		return err
   126  	}
   127  	return nil
   128  }
   129  
   130  // askUsername will ask username to connect to the Engine API.
   131  // The username provided will be added in the Config struct.
   132  // Returns Config and error if failure.
   133  func askUsername(c *Config) error {
   134  	err := survey.Ask([]*survey.Question{
   135  		{
   136  			Prompt: &survey.Input{
   137  				Message: "Engine username",
   138  				Help:    "The username to connect to the Engine API",
   139  				Default: "admin@internal",
   140  			},
   141  			Validate: survey.ComposeValidators(survey.Required),
   142  		},
   143  	}, &c.Username)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	return nil
   149  }
   150  
   151  // askQuestionTrueOrFalse generic function to ask question to users which
   152  // requires true (Yes) or false (No) as answer
   153  func askQuestionTrueOrFalse(question string, helpMessage string) (bool, error) {
   154  	value := false
   155  	err := survey.AskOne(
   156  		&survey.Confirm{
   157  			Message: question,
   158  			Help:    helpMessage,
   159  		},
   160  		&value,
   161  		survey.WithValidator(survey.Required),
   162  	)
   163  	if err != nil {
   164  		return value, err
   165  	}
   166  
   167  	return value, nil
   168  }
   169  
   170  // askCredentials will handle username and password for connecting with Engine
   171  func askCredentials(c Config) (Config, error) {
   172  	loginAttempts := 3
   173  	logrus.Debugf("login attempts available: %d", loginAttempts)
   174  	for loginAttempts > 0 {
   175  		err := askUsername(&c)
   176  		if err != nil {
   177  			return c, err
   178  		}
   179  
   180  		err = askPassword(&c)
   181  		if err != nil {
   182  			loginAttempts = loginAttempts - 1
   183  			logrus.Debugf("login attempts now: %d", loginAttempts)
   184  			if loginAttempts == 0 {
   185  				return c, err
   186  			}
   187  		} else {
   188  			break
   189  		}
   190  	}
   191  	return c, nil
   192  }
   193  
   194  // showPEM will print information about PEM file provided in param or error
   195  // if a failure happens
   196  func showPEM(pemFilePath string) error {
   197  	certpem, err := os.ReadFile(pemFilePath)
   198  	if err != nil {
   199  		return errors.Wrapf(err, "failed to read the cert: %s", pemFilePath)
   200  	}
   201  
   202  	block, _ := pem.Decode(certpem)
   203  	if block == nil {
   204  		return errors.New("failed to parse certificate PEM")
   205  	}
   206  
   207  	if block.Type != "CERTIFICATE" {
   208  		return errors.New("PEM-block should be CERTIFICATE type")
   209  	}
   210  
   211  	cert, err := x509.ParseCertificate(block.Bytes)
   212  	if err != nil {
   213  		logrus.Debugf("Failed to read the cert: %s", err)
   214  		return errors.Wrapf(err, "failed to read the cert: %s", pemFilePath)
   215  	}
   216  
   217  	logrus.Info("Loaded the following PEM file:")
   218  
   219  	logrus.Info("\tVersion: ", cert.Version)
   220  	logrus.Info("\tSignature Algorithm: ", cert.SignatureAlgorithm.String())
   221  	logrus.Info("\tSerial Number: ", cert.SerialNumber)
   222  	logrus.Info("\tIssuer: ", cert.Issuer.String())
   223  	logrus.Info("\tValidity:")
   224  	logrus.Info("\t\tNot Before: ", cert.NotBefore)
   225  	logrus.Info("\t\tNot After: ", cert.NotAfter)
   226  	logrus.Info("\tSubject: ", cert.Subject.ToRDNSequence())
   227  
   228  	return nil
   229  
   230  }
   231  
   232  // askPEMFile ask users the PEM bundle and returns the bundle string
   233  // or in case of failure returns error
   234  func askPEMFile() (string, error) {
   235  	bundlePEM := ""
   236  	err := survey.AskOne(
   237  		&survey.Multiline{
   238  			Message: "Certificate bundle",
   239  			Help:    "The certificate bundle to installer be able to communicate with oVirt API",
   240  		},
   241  		&bundlePEM,
   242  		survey.WithValidator(survey.Required),
   243  	)
   244  	if err != nil {
   245  		return bundlePEM, err
   246  	}
   247  
   248  	return bundlePEM, nil
   249  }
   250  
   251  // engineSetup will ask users: FQDN, execute validations and about
   252  // the credentials. In case of failure, returns Config and error
   253  func engineSetup() (Config, error) {
   254  	engineConfig := Config{}
   255  	httpResource := clientHTTP{}
   256  
   257  	err := survey.Ask([]*survey.Question{
   258  		{
   259  			Prompt: &survey.Input{
   260  				Message: "Engine FQDN[:PORT]",
   261  				Help:    "The Engine FQDN[:PORT] (engine.example.com:443)",
   262  			},
   263  			Validate: survey.ComposeValidators(survey.Required),
   264  		},
   265  	}, &engineConfig.FQDN)
   266  	if err != nil {
   267  		return engineConfig, err
   268  	}
   269  	logrus.Debug("engine FQDN: ", engineConfig.FQDN)
   270  
   271  	// By default, we set Insecure true
   272  	engineConfig.Insecure = true
   273  
   274  	// Set c.URL with the API endpoint
   275  	engineConfig.URL = fmt.Sprintf("https://%s/ovirt-engine/api", engineConfig.FQDN)
   276  	logrus.Debug("Engine URL: ", engineConfig.URL)
   277  
   278  	// Start creating clientHTTP struct for checking if Engine FQDN is responding
   279  	httpResource.skipVerify = true
   280  	httpResource.urlAddr = engineConfig.URL
   281  	err = httpResource.checkURLResponse()
   282  	if err != nil {
   283  		return engineConfig, err
   284  	}
   285  
   286  	// Set Engine PEM URL for Download
   287  	engineConfig.PemURL = fmt.Sprintf(
   288  		"https://%s/ovirt-engine/services/pki-resource?resource=ca-certificate&format=X509-PEM-CA",
   289  		engineConfig.FQDN)
   290  	logrus.Debug("PEM URL: ", engineConfig.PemURL)
   291  
   292  	// Create tmpFile to store the Engine PEM file
   293  	tmpFile, err := os.CreateTemp(os.TempDir(), "engine-")
   294  	if err != nil {
   295  		return engineConfig, err
   296  	}
   297  	defer os.Remove(tmpFile.Name())
   298  
   299  	// Download PEM
   300  	httpResource.saveFilePath = tmpFile.Name()
   301  	httpResource.skipVerify = true
   302  	httpResource.urlAddr = engineConfig.PemURL
   303  	err = httpResource.downloadFile()
   304  	if errors.Is(err, errHTTPNotFound) {
   305  		return engineConfig, err
   306  	}
   307  
   308  	if err != nil {
   309  		logrus.Warning("cannot download PEM file from Engine!", err)
   310  		answer, err := askQuestionTrueOrFalse(
   311  			"Would you like to continue?",
   312  			"By not using a trusted CA, insecure connections can "+
   313  				"cause man-in-the-middle attacks among many others.")
   314  		if err != nil || !answer {
   315  			return engineConfig, err
   316  		}
   317  	} else {
   318  		err = showPEM(httpResource.saveFilePath)
   319  		if err != nil {
   320  			engineConfig.Insecure = true
   321  		} else {
   322  			answer, err := askQuestionTrueOrFalse(
   323  				"Would you like to use the above certificate to connect to the Engine? ",
   324  				"Certificate to connect to the Engine. Make sure this cert CA is trusted locally.")
   325  			if err != nil {
   326  				return engineConfig, err
   327  			}
   328  			if answer {
   329  				pemFile, err := readFile(httpResource.saveFilePath)
   330  				engineConfig.CABundle = string(pemFile)
   331  				if err != nil {
   332  					return engineConfig, err
   333  				}
   334  				if len(engineConfig.CABundle) > 0 {
   335  					engineConfig.Insecure = false
   336  				}
   337  			} else {
   338  				answer, err = askQuestionTrueOrFalse(
   339  					"Would you like to import another PEM bundle?",
   340  					"You can use your own PEM bundle to connect to the Engine API")
   341  				if err != nil {
   342  					return engineConfig, err
   343  				}
   344  				if answer {
   345  					engineConfig.CABundle, _ = askPEMFile()
   346  					if len(engineConfig.CABundle) > 0 {
   347  						engineConfig.Insecure = false
   348  					}
   349  				}
   350  
   351  			}
   352  		}
   353  	}
   354  
   355  	if !engineConfig.Insecure {
   356  		err = httpResource.addTrustBundle(engineConfig.CABundle, &engineConfig)
   357  		if err != nil {
   358  			engineConfig.Insecure = true
   359  		}
   360  	}
   361  
   362  	if engineConfig.Insecure {
   363  		logrus.Error(
   364  			"****************************************************************************\n",
   365  			"* Could not configure secure communication to the oVirt engine.            *\n",
   366  			"* As of 4.7 insecure mode for oVirt is no longer supported in the          *\n",
   367  			"* installer. Please see the help article titled \"Installing OpenShift on   *\n",
   368  			"* RHV/oVirt in insecure mode\" for details how to configure insecure mode   *\n",
   369  			"* manually.                                                                *\n",
   370  			"****************************************************************************",
   371  		)
   372  		return engineConfig,
   373  			errors.New(
   374  				"cannot detect engine ca cert imported in the system",
   375  			)
   376  	}
   377  	return askCredentials(engineConfig)
   378  }