github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/alias-set.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"bufio"
    22  	"context"
    23  	"crypto/tls"
    24  	"crypto/x509"
    25  	"fmt"
    26  	"math/rand"
    27  	"net"
    28  	"net/http"
    29  	"os"
    30  	"strings"
    31  	"time"
    32  
    33  	"github.com/fatih/color"
    34  	"github.com/minio/cli"
    35  	"github.com/minio/mc/pkg/probe"
    36  	"github.com/minio/minio-go/v7"
    37  	"github.com/minio/pkg/v2/console"
    38  	"golang.org/x/term"
    39  )
    40  
    41  const cred = "YellowItalics"
    42  
    43  var aliasSetFlags = []cli.Flag{
    44  	cli.StringFlag{
    45  		Name:  "path",
    46  		Value: "auto",
    47  		Usage: "bucket path lookup supported by the server. Valid options are '[auto, on, off]'",
    48  	},
    49  	cli.StringFlag{
    50  		Name:  "api",
    51  		Usage: "API signature. Valid options are '[S3v4, S3v2]'",
    52  	},
    53  }
    54  
    55  var aliasSetCmd = cli.Command{
    56  	Name:      "set",
    57  	ShortName: "s",
    58  	Usage:     "set a new alias to configuration file",
    59  	Action: func(cli *cli.Context) error {
    60  		return mainAliasSet(cli, false)
    61  	},
    62  	OnUsageError:    onUsageError,
    63  	Before:          setGlobalsFromContext,
    64  	Flags:           append(aliasSetFlags, globalFlags...),
    65  	HideHelpCommand: true,
    66  	CustomHelpTemplate: `NAME:
    67    {{.HelpName}} - {{.Usage}}
    68  
    69  USAGE:
    70    {{.HelpName}} ALIAS URL ACCESSKEY SECRETKEY
    71  
    72  FLAGS:
    73    {{range .VisibleFlags}}{{.}}
    74    {{end}}
    75  EXAMPLES:
    76    1. Add MinIO service under "myminio" alias. For security reasons turn off bash history momentarily.
    77       {{.DisableHistory}}
    78       {{.Prompt}} {{.HelpName}} myminio http://localhost:9000 minio minio123
    79       {{.EnableHistory}}
    80    2. Add MinIO service under "myminio" alias, to use dns style bucket lookup. For security reasons
    81       turn off bash history momentarily.
    82       {{.DisableHistory}}
    83       {{.Prompt}} {{.HelpName}} myminio http://localhost:9000 minio minio123 --api "s3v4" --path "off"
    84       {{.EnableHistory}}
    85    3. Add Amazon S3 storage service under "mys3" alias. For security reasons turn off bash history momentarily.
    86       {{.DisableHistory}}
    87       {{.Prompt}} {{.HelpName}} mys3 https://s3.amazonaws.com \
    88                   BKIKJAA5BMMU2RHO6IBB V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
    89       {{.EnableHistory}}
    90    4. Add Amazon S3 storage service under "mys3" alias, prompting for keys.
    91       {{.Prompt}} {{.HelpName}} mys3 https://s3.amazonaws.com --api "s3v4" --path "off"
    92       Enter Access Key: BKIKJAA5BMMU2RHO6IBB
    93       Enter Secret Key: V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
    94    5. Add Amazon S3 storage service under "mys3" alias using piped keys.
    95       {{.DisableHistory}}
    96       {{.Prompt}} echo -e "BKIKJAA5BMMU2RHO6IBB\nV8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12" | \
    97                   {{.HelpName}} mys3 https://s3.amazonaws.com --api "s3v4" --path "off"
    98       {{.EnableHistory}}
    99  `,
   100  }
   101  
   102  // checkAliasSetSyntax - verifies input arguments to 'alias set'.
   103  func checkAliasSetSyntax(ctx *cli.Context, accessKey, secretKey string, deprecated bool) {
   104  	args := ctx.Args()
   105  	argsNr := len(args)
   106  
   107  	if argsNr == 0 {
   108  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
   109  	}
   110  
   111  	if argsNr > 4 || argsNr < 2 {
   112  		fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
   113  			"Incorrect number of arguments for alias set command.")
   114  	}
   115  
   116  	alias := cleanAlias(args.Get(0))
   117  	url := args.Get(1)
   118  	api := ctx.String("api")
   119  	path := ctx.String("path")
   120  	bucketLookup := ctx.String("lookup")
   121  
   122  	if !isValidAlias(alias) {
   123  		fatalIf(errInvalidAlias(alias), "Invalid alias.")
   124  	}
   125  
   126  	if !isValidHostURL(url) {
   127  		fatalIf(errInvalidURL(url), "Invalid URL.")
   128  	}
   129  
   130  	if !isValidAccessKey(accessKey) {
   131  		fatalIf(errInvalidArgument().Trace(accessKey),
   132  			"Invalid access key `"+accessKey+"`.")
   133  	}
   134  
   135  	if !isValidSecretKey(secretKey) {
   136  		fatalIf(errInvalidArgument().Trace(secretKey),
   137  			"Invalid secret key `"+secretKey+"`.")
   138  	}
   139  
   140  	if api != "" && !isValidAPI(api) { // Empty value set to default "S3v4".
   141  		fatalIf(errInvalidArgument().Trace(api),
   142  			"Unrecognized API signature. Valid options are `[S3v4, S3v2]`.")
   143  	}
   144  
   145  	if deprecated {
   146  		if !isValidLookup(bucketLookup) {
   147  			fatalIf(errInvalidArgument().Trace(bucketLookup),
   148  				"Unrecognized bucket lookup. Valid options are `[dns,auto, path]`.")
   149  		}
   150  	} else {
   151  		if !isValidPath(path) {
   152  			fatalIf(errInvalidArgument().Trace(bucketLookup),
   153  				"Unrecognized path value. Valid options are `[auto, on, off]`.")
   154  		}
   155  	}
   156  }
   157  
   158  // setAlias - set an alias config.
   159  func setAlias(alias string, aliasCfgV10 aliasConfigV10) aliasMessage {
   160  	mcCfgV10, err := loadMcConfig()
   161  	fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config `"+mustGetMcConfigPath()+"`.")
   162  
   163  	// Add new host.
   164  	mcCfgV10.Aliases[alias] = aliasCfgV10
   165  
   166  	err = saveMcConfig(mcCfgV10)
   167  	fatalIf(err.Trace(alias), "Unable to update hosts in config version `"+mustGetMcConfigPath()+"`.")
   168  
   169  	return aliasMessage{
   170  		Alias:     alias,
   171  		URL:       aliasCfgV10.URL,
   172  		AccessKey: aliasCfgV10.AccessKey,
   173  		SecretKey: aliasCfgV10.SecretKey,
   174  		API:       aliasCfgV10.API,
   175  		Path:      aliasCfgV10.Path,
   176  	}
   177  }
   178  
   179  // probeS3Signature - auto probe S3 server signature: issue a Stat call
   180  // using v4 signature then v2 in case of failure.
   181  func probeS3Signature(ctx context.Context, accessKey, secretKey, url string, peerCert *x509.Certificate) (string, *probe.Error) {
   182  	probeBucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "probe-bsign-")
   183  	// Test s3 connection for API auto probe
   184  	s3Config := &Config{
   185  		// S3 connection parameters
   186  		Insecure:          globalInsecure,
   187  		AccessKey:         accessKey,
   188  		SecretKey:         secretKey,
   189  		HostURL:           urlJoinPath(url, probeBucketName),
   190  		Debug:             globalDebug,
   191  		ConnReadDeadline:  globalConnReadDeadline,
   192  		ConnWriteDeadline: globalConnWriteDeadline,
   193  		UploadLimit:       int64(globalLimitUpload),
   194  		DownloadLimit:     int64(globalLimitDownload),
   195  	}
   196  	if peerCert != nil {
   197  		configurePeerCertificate(s3Config, peerCert)
   198  	}
   199  
   200  	probeSignatureType := func(stype string) (string, *probe.Error) {
   201  		s3Config.Signature = stype
   202  		s3Client, err := S3New(s3Config)
   203  		if err != nil {
   204  			return "", err
   205  		}
   206  
   207  		if _, err := s3Client.Stat(ctx, StatOptions{}); err != nil {
   208  			e := err.ToGoError()
   209  			if _, ok := e.(BucketDoesNotExist); ok {
   210  				// Bucket doesn't exist, means signature probing worked successfully.
   211  				return stype, nil
   212  			}
   213  			// AccessDenied means Stat() is not allowed but credentials are valid.
   214  			// AccessDenied is only returned when policy doesn't allow HeadBucket
   215  			// operations.
   216  			if minio.ToErrorResponse(err.ToGoError()).Code == "AccessDenied" {
   217  				return stype, nil
   218  			}
   219  
   220  			// For any other errors we fail.
   221  			return "", err.Trace(s3Config.Signature)
   222  		}
   223  		return stype, nil
   224  	}
   225  
   226  	stype, err := probeSignatureType("s3v4")
   227  	if err != nil {
   228  		if stype, err = probeSignatureType("s3v2"); err != nil {
   229  			return "", err.Trace("s3v4", "s3v2")
   230  		}
   231  		return stype, nil
   232  	}
   233  	return stype, nil
   234  }
   235  
   236  // BuildS3Config constructs an S3 Config and does
   237  // signature auto-probe when needed.
   238  func BuildS3Config(ctx context.Context, alias, url, accessKey, secretKey, api, path string, peerCert *x509.Certificate) (*Config, *probe.Error) {
   239  	s3Config := NewS3Config(alias, url, &aliasConfigV10{
   240  		AccessKey: accessKey,
   241  		SecretKey: secretKey,
   242  		URL:       url,
   243  		Path:      path,
   244  	})
   245  
   246  	if peerCert != nil {
   247  		configurePeerCertificate(s3Config, peerCert)
   248  	}
   249  
   250  	// If api is provided we do not auto probe signature, this is
   251  	// required in situations when signature type is provided by the user.
   252  	if api != "" {
   253  		s3Config.Signature = api
   254  		return s3Config, nil
   255  	}
   256  	// Probe S3 signature version
   257  	api, err := probeS3Signature(ctx, accessKey, secretKey, url, peerCert)
   258  	if err != nil {
   259  		return nil, err.Trace(url, accessKey, api, path)
   260  	}
   261  
   262  	s3Config.Signature = api
   263  	// Success.
   264  	return s3Config, nil
   265  }
   266  
   267  // fetchAliasKeys - returns the user accessKey and secretKey
   268  func fetchAliasKeys(args cli.Args) (string, string) {
   269  	accessKey := ""
   270  	secretKey := ""
   271  	console.SetColor(cred, color.New(color.FgYellow, color.Italic))
   272  	isTerminal := term.IsTerminal(int(os.Stdin.Fd()))
   273  	reader := bufio.NewReader(os.Stdin)
   274  
   275  	argsNr := len(args)
   276  
   277  	if argsNr == 2 {
   278  		if isTerminal {
   279  			fmt.Printf("%s", console.Colorize(cred, "Enter Access Key: "))
   280  		}
   281  		value, _, _ := reader.ReadLine()
   282  		accessKey = string(value)
   283  	} else {
   284  		accessKey = args.Get(2)
   285  	}
   286  
   287  	if argsNr == 2 || argsNr == 3 {
   288  		if isTerminal {
   289  			fmt.Printf("%s", console.Colorize(cred, "Enter Secret Key: "))
   290  			bytePassword, _ := term.ReadPassword(int(os.Stdin.Fd()))
   291  			fmt.Printf("\n")
   292  			secretKey = string(bytePassword)
   293  		} else {
   294  			value, _, _ := reader.ReadLine()
   295  			secretKey = string(value)
   296  		}
   297  	} else {
   298  		secretKey = args.Get(3)
   299  	}
   300  
   301  	return accessKey, secretKey
   302  }
   303  
   304  func mainAliasSet(cli *cli.Context, deprecated bool) error {
   305  	console.SetColor("AliasMessage", color.New(color.FgGreen))
   306  	var (
   307  		args  = cli.Args()
   308  		alias = cleanAlias(args.Get(0))
   309  		url   = trimTrailingSeparator(args.Get(1))
   310  		api   = cli.String("api")
   311  		path  = cli.String("path")
   312  
   313  		peerCert *x509.Certificate
   314  		err      *probe.Error
   315  	)
   316  
   317  	// Support deprecated lookup flag
   318  	if deprecated {
   319  		lookup := strings.ToLower(strings.TrimSpace(cli.String("lookup")))
   320  		switch lookup {
   321  		case "", "auto":
   322  			path = "auto"
   323  		case "path":
   324  			path = "on"
   325  		case "dns":
   326  			path = "off"
   327  		default:
   328  		}
   329  	}
   330  
   331  	accessKey, secretKey := fetchAliasKeys(args)
   332  	checkAliasSetSyntax(cli, accessKey, secretKey, deprecated)
   333  
   334  	ctx, cancelAliasAdd := context.WithCancel(globalContext)
   335  	defer cancelAliasAdd()
   336  
   337  	if !globalInsecure && !globalJSON && term.IsTerminal(int(os.Stdout.Fd())) {
   338  		peerCert, err = promptTrustSelfSignedCert(ctx, url, alias)
   339  		fatalIf(err.Trace(alias, url, accessKey), "Unable to initialize new alias from the provided credentials.")
   340  	}
   341  
   342  	s3Config, err := BuildS3Config(ctx, alias, url, accessKey, secretKey, api, path, peerCert)
   343  	fatalIf(err.Trace(alias, url, accessKey), "Unable to initialize new alias from the provided credentials.")
   344  
   345  	msg := setAlias(alias, aliasConfigV10{
   346  		URL:       s3Config.HostURL,
   347  		AccessKey: s3Config.AccessKey,
   348  		SecretKey: s3Config.SecretKey,
   349  		API:       s3Config.Signature,
   350  		Path:      path,
   351  	}) // Add an alias with specified credentials.
   352  
   353  	msg.op = "set"
   354  	if deprecated {
   355  		msg.op = "add"
   356  	}
   357  
   358  	printMsg(msg)
   359  	return nil
   360  }
   361  
   362  // configurePeerCertificate adds the peer certificate to the
   363  // TLS root CAs of s3Config. Once configured, any client
   364  // initialized with this config trusts the given peer certificate.
   365  func configurePeerCertificate(s3Config *Config, peerCert *x509.Certificate) {
   366  	tr, ok := s3Config.Transport.(*http.Transport)
   367  	if !ok {
   368  		return
   369  	}
   370  	switch {
   371  	case tr == nil:
   372  		if globalRootCAs != nil {
   373  			globalRootCAs.AddCert(peerCert)
   374  		}
   375  		tr = &http.Transport{
   376  			Proxy: http.ProxyFromEnvironment,
   377  			DialContext: (&net.Dialer{
   378  				Timeout:   10 * time.Second,
   379  				KeepAlive: 15 * time.Second,
   380  			}).DialContext,
   381  			MaxIdleConnsPerHost:   256,
   382  			IdleConnTimeout:       90 * time.Second,
   383  			TLSHandshakeTimeout:   10 * time.Second,
   384  			ExpectContinueTimeout: 10 * time.Second,
   385  			DisableCompression:    true,
   386  			TLSClientConfig:       &tls.Config{RootCAs: globalRootCAs},
   387  		}
   388  	case tr.TLSClientConfig == nil || tr.TLSClientConfig.RootCAs == nil:
   389  		if globalRootCAs != nil {
   390  			globalRootCAs.AddCert(peerCert)
   391  		}
   392  		tr.TLSClientConfig = &tls.Config{RootCAs: globalRootCAs}
   393  	default:
   394  		tr.TLSClientConfig.RootCAs.AddCert(peerCert)
   395  	}
   396  	s3Config.Transport = tr
   397  }