mirror of
https://github.com/minio/mc.git
synced 2025-04-19 21:02:15 +03:00
507 lines
15 KiB
Go
507 lines
15 KiB
Go
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
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
|
|
}
|