1// Copyright (c) 2015-2021 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 18package cmd 19 20import ( 21 "context" 22 "fmt" 23 "path/filepath" 24 "strings" 25 "time" 26 27 "github.com/fatih/color" 28 "github.com/minio/cli" 29 "github.com/minio/minio-go/v7" 30 "github.com/minio/pkg/console" 31) 32 33var ( 34 lhSetFlags = []cli.Flag{ 35 cli.BoolFlag{ 36 Name: "recursive, r", 37 Usage: "apply legal hold recursively", 38 }, 39 cli.StringFlag{ 40 Name: "version-id, vid", 41 Usage: "apply legal hold to a specific object version", 42 }, 43 cli.StringFlag{ 44 Name: "rewind", 45 Usage: "apply legal hold on an object version at specified time", 46 }, 47 cli.BoolFlag{ 48 Name: "versions", 49 Usage: "apply legal hold on multiple versions of an object", 50 }, 51 } 52) 53var legalHoldSetCmd = cli.Command{ 54 Name: "set", 55 Usage: "set legal hold for object(s)", 56 Action: mainLegalHoldSet, 57 OnUsageError: onUsageError, 58 Before: setGlobalsFromContext, 59 Flags: append(lhSetFlags, globalFlags...), 60 CustomHelpTemplate: `NAME: 61 {{.HelpName}} - {{.Usage}} 62 63USAGE: 64 {{.HelpName}} [FLAGS] TARGET 65 66FLAGS: 67 {{range .VisibleFlags}}{{.}} 68 {{end}} 69 70EXAMPLES: 71 1. Enable legal hold on a specific object 72 $ {{.HelpName}} myminio/mybucket/prefix/obj.csv 73 74 2. Enable legal hold on a specific object version 75 $ {{.HelpName}} myminio/mybucket/prefix/obj.csv --version-id "HiMFUTOowG6ylfNi4LKxD3ieHbgfgrvC" 76 77 3. Enable object legal hold recursively for all objects at a prefix 78 $ {{.HelpName}} myminio/mybucket/prefix --recursive 79 80 4. Enable object legal hold recursively for all objects versions older than one year 81 $ {{.HelpName}} myminio/mybucket/prefix --recursive --rewind 365d --versions 82`, 83} 84 85// setLegalHold - Set legalhold for all objects within a given prefix. 86func setLegalHold(ctx context.Context, urlStr, versionID string, timeRef time.Time, withOlderVersions, recursive bool, lhold minio.LegalHoldStatus) error { 87 88 clnt, err := newClient(urlStr) 89 if err != nil { 90 fatalIf(err.Trace(), "Unable to parse the provided url.") 91 } 92 93 prefixPath := clnt.GetURL().Path 94 prefixPath = filepath.ToSlash(prefixPath) 95 if !strings.HasSuffix(prefixPath, "/") { 96 prefixPath = prefixPath[:strings.LastIndex(prefixPath, "/")+1] 97 } 98 prefixPath = strings.TrimPrefix(prefixPath, "./") 99 100 if !recursive && !withOlderVersions { 101 err = clnt.PutObjectLegalHold(ctx, versionID, lhold) 102 if err != nil { 103 errorIf(err.Trace(urlStr), "Failed to set legal hold on `"+urlStr+"` successfully") 104 } else { 105 contentURL := filepath.ToSlash(clnt.GetURL().Path) 106 key := strings.TrimPrefix(contentURL, prefixPath) 107 108 printMsg(legalHoldCmdMessage{ 109 LegalHold: lhold, 110 Status: "success", 111 URLPath: clnt.GetURL().String(), 112 Key: key, 113 VersionID: versionID, 114 }) 115 } 116 return nil 117 } 118 119 alias, _, _ := mustExpandAlias(urlStr) 120 var cErr error 121 objectsFound := false 122 lstOptions := ListOptions{Recursive: recursive, ShowDir: DirNone} 123 if !timeRef.IsZero() { 124 lstOptions.WithOlderVersions = withOlderVersions 125 lstOptions.TimeRef = timeRef 126 } 127 for content := range clnt.List(ctx, lstOptions) { 128 if content.Err != nil { 129 errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.") 130 cErr = exitStatus(globalErrorExitStatus) // Set the exit status. 131 continue 132 } 133 134 if !recursive && alias+getKey(content) != getStandardizedURL(urlStr) { 135 break 136 } 137 138 objectsFound = true 139 140 newClnt, perr := newClientFromAlias(alias, content.URL.String()) 141 if perr != nil { 142 errorIf(content.Err.Trace(clnt.GetURL().String()), "Invalid URL") 143 continue 144 } 145 146 probeErr := newClnt.PutObjectLegalHold(ctx, content.VersionID, lhold) 147 if probeErr != nil { 148 errorIf(probeErr.Trace(content.URL.Path), "Failed to set legal hold on `"+content.URL.Path+"` successfully") 149 } else { 150 if !globalJSON { 151 contentURL := filepath.ToSlash(content.URL.Path) 152 key := strings.TrimPrefix(contentURL, prefixPath) 153 154 printMsg(legalHoldCmdMessage{ 155 LegalHold: lhold, 156 Status: "success", 157 URLPath: content.URL.String(), 158 Key: key, 159 VersionID: content.VersionID, 160 }) 161 } 162 } 163 } 164 165 if cErr == nil && !globalJSON { 166 if !objectsFound { 167 console.Print(console.Colorize("LegalHoldMessageFailure", 168 fmt.Sprintf("No objects/versions found while setting legal hold on `%s`. \n", urlStr))) 169 } 170 } 171 return cErr 172} 173 174// Validate command line arguments. 175func parseLegalHoldArgs(cliCtx *cli.Context) (targetURL, versionID string, timeRef time.Time, recursive, withVersions bool) { 176 args := cliCtx.Args() 177 if len(args) != 1 { 178 cli.ShowCommandHelpAndExit(cliCtx, cliCtx.Command.Name, 1) 179 } 180 181 targetURL = args[0] 182 if targetURL == "" { 183 fatalIf(errInvalidArgument(), "You cannot pass an empty target url.") 184 } 185 186 versionID = cliCtx.String("version-id") 187 recursive = cliCtx.Bool("recursive") 188 withVersions = cliCtx.Bool("versions") 189 rewind := cliCtx.String("rewind") 190 191 if versionID != "" && (recursive || withVersions || rewind != "") { 192 fatalIf(errInvalidArgument(), "You cannot pass --version-id with any of --versions, --recursive and --rewind flags.") 193 } 194 195 timeRef = parseRewindFlag(rewind) 196 return 197} 198 199// main for legalhold set command. 200func mainLegalHoldSet(cliCtx *cli.Context) error { 201 console.SetColor("LegalHoldSuccess", color.New(color.FgGreen, color.Bold)) 202 console.SetColor("LegalHoldFailure", color.New(color.FgRed, color.Bold)) 203 console.SetColor("LegalHoldPartialFailure", color.New(color.FgRed, color.Bold)) 204 console.SetColor("LegalHoldMessageFailure", color.New(color.FgYellow)) 205 206 targetURL, versionID, timeRef, recursive, withVersions := parseLegalHoldArgs(cliCtx) 207 if timeRef.IsZero() && withVersions { 208 timeRef = time.Now().UTC() 209 } 210 211 ctx, cancelLegalHold := context.WithCancel(globalContext) 212 defer cancelLegalHold() 213 214 enabled, err := isBucketLockEnabled(ctx, targetURL) 215 if err != nil { 216 fatalIf(err, "Unable to set legalhold on `%s`", targetURL) 217 } 218 if !enabled { 219 fatalIf(errDummy().Trace(), "Bucket lock needs to be enabled in order to use this feature.") 220 } 221 222 return setLegalHold(ctx, targetURL, versionID, timeRef, withVersions, recursive, minio.LegalHoldEnabled) 223} 224