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 }