github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/anonymous-main.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  	"bytes"
    22  	"context"
    23  	"io"
    24  	"net/url"
    25  	"os"
    26  	"strings"
    27  
    28  	"github.com/fatih/color"
    29  	"github.com/minio/cli"
    30  	json "github.com/minio/colorjson"
    31  	"github.com/minio/mc/pkg/probe"
    32  	"github.com/minio/pkg/v2/console"
    33  )
    34  
    35  var anonymousFlags = []cli.Flag{
    36  	cli.BoolFlag{
    37  		Name:  "recursive, r",
    38  		Usage: "list recursively",
    39  	},
    40  }
    41  
    42  // Manage anonymous access to buckets and objects.
    43  var anonymousCmd = cli.Command{
    44  	Name:         "anonymous",
    45  	Usage:        "manage anonymous access to buckets and objects",
    46  	Action:       mainAnonymous,
    47  	OnUsageError: onUsageError,
    48  	Before:       setGlobalsFromContext,
    49  	Flags:        append(anonymousFlags, globalFlags...),
    50  	CustomHelpTemplate: `Name:
    51    {{.HelpName}} - {{.Usage}}
    52  
    53  USAGE:
    54    {{.HelpName}} [FLAGS] set PERMISSION TARGET
    55    {{.HelpName}} [FLAGS] set-json FILE TARGET
    56    {{.HelpName}} [FLAGS] get TARGET
    57    {{.HelpName}} [FLAGS] get-json TARGET
    58    {{.HelpName}} [FLAGS] list TARGET
    59  {{if .VisibleFlags}}
    60  FLAGS:
    61    {{range .VisibleFlags}}{{.}}
    62    {{end}}{{end}}
    63  PERMISSION:
    64    Allowed policies are: [private, public, download, upload].
    65  
    66  FILE:
    67    A valid S3 anonymous JSON filepath.
    68  
    69  EXAMPLES:
    70    1. Set bucket to "download" on Amazon S3 cloud storage.
    71       {{.Prompt}} {{.HelpName}} set download s3/mybucket
    72  
    73    2. Set bucket to "public" on Amazon S3 cloud storage.
    74       {{.Prompt}} {{.HelpName}} set public s3/shared
    75  
    76    3. Set bucket to "upload" on Amazon S3 cloud storage.
    77       {{.Prompt}} {{.HelpName}} set upload s3/incoming
    78  
    79    4. Set anonymous to "public" for bucket with prefix on Amazon S3 cloud storage.
    80       {{.Prompt}} {{.HelpName}} set public s3/public-commons/images
    81  
    82    5. Set a custom prefix based bucket anonymous on Amazon S3 cloud storage using a JSON file.
    83       {{.Prompt}} {{.HelpName}} set-json /path/to/anonymous.json s3/public-commons/images 
    84  
    85    6. Get bucket permissions.
    86       {{.Prompt}} {{.HelpName}} get s3/shared
    87  
    88    7. Get bucket permissions in JSON format.
    89       {{.Prompt}} {{.HelpName}} get-json s3/shared
    90  
    91    8. List policies set to a specified bucket.
    92       {{.Prompt}} {{.HelpName}} list s3/shared
    93  
    94    9. List public object URLs recursively.
    95       {{.Prompt}} {{.HelpName}} --recursive links s3/shared/
    96  `,
    97  }
    98  
    99  // anonymousRules contains anonymous rule
   100  type anonymousRules struct {
   101  	Resource string `json:"resource"`
   102  	Allow    string `json:"allow"`
   103  }
   104  
   105  // String colorized access message.
   106  func (s anonymousRules) String() string {
   107  	return console.Colorize("Anonymous", s.Resource+" => "+s.Allow+"")
   108  }
   109  
   110  // JSON jsonified anonymous message.
   111  func (s anonymousRules) JSON() string {
   112  	anonymousJSONBytes, e := json.MarshalIndent(s, "", " ")
   113  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   114  	return string(anonymousJSONBytes)
   115  }
   116  
   117  // anonymousMessage is container for anonymous command on bucket success and failure messages.
   118  type anonymousMessage struct {
   119  	Operation string                 `json:"operation"`
   120  	Status    string                 `json:"status"`
   121  	Bucket    string                 `json:"bucket"`
   122  	Perms     accessPerms            `json:"permission"`
   123  	Anonymous map[string]interface{} `json:"anonymous,omitempty"`
   124  }
   125  
   126  // String colorized access message.
   127  func (s anonymousMessage) String() string {
   128  	if s.Operation == "set" {
   129  		return console.Colorize("Anonymous",
   130  			"Access permission for `"+s.Bucket+"` is set to `"+string(s.Perms)+"`")
   131  	}
   132  	if s.Operation == "get" {
   133  		return console.Colorize("Anonymous",
   134  			"Access permission for `"+s.Bucket+"`"+" is `"+string(s.Perms)+"`")
   135  	}
   136  	if s.Operation == "set-json" {
   137  		return console.Colorize("Anonymous",
   138  			"Access permission for `"+s.Bucket+"`"+" is set from `"+string(s.Perms)+"`")
   139  	}
   140  	if s.Operation == "get-json" {
   141  		anonymous, e := json.MarshalIndent(s.Anonymous, "", " ")
   142  		fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   143  		return string(anonymous)
   144  	}
   145  	// nothing to print
   146  	return ""
   147  }
   148  
   149  // JSON jsonified anonymous message.
   150  func (s anonymousMessage) JSON() string {
   151  	anonymousJSONBytes, e := json.MarshalIndent(s, "", " ")
   152  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   153  
   154  	return string(anonymousJSONBytes)
   155  }
   156  
   157  // anonymousLinksMessage is container for anonymous links command
   158  type anonymousLinksMessage struct {
   159  	Status string `json:"status"`
   160  	URL    string `json:"url"`
   161  }
   162  
   163  // String colorized access message.
   164  func (s anonymousLinksMessage) String() string {
   165  	return console.Colorize("Anonymous", s.URL)
   166  }
   167  
   168  // JSON jsonified anonymous message.
   169  func (s anonymousLinksMessage) JSON() string {
   170  	anonymousJSONBytes, e := json.MarshalIndent(s, "", " ")
   171  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   172  
   173  	return string(anonymousJSONBytes)
   174  }
   175  
   176  // checkAnonymousSyntax check for incoming syntax.
   177  func checkAnonymousSyntax(ctx *cli.Context) {
   178  	argsLength := len(ctx.Args())
   179  	// Always print a help message when we have extra arguments
   180  	if argsLength > 3 {
   181  		showCommandHelpAndExit(ctx, 1) // last argument is exit code.
   182  	}
   183  	// Always print a help message when no arguments specified
   184  	if argsLength < 1 {
   185  		showCommandHelpAndExit(ctx, 1)
   186  	}
   187  
   188  	firstArg := ctx.Args().Get(0)
   189  	secondArg := ctx.Args().Get(1)
   190  
   191  	// More syntax checking
   192  	switch accessPerms(firstArg) {
   193  	case "set":
   194  		// Always expect three arguments when setting a anonymous permission.
   195  		if argsLength != 3 {
   196  			showCommandHelpAndExit(ctx, 1)
   197  		}
   198  		if accessPerms(secondArg) != accessNone &&
   199  			accessPerms(secondArg) != accessDownload &&
   200  			accessPerms(secondArg) != accessUpload &&
   201  			accessPerms(secondArg) != accessPrivate &&
   202  			accessPerms(secondArg) != accessPublic {
   203  			fatalIf(errDummy().Trace(),
   204  				"Unrecognized permission `"+secondArg+"`. Allowed values are [private, public, download, upload].")
   205  		}
   206  
   207  	case "set-json":
   208  		// Always expect three arguments when setting a anonymous permission.
   209  		if argsLength != 3 {
   210  			showCommandHelpAndExit(ctx, 1)
   211  		}
   212  	case "get", "get-json":
   213  		// get or get-json always expects two arguments
   214  		if argsLength != 2 {
   215  			showCommandHelpAndExit(ctx, 1)
   216  		}
   217  	case "list":
   218  		// Always expect an argument after list cmd
   219  		if argsLength != 2 {
   220  			showCommandHelpAndExit(ctx, 1)
   221  		}
   222  	case "links":
   223  		// Always expect an argument after links cmd
   224  		if argsLength != 2 {
   225  			showCommandHelpAndExit(ctx, 1)
   226  		}
   227  	default:
   228  		showCommandHelpAndExit(ctx, 1)
   229  	}
   230  }
   231  
   232  // Convert an accessPerms to a string recognizable by minio-go
   233  func accessPermToString(perm accessPerms) string {
   234  	anonymous := ""
   235  	switch perm {
   236  	case accessNone, accessPrivate:
   237  		anonymous = "none"
   238  	case accessDownload:
   239  		anonymous = "readonly"
   240  	case accessUpload:
   241  		anonymous = "writeonly"
   242  	case accessPublic:
   243  		anonymous = "readwrite"
   244  	case accessCustom:
   245  		anonymous = "custom"
   246  	}
   247  	return anonymous
   248  }
   249  
   250  // doSetAccess do set access.
   251  func doSetAccess(ctx context.Context, targetURL string, targetPERMS accessPerms) *probe.Error {
   252  	clnt, err := newClient(targetURL)
   253  	if err != nil {
   254  		return err.Trace(targetURL)
   255  	}
   256  	anonymous := accessPermToString(targetPERMS)
   257  	if err = clnt.SetAccess(ctx, anonymous, false); err != nil {
   258  		return err.Trace(targetURL, string(targetPERMS))
   259  	}
   260  	return nil
   261  }
   262  
   263  // doSetAccessJSON do set access JSON.
   264  func doSetAccessJSON(ctx context.Context, targetURL string, targetPERMS accessPerms) *probe.Error {
   265  	clnt, err := newClient(targetURL)
   266  	if err != nil {
   267  		return err.Trace(targetURL)
   268  	}
   269  	fileReader, e := os.Open(string(targetPERMS))
   270  	if e != nil {
   271  		fatalIf(probe.NewError(e).Trace(), "Unable to set anonymous for `"+targetURL+"`.")
   272  	}
   273  	defer fileReader.Close()
   274  
   275  	const maxJSONSize = 120 * 1024 // 120KiB
   276  	configBuf := make([]byte, maxJSONSize+1)
   277  
   278  	n, e := io.ReadFull(fileReader, configBuf)
   279  	if e == nil {
   280  		return probe.NewError(bytes.ErrTooLarge).Trace(targetURL)
   281  	}
   282  	if e != io.ErrUnexpectedEOF {
   283  		return probe.NewError(e).Trace(targetURL)
   284  	}
   285  
   286  	configBytes := configBuf[:n]
   287  	if err = clnt.SetAccess(ctx, string(configBytes), true); err != nil {
   288  		return err.Trace(targetURL, string(targetPERMS))
   289  	}
   290  	return nil
   291  }
   292  
   293  // Convert a minio-go permission to accessPerms type
   294  func stringToAccessPerm(perm string) accessPerms {
   295  	var anonymous accessPerms
   296  	switch perm {
   297  	case "none":
   298  		anonymous = accessPrivate
   299  	case "readonly":
   300  		anonymous = accessDownload
   301  	case "writeonly":
   302  		anonymous = accessUpload
   303  	case "readwrite":
   304  		anonymous = accessPublic
   305  	case "private":
   306  		anonymous = accessPrivate
   307  	case "custom":
   308  		anonymous = accessCustom
   309  	}
   310  	return anonymous
   311  }
   312  
   313  // doGetAccess do get access.
   314  func doGetAccess(ctx context.Context, targetURL string) (perms accessPerms, anonymousStr string, err *probe.Error) {
   315  	clnt, err := newClient(targetURL)
   316  	if err != nil {
   317  		return "", "", err.Trace(targetURL)
   318  	}
   319  	perm, anonymousJSON, err := clnt.GetAccess(ctx)
   320  	if err != nil {
   321  		return "", "", err.Trace(targetURL)
   322  	}
   323  	return stringToAccessPerm(perm), anonymousJSON, nil
   324  }
   325  
   326  // doGetAccessRules do get access rules.
   327  func doGetAccessRules(ctx context.Context, targetURL string) (r map[string]string, err *probe.Error) {
   328  	clnt, err := newClient(targetURL)
   329  	if err != nil {
   330  		return map[string]string{}, err.Trace(targetURL)
   331  	}
   332  	return clnt.GetAccessRules(ctx)
   333  }
   334  
   335  // Run anonymous list command
   336  func runAnonymousListCmd(args cli.Args) {
   337  	ctx, cancelAnonymousList := context.WithCancel(globalContext)
   338  	defer cancelAnonymousList()
   339  
   340  	targetURL := args.First()
   341  	policies, err := doGetAccessRules(ctx, targetURL)
   342  	if err != nil {
   343  		switch err.ToGoError().(type) {
   344  		case APINotImplemented:
   345  			fatalIf(err.Trace(), "Unable to list policies of a non S3 url `"+targetURL+"`.")
   346  		default:
   347  			fatalIf(err.Trace(targetURL), "Unable to list policies of target `"+targetURL+"`.")
   348  		}
   349  	}
   350  	for k, v := range policies {
   351  		printMsg(anonymousRules{Resource: k, Allow: v})
   352  	}
   353  }
   354  
   355  // Run anonymous links command
   356  func runAnonymousLinksCmd(args cli.Args, recursive bool) {
   357  	ctx, cancelAnonymousLinks := context.WithCancel(globalContext)
   358  	defer cancelAnonymousLinks()
   359  
   360  	// Get alias/bucket/prefix argument
   361  	targetURL := args.First()
   362  
   363  	// Fetch all policies associated to the passed url
   364  	policies, err := doGetAccessRules(ctx, targetURL)
   365  	if err != nil {
   366  		switch err.ToGoError().(type) {
   367  		case APINotImplemented:
   368  			fatalIf(err.Trace(), "Unable to list policies of a non S3 url `"+targetURL+"`.")
   369  		default:
   370  			fatalIf(err.Trace(targetURL), "Unable to list policies of target `"+targetURL+"`.")
   371  		}
   372  	}
   373  
   374  	// Extract alias from the passed argument, we'll need it to
   375  	// construct new pathes to list public objects
   376  	alias, path := url2Alias(targetURL)
   377  
   378  	// Iterate over anonymous rules to fetch public urls, then search
   379  	// for objects under those urls
   380  	for k, v := range policies {
   381  		// Trim the asterisk in anonymous rules
   382  		anonymousPath := strings.TrimSuffix(k, "*")
   383  		// Check if current anonymous prefix is related to the url passed by the user
   384  		if !strings.HasPrefix(anonymousPath, path) {
   385  			continue
   386  		}
   387  		// Check if the found anonymous has read permission
   388  		perm := stringToAccessPerm(v)
   389  		if perm != accessDownload && perm != accessPublic {
   390  			continue
   391  		}
   392  		// Construct the new path to search for public objects
   393  		newURL := alias + "/" + anonymousPath
   394  		clnt, err := newClient(newURL)
   395  		fatalIf(err.Trace(newURL), "Unable to initialize target `"+targetURL+"`.")
   396  		// Search for public objects
   397  		for content := range clnt.List(globalContext, ListOptions{Recursive: recursive, ShowDir: DirFirst}) {
   398  			if content.Err != nil {
   399  				errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
   400  				continue
   401  			}
   402  
   403  			if content.Type.IsDir() && recursive {
   404  				continue
   405  			}
   406  
   407  			// Encode public URL
   408  			u, e := url.Parse(content.URL.String())
   409  			errorIf(probe.NewError(e), "Unable to parse url `"+content.URL.String()+"`.")
   410  			publicURL := u.String()
   411  
   412  			// Construct the message to be displayed to the user
   413  			msg := anonymousLinksMessage{
   414  				Status: "success",
   415  				URL:    publicURL,
   416  			}
   417  			// Print the found object
   418  			printMsg(msg)
   419  		}
   420  	}
   421  }
   422  
   423  // Run anonymous cmd to fetch set permission
   424  func runAnonymousCmd(args cli.Args) {
   425  	ctx, cancelAnonymous := context.WithCancel(globalContext)
   426  	defer cancelAnonymous()
   427  
   428  	var targetURL, anonymousStr string
   429  	var perms accessPerms
   430  	var probeErr *probe.Error
   431  
   432  	operation := args.First()
   433  	switch operation {
   434  	case "set":
   435  		perms = accessPerms(args.Get(1))
   436  		if !perms.isValidAccessPERM() {
   437  			fatalIf(errDummy().Trace(), "Invalid access permission: `"+string(perms)+"`.")
   438  		}
   439  		targetURL = args.Get(2)
   440  		probeErr = doSetAccess(ctx, targetURL, perms)
   441  		if probeErr == nil {
   442  			perms, _, probeErr = doGetAccess(ctx, targetURL)
   443  		}
   444  	case "set-json":
   445  		perms = accessPerms(args.Get(1))
   446  		if !perms.isValidAccessFile() {
   447  			fatalIf(errDummy().Trace(), "Invalid access file: `"+string(perms)+"`.")
   448  		}
   449  		targetURL = args.Get(2)
   450  		probeErr = doSetAccessJSON(ctx, targetURL, perms)
   451  	case "get", "get-json":
   452  		targetURL = args.Get(1)
   453  		perms, anonymousStr, probeErr = doGetAccess(ctx, targetURL)
   454  	default:
   455  		fatalIf(errDummy().Trace(), "Invalid operation: `"+operation+"`.")
   456  	}
   457  	// Upon error exit.
   458  	if probeErr != nil {
   459  		switch probeErr.ToGoError().(type) {
   460  		case APINotImplemented:
   461  			fatalIf(probeErr.Trace(), "Unable to "+operation+" anonymous of a non S3 url `"+targetURL+"`.")
   462  		default:
   463  			fatalIf(probeErr.Trace(targetURL, string(perms)),
   464  				"Unable to "+operation+" anonymous `"+string(perms)+"` for `"+targetURL+"`.")
   465  		}
   466  	}
   467  	anonymousJSON := map[string]interface{}{}
   468  	if anonymousStr != "" {
   469  		e := json.Unmarshal([]byte(anonymousStr), &anonymousJSON)
   470  		fatalIf(probe.NewError(e), "Unable to unmarshal custom anonymous file.")
   471  	}
   472  	printMsg(anonymousMessage{
   473  		Status:    "success",
   474  		Operation: operation,
   475  		Bucket:    targetURL,
   476  		Perms:     perms,
   477  		Anonymous: anonymousJSON,
   478  	})
   479  }
   480  
   481  func mainAnonymous(ctx *cli.Context) error {
   482  	// check 'anonymous' cli arguments.
   483  	checkAnonymousSyntax(ctx)
   484  
   485  	// Additional command speific theme customization.
   486  	console.SetColor("Anonymous", color.New(color.FgGreen, color.Bold))
   487  
   488  	switch ctx.Args().First() {
   489  	case "set", "set-json", "get", "get-json":
   490  		// anonymous set [private|public|download|upload] alias/bucket/prefix
   491  		// anonymous set-json path-to-anonymous-json-file alias/bucket/prefix
   492  		// anonymous get alias/bucket/prefix
   493  		// anonymous get-json alias/bucket/prefix
   494  		runAnonymousCmd(ctx.Args())
   495  	case "list":
   496  		// anonymous list alias/bucket/prefix
   497  		runAnonymousListCmd(ctx.Args().Tail())
   498  	case "links":
   499  		// anonymous links alias/bucket/prefix
   500  		runAnonymousLinksCmd(ctx.Args().Tail(), ctx.Bool("recursive"))
   501  	default:
   502  		// Shows command example and exit
   503  		showCommandHelpAndExit(ctx, 1)
   504  	}
   505  	return nil
   506  }