1
0
mirror of https://github.com/minio/mc.git synced 2025-11-10 13:42:32 +03:00
Files
mc/cmd/policy-main.go
Ashish Kumar Sinha fef437bdd6 Modify policy command (#2768)
Add set, set-json, get , get-json sub-commands.

Example:
mc policy set download s3/mybucket
mc policy set-json path-to-json-file s3/mybucket
mc policy get s3/mybucket
mc policy get-json s3/mybucket
2019-08-06 21:09:28 -07:00

493 lines
14 KiB
Go

/*
* MinIO Client, (C) 2015-2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"bytes"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/minio/cli"
json "github.com/minio/mc/pkg/colorjson"
"github.com/minio/mc/pkg/console"
"github.com/minio/mc/pkg/probe"
)
var (
policyFlags = []cli.Flag{
cli.BoolFlag{
Name: "recursive, r",
Usage: "list recursively",
},
}
)
// Manage anonymous access to buckets and objects.
var policyCmd = cli.Command{
Name: "policy",
Usage: "manage anonymous access to buckets and objects",
Action: mainPolicy,
Before: setGlobalsFromContext,
Flags: append(policyFlags, globalFlags...),
CustomHelpTemplate: `Name:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] PERMISSION TARGET
{{.HelpName}} [FLAGS] FILE TARGET
{{.HelpName}} [FLAGS] TARGET
{{.HelpName}} list [FLAGS] TARGET
{{if .VisibleFlags}}
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
PERMISSION:
Allowed policies are: [none, download, upload, public].
FILE:
A valid S3 policy JSON filepath.
EXAMPLES:
1. Set bucket to "download" on Amazon S3 cloud storage.
$ {{.HelpName}} set download s3/burningman2011
2. Set bucket to "public" on Amazon S3 cloud storage.
$ {{.HelpName}} set public s3/shared
3. Set bucket to "upload" on Amazon S3 cloud storage.
$ {{.HelpName}} set upload s3/incoming
4. Set policy to "public" for bucket with prefix on Amazon S3 cloud storage.
$ {{.HelpName}} set public s3/public-commons/images
5. Set a custom prefix based bucket policy on Amazon S3 cloud storage using a JSON file.
$ {{.HelpName}} set-json /path/to/policy.json s3/public-commons/images
6. Get bucket permissions.
$ {{.HelpName}} get s3/shared
7. Get bucket permissions in JSON format.
$ {{.HelpName}} get-json s3/shared
8. List policies set to a specified bucket.
$ {{.HelpName}} list s3/shared
9. List public object URLs recursively.
$ {{.HelpName}} --recursive links s3/shared/
`,
}
// policyRules contains policy rule
type policyRules struct {
Resource string `json:"resource"`
Allow string `json:"allow"`
}
// String colorized access message.
func (s policyRules) String() string {
return console.Colorize("Policy", s.Resource+" => "+s.Allow+"")
}
// JSON jsonified policy message.
func (s policyRules) JSON() string {
policyJSONBytes, e := json.MarshalIndent(s, "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(policyJSONBytes)
}
// policyMessage is container for policy command on bucket success and failure messages.
type policyMessage struct {
Operation string `json:"operation"`
Status string `json:"status"`
Bucket string `json:"bucket"`
Perms accessPerms `json:"permission"`
Policy map[string]interface{} `json:"policy,omitempty"`
}
// String colorized access message.
func (s policyMessage) String() string {
if s.Operation == "set" {
return console.Colorize("Policy",
"Access permission for `"+s.Bucket+"` is set to `"+string(s.Perms)+"`")
}
if s.Operation == "get" {
return console.Colorize("Policy",
"Access permission for `"+s.Bucket+"`"+" is `"+string(s.Perms)+"`")
}
if s.Operation == "set-json" {
return console.Colorize("Policy",
"Access permission for `"+s.Bucket+"`"+" is set from `"+string(s.Perms)+"`")
}
if s.Operation == "get-json" {
policy, e := json.MarshalIndent(s.Policy, "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(policy)
}
// nothing to print
return ""
}
// JSON jsonified policy message.
func (s policyMessage) JSON() string {
policyJSONBytes, e := json.MarshalIndent(s, "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(policyJSONBytes)
}
// policyLinksMessage is container for policy links command
type policyLinksMessage struct {
Status string `json:"status"`
URL string `json:"url"`
}
// String colorized access message.
func (s policyLinksMessage) String() string {
return console.Colorize("Policy", string(s.URL))
}
// JSON jsonified policy message.
func (s policyLinksMessage) JSON() string {
policyJSONBytes, e := json.MarshalIndent(s, "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(policyJSONBytes)
}
// checkPolicySyntax check for incoming syntax.
func checkPolicySyntax(ctx *cli.Context) {
argsLength := len(ctx.Args())
// Always print a help message when we have extra arguments
if argsLength > 3 {
cli.ShowCommandHelpAndExit(ctx, "policy", 1) // last argument is exit code.
}
// Always print a help message when no arguments specified
if argsLength < 1 {
cli.ShowCommandHelpAndExit(ctx, "policy", 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 policy permission.
if argsLength != 3 {
cli.ShowCommandHelpAndExit(ctx, "policy", 1)
}
if accessPerms(secondArg) != accessNone &&
accessPerms(secondArg) != accessDownload &&
accessPerms(secondArg) != accessUpload &&
accessPerms(secondArg) != accessPublic {
fatalIf(errDummy().Trace(),
"Unrecognized permission `"+string(secondArg)+"`. Allowed values are [none, download, upload, public].")
}
case "set-json":
// Always expect three arguments when setting a policy permission.
if argsLength != 3 {
cli.ShowCommandHelpAndExit(ctx, "policy", 1)
}
// Validate the type of input file
if filepath.Ext(string(secondArg)) != ".json" {
fatalIf(errDummy().Trace(),
"Unrecognized policy file format `"+string(secondArg)+"`. Only json files are accepted.")
}
case "get", "get-json":
// get or get-json always expects two arguments
if argsLength != 2 {
cli.ShowCommandHelpAndExit(ctx, "policy", 1)
}
case "list":
// Always expect an argument after list cmd
if argsLength != 2 {
cli.ShowCommandHelpAndExit(ctx, "policy", 1)
}
case "links":
// Always expect an argument after links cmd
if argsLength != 2 {
cli.ShowCommandHelpAndExit(ctx, "policy", 1)
}
default:
cli.ShowCommandHelpAndExit(ctx, "policy", 1)
}
}
// Convert an accessPerms to a string recognizable by minio-go
func accessPermToString(perm accessPerms) string {
policy := ""
switch perm {
case accessNone:
policy = "none"
case accessDownload:
policy = "readonly"
case accessUpload:
policy = "writeonly"
case accessPublic:
policy = "readwrite"
case accessCustom:
policy = "custom"
}
return policy
}
// doSetAccess do set access.
func doSetAccess(targetURL string, targetPERMS accessPerms) *probe.Error {
clnt, err := newClient(targetURL)
if err != nil {
return err.Trace(targetURL)
}
policy := accessPermToString(targetPERMS)
if err = clnt.SetAccess(policy, false); err != nil {
return err.Trace(targetURL, string(targetPERMS))
}
return nil
}
// doSetAccessJSON do set access JSON.
func doSetAccessJSON(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 policy 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(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 policy accessPerms
switch perm {
case "none":
policy = accessNone
case "readonly":
policy = accessDownload
case "writeonly":
policy = accessUpload
case "readwrite":
policy = accessPublic
case "custom":
policy = accessCustom
}
return policy
}
// doGetAccess do get access.
func doGetAccess(targetURL string) (perms accessPerms, policyStr string, err *probe.Error) {
clnt, err := newClient(targetURL)
if err != nil {
return "", "", err.Trace(targetURL)
}
perm, policyJSON, err := clnt.GetAccess()
if err != nil {
return "", "", err.Trace(targetURL)
}
return stringToAccessPerm(perm), policyJSON, nil
}
// doGetAccessRules do get access rules.
func doGetAccessRules(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()
}
// Run policy list command
func runPolicyListCmd(args cli.Args) {
targetURL := args.First()
policies, err := doGetAccessRules(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(policyRules{Resource: k, Allow: v})
}
}
// Run policy links command
func runPolicyLinksCmd(args cli.Args, recursive bool) {
// Get alias/bucket/prefix argument
targetURL := args.First()
// Fetch all policies associated to the passed url
policies, err := doGetAccessRules(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)
isRecursive := recursive
isIncomplete := false
// Iterate over policy rules to fetch public urls, then search
// for objects under those urls
for k, v := range policies {
// Trim the asterisk in policy rules
policyPath := strings.TrimSuffix(k, "*")
// Check if current policy prefix is related to the url passed by the user
if !strings.HasPrefix(policyPath, path) {
continue
}
// Check if the found policy has read permission
perm := stringToAccessPerm(v)
if perm != accessDownload && perm != accessPublic {
continue
}
// Construct the new path to search for public objects
newURL := alias + "/" + policyPath
clnt, err := newClient(newURL)
fatalIf(err.Trace(newURL), "Unable to initialize target `"+targetURL+"`.")
// Search for public objects
for content := range clnt.List(isRecursive, isIncomplete, DirFirst) {
if content.Err != nil {
errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
continue
}
if content.Type.IsDir() && isRecursive {
continue
}
// Encode public URL
u, e := url.Parse(content.URL.String())
errorIf(probe.NewError(e), "Unable to parse url `"+content.URL.String()+"`.")
publicURL := u.String()
// Construct the message to be displayed to the user
msg := policyLinksMessage{
Status: "success",
URL: publicURL,
}
// Print the found object
printMsg(msg)
}
}
}
// Run policy cmd to fetch set permission
func runPolicyCmd(args cli.Args) {
var operation, policyStr string
var probeErr *probe.Error
perms := accessPerms(args.Get(1))
targetURL := args.Get(2)
if perms.isValidAccessPERM() {
probeErr = doSetAccess(targetURL, perms)
operation = "set"
} else if perms.isValidAccessFile() {
probeErr = doSetAccessJSON(targetURL, perms)
operation = "set-json"
} else {
targetURL = args.Get(1)
operation = "get"
if args.First() == "get-json" {
operation = "get-json"
}
perms, policyStr, probeErr = doGetAccess(targetURL)
}
// Upon error exit.
if probeErr != nil {
switch probeErr.ToGoError().(type) {
case APINotImplemented:
fatalIf(probeErr.Trace(), "Unable to "+operation+" policy of a non S3 url `"+targetURL+"`.")
default:
fatalIf(probeErr.Trace(targetURL, string(perms)),
"Unable to "+operation+" policy `"+string(perms)+"` for `"+targetURL+"`.")
}
}
policyJSON := map[string]interface{}{}
if policyStr != "" {
e := json.Unmarshal([]byte(policyStr), &policyJSON)
fatalIf(probe.NewError(e), "Cannot unmarshal custom policy file.")
}
printMsg(policyMessage{
Status: "success",
Operation: operation,
Bucket: targetURL,
Perms: perms,
Policy: policyJSON,
})
}
func mainPolicy(ctx *cli.Context) error {
// check 'policy' cli arguments.
checkPolicySyntax(ctx)
// Additional command speific theme customization.
console.SetColor("Policy", color.New(color.FgGreen, color.Bold))
switch ctx.Args().First() {
case "set", "set-json", "get", "get-json":
// policy set [download|upload|public|none] alias/bucket/prefix
// policy set-json path-to-policy-json-file alias/bucket/prefix
// policy get alias/bucket/prefix
// policy get-json alias/bucket/prefix
runPolicyCmd(ctx.Args())
case "list":
// policy list alias/bucket/prefix
runPolicyListCmd(ctx.Args().Tail())
case "links":
// policy links alias/bucket/prefix
runPolicyLinksCmd(ctx.Args().Tail(), ctx.Bool("recursive"))
default:
// Shows command example and exit
cli.ShowCommandHelpAndExit(ctx, "policy", 1)
}
return nil
}