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 }