github.com/osievert/jfrog-cli-core@v1.2.7/artifactory/commands/config.go (about) 1 package commands 2 3 import ( 4 "errors" 5 "fmt" 6 "github.com/jfrog/jfrog-cli-core/utils/coreutils" 7 "github.com/jfrog/jfrog-cli-core/utils/ioutils" 8 "io/ioutil" 9 "reflect" 10 "strings" 11 "sync" 12 "syscall" 13 14 "github.com/jfrog/jfrog-client-go/auth" 15 16 "github.com/jfrog/jfrog-cli-core/artifactory/commands/generic" 17 "github.com/jfrog/jfrog-cli-core/artifactory/utils" 18 "github.com/jfrog/jfrog-cli-core/utils/config" 19 "github.com/jfrog/jfrog-cli-core/utils/lock" 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 "golang.org/x/crypto/ssh/terminal" 25 ) 26 27 // Internal golang locking for the same process. 28 var mutex sync.Mutex 29 30 type ConfigCommand struct { 31 details *config.ArtifactoryDetails 32 defaultDetails *config.ArtifactoryDetails 33 interactive bool 34 encPassword bool 35 useBasicAuthOnly bool 36 serverId string 37 } 38 39 func NewConfigCommand() *ConfigCommand { 40 return &ConfigCommand{} 41 } 42 43 func (cc *ConfigCommand) SetServerId(serverId string) *ConfigCommand { 44 cc.serverId = serverId 45 return cc 46 } 47 48 func (cc *ConfigCommand) SetEncPassword(encPassword bool) *ConfigCommand { 49 cc.encPassword = encPassword 50 return cc 51 } 52 53 func (cc *ConfigCommand) SetUseBasicAuthOnly(useBasicAuthOnly bool) *ConfigCommand { 54 cc.useBasicAuthOnly = useBasicAuthOnly 55 return cc 56 } 57 58 func (cc *ConfigCommand) SetInteractive(interactive bool) *ConfigCommand { 59 cc.interactive = interactive 60 return cc 61 } 62 63 func (cc *ConfigCommand) SetDefaultDetails(defaultDetails *config.ArtifactoryDetails) *ConfigCommand { 64 cc.defaultDetails = defaultDetails 65 return cc 66 } 67 68 func (cc *ConfigCommand) SetDetails(details *config.ArtifactoryDetails) *ConfigCommand { 69 cc.details = details 70 return cc 71 } 72 73 func (cc *ConfigCommand) Run() error { 74 return cc.Config() 75 } 76 77 func (cc *ConfigCommand) RtDetails() (*config.ArtifactoryDetails, error) { 78 // If cc.details is not empty, then return it. 79 if cc.details != nil && !reflect.DeepEqual(config.ArtifactoryDetails{}, *cc.details) { 80 return cc.details, nil 81 } 82 // If cc.defaultDetails is not empty, then return it. 83 if cc.defaultDetails != nil && !reflect.DeepEqual(config.ArtifactoryDetails{}, *cc.defaultDetails) { 84 return cc.defaultDetails, nil 85 } 86 return nil, nil 87 } 88 89 func (cc *ConfigCommand) CommandName() string { 90 return "rt_config" 91 } 92 93 func (cc *ConfigCommand) Config() error { 94 mutex.Lock() 95 lockFile, err := lock.CreateLock() 96 defer mutex.Unlock() 97 defer lockFile.Unlock() 98 99 if err != nil { 100 return err 101 } 102 103 configurations, err := cc.prepareConfigurationData() 104 if err != nil { 105 return err 106 } 107 if cc.interactive { 108 err = cc.getConfigurationFromUser() 109 if err != nil { 110 return err 111 } 112 } 113 114 // Artifactory expects the username to be lower-cased. In case it is not, 115 // Artifactory will silently save it lower-cased, but the token creation 116 // REST API will fail with a non lower-cased username. 117 cc.details.User = strings.ToLower(cc.details.User) 118 119 if len(configurations) == 1 { 120 cc.details.IsDefault = true 121 } 122 123 err = checkSingleAuthMethod(cc.details) 124 if err != nil { 125 return err 126 } 127 128 if cc.encPassword { 129 err = cc.encryptPassword() 130 if err != nil { 131 return err 132 } 133 } 134 135 if !cc.useBasicAuthOnly { 136 cc.configRefreshableToken() 137 } 138 139 return config.SaveArtifactoryConf(configurations) 140 } 141 142 func (cc *ConfigCommand) configRefreshableToken() { 143 if (cc.details.User == "" || cc.details.Password == "") && cc.details.ApiKey == "" { 144 return 145 } 146 // Set the default interval for the refreshable tokens to be initialized in the next CLI run. 147 cc.details.TokenRefreshInterval = coreutils.TokenRefreshDefaultInterval 148 } 149 150 func (cc *ConfigCommand) prepareConfigurationData() ([]*config.ArtifactoryDetails, error) { 151 // If details is nil, initialize a new one 152 if cc.details == nil { 153 cc.details = new(config.ArtifactoryDetails) 154 if cc.defaultDetails != nil { 155 cc.details.InsecureTls = cc.defaultDetails.InsecureTls 156 } 157 } 158 159 // Get configurations list 160 configurations, err := config.GetAllArtifactoryConfigs() 161 if err != nil { 162 return configurations, err 163 } 164 165 // Get default server details 166 if cc.defaultDetails == nil { 167 cc.defaultDetails, err = config.GetDefaultConfiguredArtifactoryConf(configurations) 168 if err != nil { 169 return configurations, errorutils.CheckError(err) 170 } 171 } 172 173 // Get server id 174 if cc.interactive && cc.serverId == "" { 175 ioutils.ScanFromConsole("Artifactory server ID", &cc.serverId, cc.defaultDetails.ServerId) 176 } 177 cc.details.ServerId = cc.resolveServerId() 178 179 // Remove and get the server details from the configurations list 180 tempConfiguration, configurations := config.GetAndRemoveConfiguration(cc.details.ServerId, configurations) 181 182 // Change default server details if the server was exist in the configurations list 183 if tempConfiguration != nil { 184 cc.defaultDetails = tempConfiguration 185 cc.details.IsDefault = tempConfiguration.IsDefault 186 } 187 188 // Append the configuration to the configurations list 189 configurations = append(configurations, cc.details) 190 return configurations, err 191 } 192 193 /// Returning the first non empty value: 194 // 1. The serverId argument sent. 195 // 2. details.ServerId 196 // 3. defaultDetails.ServerId 197 // 4. config.DEFAULT_SERVER_ID 198 func (cc *ConfigCommand) resolveServerId() string { 199 if cc.serverId != "" { 200 return cc.serverId 201 } 202 if cc.details.ServerId != "" { 203 return cc.details.ServerId 204 } 205 if cc.defaultDetails.ServerId != "" { 206 return cc.defaultDetails.ServerId 207 } 208 return config.DefaultServerId 209 } 210 211 func (cc *ConfigCommand) getConfigurationFromUser() error { 212 allowUsingSavedPassword := true 213 // Artifactory URL 214 if cc.details.Url == "" { 215 ioutils.ScanFromConsole("JFrog Artifactory URL", &cc.details.Url, cc.defaultDetails.Url) 216 allowUsingSavedPassword = false 217 } 218 219 // Distribution URL 220 if cc.details.DistributionUrl == "" { 221 ioutils.ScanFromConsole("JFrog Distribution URL (Optional)", &cc.details.DistributionUrl, cc.defaultDetails.DistributionUrl) 222 allowUsingSavedPassword = false 223 } 224 225 // Ssh-Key 226 if fileutils.IsSshUrl(cc.details.Url) { 227 return getSshKeyPath(cc.details) 228 } 229 cc.details.Url = clientutils.AddTrailingSlashIfNeeded(cc.details.Url) 230 231 // Api-Key/Password/Access-Token 232 if cc.details.ApiKey == "" && cc.details.Password == "" && cc.details.AccessToken == "" { 233 err := readAccessTokenFromConsole(cc.details) 234 if err != nil { 235 return err 236 } 237 if len(cc.details.GetAccessToken()) == 0 { 238 err = ioutils.ReadCredentialsFromConsole(cc.details, cc.defaultDetails, allowUsingSavedPassword) 239 if err != nil { 240 return err 241 } 242 } 243 } 244 245 cc.readRefreshableTokenFromConsole() 246 cc.readClientCertInfoFromConsole() 247 return nil 248 } 249 250 func (cc *ConfigCommand) readClientCertInfoFromConsole() { 251 if cc.details.ClientCertPath != "" && cc.details.ClientCertKeyPath != "" { 252 return 253 } 254 if coreutils.AskYesNo("Is the Artifactory reverse proxy configured to accept a client certificate?", false) { 255 if cc.details.ClientCertPath == "" { 256 ioutils.ScanFromConsole("Client certificate file path", &cc.details.ClientCertPath, cc.defaultDetails.ClientCertPath) 257 } 258 if cc.details.ClientCertKeyPath == "" { 259 ioutils.ScanFromConsole("Client certificate key path", &cc.details.ClientCertKeyPath, cc.defaultDetails.ClientCertKeyPath) 260 } 261 } 262 } 263 264 func (cc *ConfigCommand) readRefreshableTokenFromConsole() { 265 if !cc.useBasicAuthOnly && ((cc.details.ApiKey != "" || cc.details.Password != "") && cc.details.AccessToken == "") { 266 useRefreshableToken := coreutils.AskYesNo("For commands which don't use external tools or the JFrog Distribution service, "+ 267 "JFrog CLI supports replacing the configured username and password/API key with automatically created access token that's refreshed hourly. "+ 268 "Enable this setting?", true) 269 cc.useBasicAuthOnly = !useRefreshableToken 270 } 271 return 272 } 273 274 func readAccessTokenFromConsole(details *config.ArtifactoryDetails) error { 275 print("Access token (Leave blank for username and password/API key): ") 276 byteToken, err := terminal.ReadPassword(int(syscall.Stdin)) 277 if err != nil { 278 return errorutils.CheckError(err) 279 } 280 // New-line required after the access token input: 281 fmt.Println() 282 if len(byteToken) > 0 { 283 details.SetAccessToken(string(byteToken)) 284 _, err := new(generic.PingCommand).SetRtDetails(details).Ping() 285 return err 286 } 287 return nil 288 } 289 290 func getSshKeyPath(details *config.ArtifactoryDetails) error { 291 // If path not provided as a key, read from console: 292 if details.SshKeyPath == "" { 293 ioutils.ScanFromConsole("SSH key file path (optional)", &details.SshKeyPath, "") 294 } 295 296 // If path still not provided, return and warn about relying on agent. 297 if details.SshKeyPath == "" { 298 log.Info("SSH Key path not provided. You can also specify a key path using the --ssh-key-path command option. If no key will be specified, you will rely on ssh-agent only.") 299 return nil 300 } 301 302 // If SSH key path provided, check if exists: 303 details.SshKeyPath = clientutils.ReplaceTildeWithUserHome(details.SshKeyPath) 304 exists, err := fileutils.IsFileExists(details.SshKeyPath, false) 305 if err != nil { 306 return err 307 } 308 309 if exists { 310 sshKeyBytes, err := ioutil.ReadFile(details.SshKeyPath) 311 if err != nil { 312 return nil 313 } 314 encryptedKey, err := auth.IsEncrypted(sshKeyBytes) 315 // If exists and not encrypted (or error occurred), return without asking for passphrase 316 if err != nil || !encryptedKey { 317 return err 318 } 319 log.Info("The key file at the specified path is encrypted, you may pass the passphrase as an option with every command (but config).") 320 321 } else { 322 log.Info("Could not find key in provided path. You may place the key file there later. If you choose to use an encrypted key, you may pass the passphrase as an option with every command.") 323 } 324 325 return err 326 } 327 328 func ShowConfig(serverName string) error { 329 var configuration []*config.ArtifactoryDetails 330 if serverName != "" { 331 singleConfig, err := config.GetArtifactorySpecificConfig(serverName, true, false) 332 if err != nil { 333 return err 334 } 335 configuration = []*config.ArtifactoryDetails{singleConfig} 336 } else { 337 var err error 338 configuration, err = config.GetAllArtifactoryConfigs() 339 if err != nil { 340 return err 341 } 342 } 343 printConfigs(configuration) 344 return nil 345 } 346 347 func Import(serverToken string) error { 348 artifactoryDetails, err := config.Import(serverToken) 349 if err != nil { 350 return err 351 } 352 log.Info("Importing server ID", "'"+artifactoryDetails.ServerId+"'") 353 configCommand := &ConfigCommand{ 354 details: artifactoryDetails, 355 serverId: artifactoryDetails.ServerId, 356 } 357 return configCommand.Config() 358 } 359 360 func Export(serverName string) error { 361 artifactoryDetails, err := config.GetArtifactorySpecificConfig(serverName, true, false) 362 if err != nil { 363 return err 364 } 365 serverToken, err := config.Export(artifactoryDetails) 366 if err != nil { 367 return err 368 } 369 log.Output(serverToken) 370 return nil 371 } 372 373 func printConfigs(configuration []*config.ArtifactoryDetails) { 374 for _, details := range configuration { 375 if details.ServerId != "" { 376 log.Output("Server ID: " + details.ServerId) 377 } 378 if details.Url != "" { 379 log.Output("Url: " + details.Url) 380 } 381 if details.ApiKey != "" { 382 log.Output("API key: " + details.ApiKey) 383 } 384 if details.User != "" { 385 log.Output("User: " + details.User) 386 } 387 if details.Password != "" { 388 log.Output("Password: ***") 389 } 390 if details.AccessToken != "" { 391 log.Output("Access token: ***") 392 } 393 if details.RefreshToken != "" { 394 log.Output("Refresh token: ***") 395 } 396 if details.SshKeyPath != "" { 397 log.Output("SSH key file path: " + details.SshKeyPath) 398 } 399 if details.ClientCertPath != "" { 400 log.Output("Client certificate file path: " + details.ClientCertPath) 401 } 402 if details.ClientCertKeyPath != "" { 403 log.Output("Client certificate key path: " + details.ClientCertKeyPath) 404 } 405 log.Output("Default: ", details.IsDefault) 406 log.Output() 407 } 408 } 409 410 func DeleteConfig(serverName string) error { 411 configurations, err := config.GetAllArtifactoryConfigs() 412 if err != nil { 413 return err 414 } 415 var isDefault, isFoundName bool 416 for i, config := range configurations { 417 if config.ServerId == serverName { 418 isDefault = config.IsDefault 419 configurations = append(configurations[:i], configurations[i+1:]...) 420 isFoundName = true 421 break 422 } 423 424 } 425 if isDefault && len(configurations) > 0 { 426 configurations[0].IsDefault = true 427 } 428 if isFoundName { 429 return config.SaveArtifactoryConf(configurations) 430 } 431 log.Info("\"" + serverName + "\" configuration could not be found.\n") 432 return nil 433 } 434 435 // Set the default configuration 436 func Use(serverId string) error { 437 configurations, err := config.GetAllArtifactoryConfigs() 438 if err != nil { 439 return err 440 } 441 var serverFound *config.ArtifactoryDetails 442 newDefaultServer := true 443 for _, config := range configurations { 444 if config.ServerId == serverId { 445 serverFound = config 446 if config.IsDefault { 447 newDefaultServer = false 448 break 449 } 450 config.IsDefault = true 451 } else { 452 config.IsDefault = false 453 } 454 } 455 // Need to save only if we found a server with the serverId 456 if serverFound != nil { 457 if newDefaultServer { 458 err = config.SaveArtifactoryConf(configurations) 459 if err != nil { 460 return err 461 } 462 } 463 log.Info(fmt.Sprintf("Using server ID '%s' (%s).", serverFound.ServerId, serverFound.Url)) 464 return nil 465 } 466 return errorutils.CheckError(errors.New(fmt.Sprintf("Could not find a server with ID '%s'.", serverId))) 467 } 468 469 func ClearConfig(interactive bool) { 470 if interactive { 471 confirmed := coreutils.AskYesNo("Are you sure you want to delete all the configurations?", false) 472 if !confirmed { 473 return 474 } 475 } 476 config.SaveArtifactoryConf(make([]*config.ArtifactoryDetails, 0)) 477 } 478 479 func GetConfig(serverId string, excludeRefreshableTokens bool) (*config.ArtifactoryDetails, error) { 480 return config.GetArtifactorySpecificConfig(serverId, true, excludeRefreshableTokens) 481 } 482 483 func (cc *ConfigCommand) encryptPassword() error { 484 if cc.details.Password == "" { 485 return nil 486 } 487 488 log.Info("Encrypting password...") 489 490 artAuth, err := cc.details.CreateArtAuthConfig() 491 if err != nil { 492 return err 493 } 494 encPassword, err := utils.GetEncryptedPasswordFromArtifactory(artAuth, cc.details.InsecureTls) 495 if err != nil { 496 return err 497 } 498 cc.details.Password = encPassword 499 return err 500 } 501 502 func checkSingleAuthMethod(details *config.ArtifactoryDetails) error { 503 boolArr := []bool{details.User != "" && details.Password != "", details.ApiKey != "", fileutils.IsSshUrl(details.Url), 504 details.AccessToken != "" && details.RefreshToken == ""} 505 if coreutils.SumTrueValues(boolArr) > 1 { 506 return errorutils.CheckError(errors.New("only one authentication method is allowed: Username + Password/API key, RSA Token (SSH) or Access Token")) 507 } 508 return nil 509 } 510 511 type ConfigCommandConfiguration struct { 512 ArtDetails *config.ArtifactoryDetails 513 Interactive bool 514 EncPassword bool 515 BasicAuthOnly bool 516 } 517 518 func GetAllArtifactoryServerIds() []string { 519 var serverIds []string 520 configuration, err := config.GetAllArtifactoryConfigs() 521 if err != nil { 522 return serverIds 523 } 524 for _, serverConfig := range configuration { 525 serverIds = append(serverIds, serverConfig.ServerId) 526 } 527 return serverIds 528 }