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  }