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