github.com/jfrog/jfrog-cli-core/v2@v2.51.0/common/commands/config.go (about) 1 package commands 2 3 import ( 4 "errors" 5 "fmt" 6 "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 7 "github.com/jfrog/jfrog-cli-core/v2/utils/config" 8 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 9 "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" 10 "github.com/jfrog/jfrog-cli-core/v2/utils/lock" 11 "github.com/jfrog/jfrog-client-go/auth" 12 "github.com/jfrog/jfrog-client-go/http/httpclient" 13 clientUtils "github.com/jfrog/jfrog-client-go/utils" 14 "github.com/jfrog/jfrog-client-go/utils/errorutils" 15 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 16 "github.com/jfrog/jfrog-client-go/utils/log" 17 "net/url" 18 "os" 19 "reflect" 20 "strconv" 21 "strings" 22 "sync" 23 ) 24 25 type ConfigAction string 26 27 const ( 28 AddOrEdit ConfigAction = "AddOrEdit" 29 Delete ConfigAction = "Delete" 30 Use ConfigAction = "Use" 31 Clear ConfigAction = "Clear" 32 ) 33 34 type AuthenticationMethod string 35 36 const ( 37 AccessToken AuthenticationMethod = "Access Token" 38 BasicAuth AuthenticationMethod = "Username and Password / API Key" 39 MTLS AuthenticationMethod = "Mutual TLS" 40 WebLogin AuthenticationMethod = "Web Login" 41 ) 42 43 // Internal golang locking for the same process. 44 var mutex sync.Mutex 45 46 type ConfigCommand struct { 47 details *config.ServerDetails 48 defaultDetails *config.ServerDetails 49 interactive bool 50 encPassword bool 51 useBasicAuthOnly bool 52 serverId string 53 // Preselected web login authentication method, supported on an interactive command only. 54 useWebLogin bool 55 // Forcibly make the configured server default. 56 makeDefault bool 57 // For unit tests 58 disablePrompts bool 59 cmdType ConfigAction 60 } 61 62 func NewConfigCommand(cmdType ConfigAction, serverId string) *ConfigCommand { 63 return &ConfigCommand{cmdType: cmdType, serverId: serverId} 64 } 65 66 func (cc *ConfigCommand) SetServerId(serverId string) *ConfigCommand { 67 cc.serverId = serverId 68 return cc 69 } 70 71 func (cc *ConfigCommand) SetEncPassword(encPassword bool) *ConfigCommand { 72 cc.encPassword = encPassword 73 return cc 74 } 75 76 func (cc *ConfigCommand) SetUseBasicAuthOnly(useBasicAuthOnly bool) *ConfigCommand { 77 cc.useBasicAuthOnly = useBasicAuthOnly 78 return cc 79 } 80 81 func (cc *ConfigCommand) SetUseWebLogin(useWebLogin bool) *ConfigCommand { 82 cc.useWebLogin = useWebLogin 83 return cc 84 } 85 86 func (cc *ConfigCommand) SetMakeDefault(makeDefault bool) *ConfigCommand { 87 cc.makeDefault = makeDefault 88 return cc 89 } 90 91 func (cc *ConfigCommand) SetInteractive(interactive bool) *ConfigCommand { 92 cc.interactive = interactive 93 return cc 94 } 95 96 func (cc *ConfigCommand) SetDefaultDetails(defaultDetails *config.ServerDetails) *ConfigCommand { 97 cc.defaultDetails = defaultDetails 98 return cc 99 } 100 101 func (cc *ConfigCommand) SetDetails(details *config.ServerDetails) *ConfigCommand { 102 cc.details = details 103 return cc 104 } 105 106 func (cc *ConfigCommand) Run() (err error) { 107 log.Debug("Locking config file to run config " + cc.cmdType + " command.") 108 mutex.Lock() 109 defer func() { 110 mutex.Unlock() 111 log.Debug("Config " + cc.cmdType + " command completed successfully. config file is released.") 112 }() 113 114 lockDirPath, err := coreutils.GetJfrogConfigLockDir() 115 if err != nil { 116 return 117 } 118 unlockFunc, err := lock.CreateLock(lockDirPath) 119 // Defer the lockFile.Unlock() function before throwing a possible error to avoid deadlock situations. 120 defer func() { 121 err = errors.Join(err, unlockFunc()) 122 }() 123 if err != nil { 124 return 125 } 126 127 switch cc.cmdType { 128 case AddOrEdit: 129 err = cc.config() 130 case Delete: 131 err = cc.delete() 132 case Use: 133 err = cc.use() 134 case Clear: 135 err = cc.clear() 136 default: 137 err = fmt.Errorf("Not supported config command type: " + string(cc.cmdType)) 138 } 139 return 140 } 141 142 func (cc *ConfigCommand) ServerDetails() (*config.ServerDetails, error) { 143 // If cc.details is not empty, then return it. 144 if cc.details != nil && !reflect.DeepEqual(config.ServerDetails{}, *cc.details) { 145 return cc.details, nil 146 } 147 // If cc.defaultDetails is not empty, then return it. 148 if cc.defaultDetails != nil && !reflect.DeepEqual(config.ServerDetails{}, *cc.defaultDetails) { 149 return cc.defaultDetails, nil 150 } 151 return nil, nil 152 } 153 154 func (cc *ConfigCommand) CommandName() string { 155 return "config" 156 } 157 158 func (cc *ConfigCommand) config() error { 159 configurations, err := cc.prepareConfigurationData() 160 if err != nil { 161 return err 162 } 163 if cc.interactive { 164 err = cc.getConfigurationFromUser() 165 } else { 166 err = cc.getConfigurationNonInteractively() 167 } 168 if err != nil { 169 return err 170 } 171 cc.addTrailingSlashes() 172 cc.lowerUsername() 173 cc.setDefaultIfNeeded(configurations) 174 if err = assertSingleAuthMethod(cc.details); err != nil { 175 return err 176 } 177 if err = cc.assertUrlsSafe(); err != nil { 178 return err 179 } 180 if err = cc.encPasswordIfNeeded(); err != nil { 181 return err 182 } 183 cc.configRefreshableTokenIfPossible() 184 return config.SaveServersConf(configurations) 185 } 186 187 func (cc *ConfigCommand) getConfigurationNonInteractively() error { 188 if cc.details.Url != "" { 189 if fileutils.IsSshUrl(cc.details.Url) { 190 coreutils.SetIfEmpty(&cc.details.ArtifactoryUrl, cc.details.Url) 191 } else { 192 cc.details.Url = clientUtils.AddTrailingSlashIfNeeded(cc.details.Url) 193 // Derive JFrog services URLs from platform URL 194 coreutils.SetIfEmpty(&cc.details.ArtifactoryUrl, cc.details.Url+"artifactory/") 195 coreutils.SetIfEmpty(&cc.details.DistributionUrl, cc.details.Url+"distribution/") 196 coreutils.SetIfEmpty(&cc.details.XrayUrl, cc.details.Url+"xray/") 197 coreutils.SetIfEmpty(&cc.details.MissionControlUrl, cc.details.Url+"mc/") 198 coreutils.SetIfEmpty(&cc.details.PipelinesUrl, cc.details.Url+"pipelines/") 199 } 200 } 201 202 if cc.details.AccessToken != "" && cc.details.User == "" { 203 if err := cc.validateTokenIsNotApiKey(); err != nil { 204 return err 205 } 206 cc.tryExtractingUsernameFromAccessToken() 207 } 208 return nil 209 } 210 211 func (cc *ConfigCommand) addTrailingSlashes() { 212 cc.details.ArtifactoryUrl = clientUtils.AddTrailingSlashIfNeeded(cc.details.ArtifactoryUrl) 213 cc.details.DistributionUrl = clientUtils.AddTrailingSlashIfNeeded(cc.details.DistributionUrl) 214 cc.details.XrayUrl = clientUtils.AddTrailingSlashIfNeeded(cc.details.XrayUrl) 215 cc.details.MissionControlUrl = clientUtils.AddTrailingSlashIfNeeded(cc.details.MissionControlUrl) 216 cc.details.PipelinesUrl = clientUtils.AddTrailingSlashIfNeeded(cc.details.PipelinesUrl) 217 } 218 219 // Artifactory expects the username to be lower-cased. In case it is not, 220 // Artifactory will silently save it lower-cased, but the token creation 221 // REST API will fail with a non-lower-cased username. 222 func (cc *ConfigCommand) lowerUsername() { 223 cc.details.User = strings.ToLower(cc.details.User) 224 } 225 226 func (cc *ConfigCommand) setDefaultIfNeeded(configurations []*config.ServerDetails) { 227 if len(configurations) == 1 { 228 cc.details.IsDefault = true 229 return 230 } 231 if cc.makeDefault { 232 for i := range configurations { 233 configurations[i].IsDefault = false 234 } 235 cc.details.IsDefault = true 236 } 237 } 238 239 func (cc *ConfigCommand) encPasswordIfNeeded() error { 240 if cc.encPassword && cc.details.ArtifactoryUrl != "" { 241 err := cc.encryptPassword() 242 if err != nil { 243 return errorutils.CheckErrorf("The following error was received while trying to encrypt your password: %s ", err) 244 } 245 } 246 return nil 247 } 248 249 func (cc *ConfigCommand) configRefreshableTokenIfPossible() { 250 if cc.useBasicAuthOnly { 251 return 252 } 253 // If username and password weren't provided, then the artifactoryToken refresh mechanism isn't set. 254 if cc.details.User == "" || cc.details.Password == "" { 255 return 256 } 257 // Set the default interval for the refreshable tokens to be initialized in the next CLI run. 258 cc.details.ArtifactoryTokenRefreshInterval = coreutils.TokenRefreshDefaultInterval 259 } 260 261 func (cc *ConfigCommand) prepareConfigurationData() ([]*config.ServerDetails, error) { 262 // If details is nil, initialize a new one 263 if cc.details == nil { 264 cc.details = new(config.ServerDetails) 265 if cc.defaultDetails != nil { 266 cc.details.InsecureTls = cc.defaultDetails.InsecureTls 267 } 268 } 269 270 // Get configurations list 271 configurations, err := config.GetAllServersConfigs() 272 if err != nil { 273 return configurations, err 274 } 275 276 // Get server id 277 if cc.interactive && cc.serverId == "" { 278 defaultServerId := "" 279 if cc.defaultDetails != nil { 280 defaultServerId = cc.defaultDetails.ServerId 281 } 282 ioutils.ScanFromConsole("Enter a unique server identifier", &cc.serverId, defaultServerId) 283 } 284 cc.details.ServerId = cc.resolveServerId() 285 286 // Remove and get the server details from the configurations list 287 tempConfiguration, configurations := config.GetAndRemoveConfiguration(cc.details.ServerId, configurations) 288 289 // Set default server details if the server existed in the configurations list. 290 // Otherwise, if default details were not set, initialize empty default details. 291 if tempConfiguration != nil { 292 cc.defaultDetails = tempConfiguration 293 cc.details.IsDefault = tempConfiguration.IsDefault 294 } else if cc.defaultDetails == nil { 295 cc.defaultDetails = new(config.ServerDetails) 296 } 297 298 // Append the configuration to the configurations list 299 configurations = append(configurations, cc.details) 300 return configurations, err 301 } 302 303 // Returning the first non-empty value: 304 // 1. The serverId argument sent. 305 // 2. details.ServerId 306 // 3. defaultDetails.ServerId 307 // 4. config.DEFAULT_SERVER_ID 308 func (cc *ConfigCommand) resolveServerId() string { 309 if cc.serverId != "" { 310 return cc.serverId 311 } 312 if cc.details.ServerId != "" { 313 return cc.details.ServerId 314 } 315 if cc.defaultDetails != nil && cc.defaultDetails.ServerId != "" { 316 return cc.defaultDetails.ServerId 317 } 318 return config.DefaultServerId 319 } 320 321 func (cc *ConfigCommand) getConfigurationFromUser() (err error) { 322 if cc.disablePrompts { 323 cc.fillSpecificUrlsFromPlatform() 324 return nil 325 } 326 327 // If using web login on existing server with platform URL, avoid prompts and skip directly to login. 328 if cc.useWebLogin && cc.defaultDetails.Url != "" { 329 cc.fillSpecificUrlsFromPlatform() 330 return cc.handleWebLogin() 331 } 332 333 if cc.details.Url == "" { 334 ioutils.ScanFromConsole("JFrog Platform URL", &cc.details.Url, cc.defaultDetails.Url) 335 } 336 337 if fileutils.IsSshUrl(cc.details.Url) || fileutils.IsSshUrl(cc.details.ArtifactoryUrl) { 338 return cc.handleSsh() 339 } 340 341 disallowUsingSavedPassword := cc.fillSpecificUrlsFromPlatform() 342 if err = cc.promptUrls(&disallowUsingSavedPassword); err != nil { 343 return 344 } 345 346 var clientCertChecked bool 347 if cc.details.Password == "" && cc.details.AccessToken == "" { 348 clientCertChecked, err = cc.promptForCredentials(disallowUsingSavedPassword) 349 if err != nil { 350 return err 351 } 352 } 353 if !clientCertChecked { 354 cc.checkClientCertForReverseProxy() 355 } 356 return 357 } 358 359 func (cc *ConfigCommand) handleSsh() error { 360 coreutils.SetIfEmpty(&cc.details.ArtifactoryUrl, cc.details.Url) 361 return getSshKeyPath(cc.details) 362 } 363 364 func (cc *ConfigCommand) fillSpecificUrlsFromPlatform() (disallowUsingSavedPassword bool) { 365 cc.details.Url = clientUtils.AddTrailingSlashIfNeeded(cc.details.Url) 366 disallowUsingSavedPassword = coreutils.SetIfEmpty(&cc.details.DistributionUrl, cc.details.Url+"distribution/") || disallowUsingSavedPassword 367 disallowUsingSavedPassword = coreutils.SetIfEmpty(&cc.details.ArtifactoryUrl, cc.details.Url+"artifactory/") || disallowUsingSavedPassword 368 disallowUsingSavedPassword = coreutils.SetIfEmpty(&cc.details.XrayUrl, cc.details.Url+"xray/") || disallowUsingSavedPassword 369 disallowUsingSavedPassword = coreutils.SetIfEmpty(&cc.details.MissionControlUrl, cc.details.Url+"mc/") || disallowUsingSavedPassword 370 disallowUsingSavedPassword = coreutils.SetIfEmpty(&cc.details.PipelinesUrl, cc.details.Url+"pipelines/") || disallowUsingSavedPassword 371 return 372 } 373 374 func (cc *ConfigCommand) checkCertificateForMTLS() { 375 if cc.details.ClientCertPath != "" && cc.details.ClientCertKeyPath != "" { 376 return 377 } 378 cc.readClientCertInfoFromConsole() 379 } 380 381 func (cc *ConfigCommand) promptAuthMethods() (selectedMethod AuthenticationMethod, err error) { 382 if cc.useWebLogin { 383 return WebLogin, nil 384 } 385 386 var selected string 387 authMethods := []AuthenticationMethod{ 388 BasicAuth, 389 AccessToken, 390 MTLS, 391 WebLogin, 392 } 393 var selectableItems []ioutils.PromptItem 394 for _, curMethod := range authMethods { 395 selectableItems = append(selectableItems, ioutils.PromptItem{Option: string(curMethod), TargetValue: &selected}) 396 } 397 err = ioutils.SelectString(selectableItems, "Select one of the following authentication methods:", false, func(item ioutils.PromptItem) { 398 *item.TargetValue = item.Option 399 selectedMethod = AuthenticationMethod(*item.TargetValue) 400 }) 401 return 402 } 403 404 func (cc *ConfigCommand) promptUrls(disallowUsingSavedPassword *bool) error { 405 promptItems := []ioutils.PromptItem{ 406 {Option: "JFrog Artifactory URL", TargetValue: &cc.details.ArtifactoryUrl, DefaultValue: cc.defaultDetails.ArtifactoryUrl}, 407 {Option: "JFrog Distribution URL", TargetValue: &cc.details.DistributionUrl, DefaultValue: cc.defaultDetails.DistributionUrl}, 408 {Option: "JFrog Xray URL", TargetValue: &cc.details.XrayUrl, DefaultValue: cc.defaultDetails.XrayUrl}, 409 {Option: "JFrog Mission Control URL", TargetValue: &cc.details.MissionControlUrl, DefaultValue: cc.defaultDetails.MissionControlUrl}, 410 {Option: "JFrog Pipelines URL", TargetValue: &cc.details.PipelinesUrl, DefaultValue: cc.defaultDetails.PipelinesUrl}, 411 } 412 return ioutils.PromptStrings(promptItems, "Select 'Save and continue' or modify any of the URLs", func(item ioutils.PromptItem) { 413 *disallowUsingSavedPassword = true 414 ioutils.ScanFromConsole(item.Option, item.TargetValue, item.DefaultValue) 415 }) 416 } 417 418 func (cc *ConfigCommand) promptForCredentials(disallowUsingSavedPassword bool) (clientCertChecked bool, err error) { 419 var authMethod AuthenticationMethod 420 authMethod, err = cc.promptAuthMethods() 421 if err != nil { 422 return 423 } 424 switch authMethod { 425 case BasicAuth: 426 return false, ioutils.ReadCredentialsFromConsole(cc.details, cc.defaultDetails, disallowUsingSavedPassword) 427 case AccessToken: 428 return false, cc.promptForAccessToken() 429 case MTLS: 430 cc.checkCertificateForMTLS() 431 log.Warn("Please notice that authentication using client certificates (mTLS) is not supported by commands which integrate with package managers.") 432 return true, nil 433 case WebLogin: 434 // Web login sends requests, so certificates must be obtained first if they are required. 435 cc.checkClientCertForReverseProxy() 436 return true, cc.handleWebLogin() 437 default: 438 return false, errorutils.CheckErrorf("unexpected authentication method") 439 } 440 } 441 442 func (cc *ConfigCommand) promptForAccessToken() error { 443 if err := readAccessTokenFromConsole(cc.details); err != nil { 444 return err 445 } 446 if err := cc.validateTokenIsNotApiKey(); err != nil { 447 return err 448 } 449 if cc.details.User == "" { 450 cc.tryExtractingUsernameFromAccessToken() 451 if cc.details.User == "" { 452 ioutils.ScanFromConsole("JFrog username (optional)", &cc.details.User, "") 453 } 454 } 455 return nil 456 } 457 458 // Some package managers support basic authentication only. To support them, we try to extract the username from the access token. 459 // This is not feasible with reference token. 460 func (cc *ConfigCommand) tryExtractingUsernameFromAccessToken() { 461 cc.details.User = auth.ExtractUsernameFromAccessToken(cc.details.AccessToken) 462 } 463 464 func (cc *ConfigCommand) readClientCertInfoFromConsole() { 465 if cc.details.ClientCertPath == "" { 466 ioutils.ScanFromConsole("Client certificate file path", &cc.details.ClientCertPath, cc.defaultDetails.ClientCertPath) 467 } 468 if cc.details.ClientCertKeyPath == "" { 469 ioutils.ScanFromConsole("Client certificate key path", &cc.details.ClientCertKeyPath, cc.defaultDetails.ClientCertKeyPath) 470 } 471 } 472 473 func (cc *ConfigCommand) checkClientCertForReverseProxy() { 474 if cc.details.ClientCertPath != "" && cc.details.ClientCertKeyPath != "" { 475 return 476 } 477 if coreutils.AskYesNo("Is the Artifactory reverse proxy configured to accept a client certificate?", false) { 478 cc.readClientCertInfoFromConsole() 479 } 480 } 481 482 func readAccessTokenFromConsole(details *config.ServerDetails) error { 483 token, err := ioutils.ScanPasswordFromConsole("JFrog access token:") 484 if err == nil { 485 details.SetAccessToken(token) 486 } 487 return err 488 } 489 490 func getSshKeyPath(details *config.ServerDetails) error { 491 // If path not provided as a key, read from console: 492 if details.SshKeyPath == "" { 493 ioutils.ScanFromConsole("SSH key file path (optional)", &details.SshKeyPath, "") 494 } 495 496 // If path still not provided, return and warn about relying on agent. 497 if details.SshKeyPath == "" { 498 log.Info("SSH Key path not provided. The ssh-agent (if active) will be used.") 499 return nil 500 } 501 502 // If SSH key path provided, check if exists: 503 details.SshKeyPath = clientUtils.ReplaceTildeWithUserHome(details.SshKeyPath) 504 exists, err := fileutils.IsFileExists(details.SshKeyPath, false) 505 if err != nil { 506 return err 507 } 508 509 messageSuffix := ": " 510 if exists { 511 sshKeyBytes, err := os.ReadFile(details.SshKeyPath) 512 if err != nil { 513 return err 514 } 515 encryptedKey, err := auth.IsEncrypted(sshKeyBytes) 516 // If exists and not encrypted (or error occurred), return without asking for passphrase 517 if err != nil || !encryptedKey { 518 return err 519 } 520 log.Info("The key file at the specified path is encrypted.") 521 } else { 522 log.Info("Could not find key in provided path. You may place the key file there later.") 523 messageSuffix = " (optional): " 524 } 525 if details.SshPassphrase == "" { 526 token, err := ioutils.ScanPasswordFromConsole("SSH key passphrase" + messageSuffix) 527 if err != nil { 528 return err 529 } 530 details.SetSshPassphrase(token) 531 } 532 return err 533 } 534 535 func ShowConfig(serverName string) error { 536 var configuration []*config.ServerDetails 537 if serverName != "" { 538 singleConfig, err := config.GetSpecificConfig(serverName, true, false) 539 if err != nil { 540 return err 541 } 542 configuration = []*config.ServerDetails{singleConfig} 543 } else { 544 var err error 545 configuration, err = config.GetAllServersConfigs() 546 if err != nil { 547 return err 548 } 549 } 550 printConfigs(configuration) 551 return nil 552 } 553 554 func Import(configTokenString string) error { 555 serverDetails, err := config.Import(configTokenString) 556 if err != nil { 557 return err 558 } 559 log.Info("Importing server ID", "'"+serverDetails.ServerId+"'") 560 configCommand := &ConfigCommand{ 561 details: serverDetails, 562 serverId: serverDetails.ServerId, 563 } 564 return configCommand.config() 565 } 566 567 func Export(serverName string) error { 568 serverDetails, err := config.GetSpecificConfig(serverName, true, false) 569 if err != nil { 570 return err 571 } 572 if serverDetails.ServerId == "" { 573 return errorutils.CheckErrorf("cannot export config, because it is empty. Run 'jf c add' and then export again") 574 } 575 configTokenString, err := config.Export(serverDetails) 576 if err != nil { 577 return err 578 } 579 log.Output(configTokenString) 580 return nil 581 } 582 583 func moveDefaultConfigToSliceEnd(configuration []*config.ServerDetails) []*config.ServerDetails { 584 lastIndex := len(configuration) - 1 585 // If configuration list has more than one config and the last one is not default, switch the last default config with the last one 586 if len(configuration) > 1 && !configuration[lastIndex].IsDefault { 587 for i, server := range configuration { 588 if server.IsDefault { 589 configuration[i] = configuration[lastIndex] 590 configuration[lastIndex] = server 591 break 592 } 593 } 594 } 595 return configuration 596 } 597 598 func printConfigs(configuration []*config.ServerDetails) { 599 // Make default config to be the last config, so it will be easy to see on the terminal 600 configuration = moveDefaultConfigToSliceEnd(configuration) 601 602 for _, details := range configuration { 603 isDefault := details.IsDefault 604 logIfNotEmpty(details.ServerId, "Server ID:\t\t\t", false, isDefault) 605 logIfNotEmpty(details.Url, "JFrog Platform URL:\t\t", false, isDefault) 606 logIfNotEmpty(details.ArtifactoryUrl, "Artifactory URL:\t\t", false, isDefault) 607 logIfNotEmpty(details.DistributionUrl, "Distribution URL:\t\t", false, isDefault) 608 logIfNotEmpty(details.XrayUrl, "Xray URL:\t\t\t", false, isDefault) 609 logIfNotEmpty(details.MissionControlUrl, "Mission Control URL:\t\t", false, isDefault) 610 logIfNotEmpty(details.PipelinesUrl, "Pipelines URL:\t\t\t", false, isDefault) 611 logIfNotEmpty(details.User, "User:\t\t\t\t", false, isDefault) 612 logIfNotEmpty(details.Password, "Password:\t\t\t", true, isDefault) 613 logAccessTokenIfNotEmpty(details.AccessToken, isDefault) 614 logIfNotEmpty(details.RefreshToken, "Refresh token:\t\t\t", true, isDefault) 615 logIfNotEmpty(details.SshKeyPath, "SSH key file path:\t\t", false, isDefault) 616 logIfNotEmpty(details.SshPassphrase, "SSH passphrase:\t\t\t", true, isDefault) 617 logIfNotEmpty(details.ClientCertPath, "Client certificate file path:\t", false, isDefault) 618 logIfNotEmpty(details.ClientCertKeyPath, "Client certificate key path:\t", false, isDefault) 619 logIfNotEmpty(strconv.FormatBool(details.IsDefault), "Default:\t\t\t", false, isDefault) 620 log.Output() 621 } 622 } 623 624 func logIfNotEmpty(value, prefix string, mask, isDefault bool) { 625 if value != "" { 626 if mask { 627 value = "***" 628 } 629 fullString := prefix + value 630 if isDefault { 631 fullString = coreutils.PrintBoldTitle(fullString) 632 } 633 log.Output(fullString) 634 } 635 } 636 637 func logAccessTokenIfNotEmpty(token string, isDefault bool) { 638 if token == "" { 639 return 640 } 641 tokenString := "***" 642 // Extract the token's subject only if it is JWT 643 if strings.Count(token, ".") == 2 { 644 subject, err := auth.ExtractSubjectFromAccessToken(token) 645 if err != nil { 646 log.Error(err) 647 } else { 648 tokenString += fmt.Sprintf(" (Subject: '%s')", subject) 649 } 650 } 651 652 logIfNotEmpty(tokenString, "Access token:\t\t\t", false, isDefault) 653 } 654 655 func (cc *ConfigCommand) delete() error { 656 configurations, err := config.GetAllServersConfigs() 657 if err != nil { 658 return err 659 } 660 var isDefault, isFoundName bool 661 for i, serverDetails := range configurations { 662 if serverDetails.ServerId == cc.serverId { 663 isDefault = serverDetails.IsDefault 664 configurations = append(configurations[:i], configurations[i+1:]...) 665 isFoundName = true 666 break 667 } 668 669 } 670 if isDefault && len(configurations) > 0 { 671 configurations[0].IsDefault = true 672 } 673 if isFoundName { 674 return config.SaveServersConf(configurations) 675 } 676 log.Info("\"" + cc.serverId + "\" configuration could not be found.\n") 677 return nil 678 } 679 680 // Set the default configuration 681 func (cc *ConfigCommand) use() error { 682 configurations, err := config.GetAllServersConfigs() 683 if err != nil { 684 return err 685 } 686 var serverFound *config.ServerDetails 687 newDefaultServer := true 688 for _, serverDetails := range configurations { 689 if serverDetails.ServerId == cc.serverId { 690 serverFound = serverDetails 691 if serverDetails.IsDefault { 692 newDefaultServer = false 693 break 694 } 695 serverDetails.IsDefault = true 696 } else { 697 serverDetails.IsDefault = false 698 } 699 } 700 // Need to save only if we found a server with the serverId 701 if serverFound != nil { 702 if newDefaultServer { 703 err = config.SaveServersConf(configurations) 704 if err != nil { 705 return err 706 } 707 } 708 usingServerLog := fmt.Sprintf("Using server ID '%s'", serverFound.ServerId) 709 if serverFound.Url != "" { 710 usingServerLog += fmt.Sprintf(" (%s)", serverFound.Url) 711 } else if serverFound.ArtifactoryUrl != "" { 712 usingServerLog += fmt.Sprintf(" (%s)", serverFound.ArtifactoryUrl) 713 } 714 log.Info(usingServerLog) 715 return nil 716 } 717 return errorutils.CheckErrorf("Could not find a server with ID '%s'.", cc.serverId) 718 } 719 720 func (cc *ConfigCommand) clear() error { 721 if cc.interactive { 722 confirmed := coreutils.AskYesNo("Are you sure you want to delete all the configurations?", false) 723 if !confirmed { 724 return nil 725 } 726 } 727 return config.SaveServersConf(make([]*config.ServerDetails, 0)) 728 } 729 730 func GetConfig(serverId string, excludeRefreshableTokens bool) (*config.ServerDetails, error) { 731 return config.GetSpecificConfig(serverId, true, excludeRefreshableTokens) 732 } 733 734 func (cc *ConfigCommand) encryptPassword() error { 735 if cc.details.Password == "" { 736 return nil 737 } 738 739 artAuth, err := cc.details.CreateArtAuthConfig() 740 if err != nil { 741 return err 742 } 743 encPassword, err := utils.GetEncryptedPasswordFromArtifactory(artAuth, cc.details.InsecureTls) 744 if err != nil { 745 return err 746 } 747 cc.details.Password = encPassword 748 return err 749 } 750 751 // Assert all services URLs are safe 752 func (cc *ConfigCommand) assertUrlsSafe() error { 753 for _, curUrl := range []string{cc.details.Url, cc.details.AccessUrl, cc.details.ArtifactoryUrl, 754 cc.details.DistributionUrl, cc.details.MissionControlUrl, cc.details.PipelinesUrl, cc.details.XrayUrl} { 755 if isUrlSafe(curUrl) { 756 continue 757 } 758 if cc.interactive { 759 if cc.disablePrompts || !coreutils.AskYesNo("Your JFrog URL uses an insecure HTTP connection, instead of HTTPS. Are you sure you want to continue?", false) { 760 return errorutils.CheckErrorf("config was aborted due to an insecure HTTP connection") 761 } 762 } else { 763 log.Warn("Your configured JFrog URL uses an insecure HTTP connection. Please consider using SSL (HTTPS instead of HTTP).") 764 } 765 return nil 766 } 767 return nil 768 } 769 770 func (cc *ConfigCommand) validateTokenIsNotApiKey() error { 771 if httpclient.IsApiKey(cc.details.AccessToken) { 772 return errors.New("the provided Access Token is an API key and should be used as a password in username/password authentication") 773 } 774 return nil 775 } 776 777 func (cc *ConfigCommand) handleWebLogin() error { 778 token, err := utils.DoWebLogin(cc.details) 779 if err != nil { 780 return err 781 } 782 cc.details.AccessToken = token.AccessToken 783 cc.details.RefreshToken = token.RefreshToken 784 cc.details.WebLogin = true 785 cc.tryExtractingUsernameFromAccessToken() 786 return nil 787 } 788 789 // Return true if a URL is safe. URL is considered not safe if the following conditions are met: 790 // 1. The URL uses an http:// scheme 791 // 2. The URL leads to a URL outside the local machine 792 func isUrlSafe(urlToCheck string) bool { 793 parsedUrl, err := url.Parse(urlToCheck) 794 if err != nil { 795 // If the URL cannot be parsed, we treat it as safe. 796 return true 797 } 798 799 if parsedUrl.Scheme != "http" { 800 return true 801 } 802 803 hostName := parsedUrl.Hostname() 804 if hostName == "127.0.0.1" || hostName == "localhost" { 805 return true 806 } 807 808 return false 809 } 810 811 func assertSingleAuthMethod(details *config.ServerDetails) error { 812 authMethods := []bool{ 813 details.User != "" && details.Password != "", 814 details.AccessToken != "" && details.ArtifactoryRefreshToken == "", 815 details.SshKeyPath != ""} 816 if coreutils.SumTrueValues(authMethods) > 1 { 817 return errorutils.CheckErrorf("Only one authentication method is allowed: Username + Password/API key, RSA Token (SSH) or Access Token") 818 } 819 return nil 820 } 821 822 type ConfigCommandConfiguration struct { 823 ServerDetails *config.ServerDetails 824 Interactive bool 825 EncPassword bool 826 BasicAuthOnly bool 827 } 828 829 func GetAllServerIds() []string { 830 var serverIds []string 831 configuration, err := config.GetAllServersConfigs() 832 if err != nil { 833 return serverIds 834 } 835 for _, serverConfig := range configuration { 836 serverIds = append(serverIds, serverConfig.ServerId) 837 } 838 return serverIds 839 }