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