// Copyright (c) 2015-2022 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "bytes" "context" "io" "net/url" "os" "strings" "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v3/console" ) var anonymousFlags = []cli.Flag{ cli.BoolFlag{ Name: "recursive, r", Usage: "list recursively", }, } // Manage anonymous access to buckets and objects. var anonymousCmd = cli.Command{ Name: "anonymous", Usage: "manage anonymous access to buckets and objects", Action: mainAnonymous, OnUsageError: onUsageError, Before: setGlobalsFromContext, Flags: append(anonymousFlags, globalFlags...), CustomHelpTemplate: `Name: {{.HelpName}} - {{.Usage}} USAGE: {{.HelpName}} [FLAGS] set PERMISSION TARGET {{.HelpName}} [FLAGS] set-json FILE TARGET {{.HelpName}} [FLAGS] get TARGET {{.HelpName}} [FLAGS] get-json TARGET {{.HelpName}} [FLAGS] list TARGET {{if .VisibleFlags}} FLAGS: {{range .VisibleFlags}}{{.}} {{end}}{{end}} PERMISSION: Allowed policies are: [private, public, download, upload]. FILE: A valid S3 anonymous JSON filepath. EXAMPLES: 1. Set bucket to "download" on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} set download s3/mybucket 2. Set bucket to "public" on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} set public s3/shared 3. Set bucket to "upload" on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} set upload s3/incoming 4. Set anonymous to "public" for bucket with prefix on Amazon S3 cloud storage. {{.Prompt}} {{.HelpName}} set public s3/public-commons/images 5. Set a custom prefix based bucket anonymous on Amazon S3 cloud storage using a JSON file. {{.Prompt}} {{.HelpName}} set-json /path/to/anonymous.json s3/public-commons/images 6. Get bucket permissions. {{.Prompt}} {{.HelpName}} get s3/shared 7. Get bucket permissions in JSON format. {{.Prompt}} {{.HelpName}} get-json s3/shared 8. List policies set to a specified bucket. {{.Prompt}} {{.HelpName}} list s3/shared 9. List public object URLs recursively. {{.Prompt}} {{.HelpName}} --recursive links s3/shared/ `, } // anonymousRules contains anonymous rule type anonymousRules struct { Resource string `json:"resource"` Allow string `json:"allow"` } // String colorized access message. func (s anonymousRules) String() string { return console.Colorize("Anonymous", s.Resource+" => "+s.Allow+"") } // JSON jsonified anonymous message. func (s anonymousRules) JSON() string { anonymousJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(anonymousJSONBytes) } // anonymousMessage is container for anonymous command on bucket success and failure messages. type anonymousMessage struct { Operation string `json:"operation"` Status string `json:"status"` Bucket string `json:"bucket"` Perms accessPerms `json:"permission"` Anonymous map[string]interface{} `json:"anonymous,omitempty"` } // String colorized access message. func (s anonymousMessage) String() string { if s.Operation == "set" { return console.Colorize("Anonymous", "Access permission for `"+s.Bucket+"` is set to `"+string(s.Perms)+"`") } if s.Operation == "get" { return console.Colorize("Anonymous", "Access permission for `"+s.Bucket+"`"+" is `"+string(s.Perms)+"`") } if s.Operation == "set-json" { return console.Colorize("Anonymous", "Access permission for `"+s.Bucket+"`"+" is set from `"+string(s.Perms)+"`") } if s.Operation == "get-json" { anonymous, e := json.MarshalIndent(s.Anonymous, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(anonymous) } // nothing to print return "" } // JSON jsonified anonymous message. func (s anonymousMessage) JSON() string { anonymousJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(anonymousJSONBytes) } // anonymousLinksMessage is container for anonymous links command type anonymousLinksMessage struct { Status string `json:"status"` URL string `json:"url"` } // String colorized access message. func (s anonymousLinksMessage) String() string { return console.Colorize("Anonymous", s.URL) } // JSON jsonified anonymous message. func (s anonymousLinksMessage) JSON() string { anonymousJSONBytes, e := json.MarshalIndent(s, "", " ") fatalIf(probe.NewError(e), "Unable to marshal into JSON.") return string(anonymousJSONBytes) } // checkAnonymousSyntax check for incoming syntax. func checkAnonymousSyntax(ctx *cli.Context) { argsLength := len(ctx.Args()) // Always print a help message when we have extra arguments if argsLength > 3 { showCommandHelpAndExit(ctx, 1) // last argument is exit code. } // Always print a help message when no arguments specified if argsLength < 1 { showCommandHelpAndExit(ctx, 1) } firstArg := ctx.Args().Get(0) secondArg := ctx.Args().Get(1) // More syntax checking switch accessPerms(firstArg) { case "set": // Always expect three arguments when setting a anonymous permission. if argsLength != 3 { showCommandHelpAndExit(ctx, 1) } if accessPerms(secondArg) != accessNone && accessPerms(secondArg) != accessDownload && accessPerms(secondArg) != accessUpload && accessPerms(secondArg) != accessPrivate && accessPerms(secondArg) != accessPublic { fatalIf(errDummy().Trace(), "Unrecognized permission `"+secondArg+"`. Allowed values are [private, public, download, upload].") } case "set-json": // Always expect three arguments when setting a anonymous permission. if argsLength != 3 { showCommandHelpAndExit(ctx, 1) } case "get", "get-json": // get or get-json always expects two arguments if argsLength != 2 { showCommandHelpAndExit(ctx, 1) } case "list": // Always expect an argument after list cmd if argsLength != 2 { showCommandHelpAndExit(ctx, 1) } case "links": // Always expect an argument after links cmd if argsLength != 2 { showCommandHelpAndExit(ctx, 1) } default: showCommandHelpAndExit(ctx, 1) } } // Convert an accessPerms to a string recognizable by minio-go func accessPermToString(perm accessPerms) string { anonymous := "" switch perm { case accessNone, accessPrivate: anonymous = "none" case accessDownload: anonymous = "readonly" case accessUpload: anonymous = "writeonly" case accessPublic: anonymous = "readwrite" case accessCustom: anonymous = "custom" } return anonymous } // doSetAccess do set access. func doSetAccess(ctx context.Context, targetURL string, targetPERMS accessPerms) *probe.Error { clnt, err := newClient(targetURL) if err != nil { return err.Trace(targetURL) } anonymous := accessPermToString(targetPERMS) if err = clnt.SetAccess(ctx, anonymous, false); err != nil { return err.Trace(targetURL, string(targetPERMS)) } return nil } // doSetAccessJSON do set access JSON. func doSetAccessJSON(ctx context.Context, targetURL string, targetPERMS accessPerms) *probe.Error { clnt, err := newClient(targetURL) if err != nil { return err.Trace(targetURL) } fileReader, e := os.Open(string(targetPERMS)) if e != nil { fatalIf(probe.NewError(e).Trace(), "Unable to set anonymous for `"+targetURL+"`.") } defer fileReader.Close() const maxJSONSize = 120 * 1024 // 120KiB configBuf := make([]byte, maxJSONSize+1) n, e := io.ReadFull(fileReader, configBuf) if e == nil { return probe.NewError(bytes.ErrTooLarge).Trace(targetURL) } if e != io.ErrUnexpectedEOF { return probe.NewError(e).Trace(targetURL) } configBytes := configBuf[:n] if err = clnt.SetAccess(ctx, string(configBytes), true); err != nil { return err.Trace(targetURL, string(targetPERMS)) } return nil } // Convert a minio-go permission to accessPerms type func stringToAccessPerm(perm string) accessPerms { var anonymous accessPerms switch perm { case "none": anonymous = accessPrivate case "readonly": anonymous = accessDownload case "writeonly": anonymous = accessUpload case "readwrite": anonymous = accessPublic case "private": anonymous = accessPrivate case "custom": anonymous = accessCustom } return anonymous } // doGetAccess do get access. func doGetAccess(ctx context.Context, targetURL string) (perms accessPerms, anonymousStr string, err *probe.Error) { clnt, err := newClient(targetURL) if err != nil { return "", "", err.Trace(targetURL) } perm, anonymousJSON, err := clnt.GetAccess(ctx) if err != nil { return "", "", err.Trace(targetURL) } return stringToAccessPerm(perm), anonymousJSON, nil } // doGetAccessRules do get access rules. func doGetAccessRules(ctx context.Context, targetURL string) (r map[string]string, err *probe.Error) { clnt, err := newClient(targetURL) if err != nil { return map[string]string{}, err.Trace(targetURL) } return clnt.GetAccessRules(ctx) } // Run anonymous list command func runAnonymousListCmd(args cli.Args) { ctx, cancelAnonymousList := context.WithCancel(globalContext) defer cancelAnonymousList() targetURL := args.First() policies, err := doGetAccessRules(ctx, targetURL) if err != nil { switch err.ToGoError().(type) { case APINotImplemented: fatalIf(err.Trace(), "Unable to list policies of a non S3 url `"+targetURL+"`.") default: fatalIf(err.Trace(targetURL), "Unable to list policies of target `"+targetURL+"`.") } } for k, v := range policies { printMsg(anonymousRules{Resource: k, Allow: v}) } } // Run anonymous links command func runAnonymousLinksCmd(args cli.Args, recursive bool) { ctx, cancelAnonymousLinks := context.WithCancel(globalContext) defer cancelAnonymousLinks() // Get alias/bucket/prefix argument targetURL := args.First() // Fetch all policies associated to the passed url policies, err := doGetAccessRules(ctx, targetURL) if err != nil { switch err.ToGoError().(type) { case APINotImplemented: fatalIf(err.Trace(), "Unable to list policies of a non S3 url `"+targetURL+"`.") default: fatalIf(err.Trace(targetURL), "Unable to list policies of target `"+targetURL+"`.") } } // Extract alias from the passed argument, we'll need it to // construct new pathes to list public objects alias, path := url2Alias(targetURL) // Iterate over anonymous rules to fetch public urls, then search // for objects under those urls for k, v := range policies { // Trim the asterisk in anonymous rules anonymousPath := strings.TrimSuffix(k, "*") // Check if current anonymous prefix is related to the url passed by the user if !strings.HasPrefix(anonymousPath, path) { continue } // Check if the found anonymous has read permission perm := stringToAccessPerm(v) if perm != accessDownload && perm != accessPublic { continue } // Construct the new path to search for public objects newURL := alias + "/" + anonymousPath clnt, err := newClient(newURL) fatalIf(err.Trace(newURL), "Unable to initialize target `"+targetURL+"`.") // Search for public objects for content := range clnt.List(globalContext, ListOptions{Recursive: recursive, ShowDir: DirFirst}) { if content.Err != nil { errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") continue } if content.Type.IsDir() && recursive { continue } // Encode public URL u, e := url.Parse(content.URL.String()) errorIf(probe.NewError(e), "Unable to parse url `%s`.", content.URL) publicURL := u.String() // Construct the message to be displayed to the user msg := anonymousLinksMessage{ Status: "success", URL: publicURL, } // Print the found object printMsg(msg) } } } // Run anonymous cmd to fetch set permission func runAnonymousCmd(args cli.Args) { ctx, cancelAnonymous := context.WithCancel(globalContext) defer cancelAnonymous() var targetURL, anonymousStr string var perms accessPerms var probeErr *probe.Error operation := args.First() switch operation { case "set": perms = accessPerms(args.Get(1)) if !perms.isValidAccessPERM() { fatalIf(errDummy().Trace(), "Invalid access permission: `"+string(perms)+"`.") } targetURL = args.Get(2) probeErr = doSetAccess(ctx, targetURL, perms) if probeErr == nil { perms, _, probeErr = doGetAccess(ctx, targetURL) } case "set-json": perms = accessPerms(args.Get(1)) if !perms.isValidAccessFile() { fatalIf(errDummy().Trace(), "Invalid access file: `"+string(perms)+"`.") } targetURL = args.Get(2) probeErr = doSetAccessJSON(ctx, targetURL, perms) case "get", "get-json": targetURL = args.Get(1) perms, anonymousStr, probeErr = doGetAccess(ctx, targetURL) default: fatalIf(errDummy().Trace(), "Invalid operation: `"+operation+"`.") } // Upon error exit. if probeErr != nil { switch probeErr.ToGoError().(type) { case APINotImplemented: fatalIf(probeErr.Trace(), "Unable to "+operation+" anonymous of a non S3 url `"+targetURL+"`.") default: fatalIf(probeErr.Trace(targetURL, string(perms)), "Unable to "+operation+" anonymous `"+string(perms)+"` for `"+targetURL+"`.") } } anonymousJSON := map[string]interface{}{} if anonymousStr != "" { e := json.Unmarshal([]byte(anonymousStr), &anonymousJSON) fatalIf(probe.NewError(e), "Unable to unmarshal custom anonymous file.") } printMsg(anonymousMessage{ Status: "success", Operation: operation, Bucket: targetURL, Perms: perms, Anonymous: anonymousJSON, }) } func mainAnonymous(ctx *cli.Context) error { // check 'anonymous' cli arguments. checkAnonymousSyntax(ctx) // Additional command speific theme customization. console.SetColor("Anonymous", color.New(color.FgGreen, color.Bold)) switch ctx.Args().First() { case "set", "set-json", "get", "get-json": // anonymous set [private|public|download|upload] alias/bucket/prefix // anonymous set-json path-to-anonymous-json-file alias/bucket/prefix // anonymous get alias/bucket/prefix // anonymous get-json alias/bucket/prefix runAnonymousCmd(ctx.Args()) case "list": // anonymous list alias/bucket/prefix runAnonymousListCmd(ctx.Args().Tail()) case "links": // anonymous links alias/bucket/prefix runAnonymousLinksCmd(ctx.Args().Tail(), ctx.Bool("recursive")) default: // Shows command example and exit showCommandHelpAndExit(ctx, 1) } return nil }