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  }