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  }