18600: Add more invalid splice request tests.
[arvados.git] / lib / controller / localdb / collection.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package localdb
6
7 import (
8         "context"
9         "fmt"
10         "net/http"
11         "os"
12         "sort"
13         "strings"
14         "time"
15
16         "git.arvados.org/arvados.git/sdk/go/arvados"
17         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
18         "git.arvados.org/arvados.git/sdk/go/auth"
19         "git.arvados.org/arvados.git/sdk/go/httpserver"
20 )
21
22 // CollectionGet defers to railsProxy for everything except blob
23 // signatures.
24 func (conn *Conn) CollectionGet(ctx context.Context, opts arvados.GetOptions) (arvados.Collection, error) {
25         if len(opts.Select) > 0 {
26                 // We need to know IsTrashed and TrashAt to implement
27                 // signing properly, even if the caller doesn't want
28                 // them.
29                 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
30         }
31         resp, err := conn.railsProxy.CollectionGet(ctx, opts)
32         if err != nil {
33                 return resp, err
34         }
35         conn.signCollection(ctx, &resp)
36         return resp, nil
37 }
38
39 // CollectionList defers to railsProxy for everything except blob
40 // signatures.
41 func (conn *Conn) CollectionList(ctx context.Context, opts arvados.ListOptions) (arvados.CollectionList, error) {
42         if len(opts.Select) > 0 {
43                 // We need to know IsTrashed and TrashAt to implement
44                 // signing properly, even if the caller doesn't want
45                 // them.
46                 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
47         }
48         resp, err := conn.railsProxy.CollectionList(ctx, opts)
49         if err != nil {
50                 return resp, err
51         }
52         for i := range resp.Items {
53                 conn.signCollection(ctx, &resp.Items[i])
54         }
55         return resp, nil
56 }
57
58 // CollectionCreate defers to railsProxy for everything except blob
59 // signatures and vocabulary checking.
60 func (conn *Conn) CollectionCreate(ctx context.Context, opts arvados.CreateOptions) (arvados.Collection, error) {
61         err := conn.checkProperties(ctx, opts.Attrs["properties"])
62         if err != nil {
63                 return arvados.Collection{}, err
64         }
65         if len(opts.Select) > 0 {
66                 // We need to know IsTrashed and TrashAt to implement
67                 // signing properly, even if the caller doesn't want
68                 // them.
69                 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
70         }
71         if err := conn.applySplices(ctx, "", opts.Attrs); err != nil {
72                 return arvados.Collection{}, err
73         }
74         resp, err := conn.railsProxy.CollectionCreate(ctx, opts)
75         if err != nil {
76                 return resp, err
77         }
78         conn.signCollection(ctx, &resp)
79         return resp, nil
80 }
81
82 // CollectionUpdate defers to railsProxy for everything except blob
83 // signatures and vocabulary checking.
84 func (conn *Conn) CollectionUpdate(ctx context.Context, opts arvados.UpdateOptions) (arvados.Collection, error) {
85         err := conn.checkProperties(ctx, opts.Attrs["properties"])
86         if err != nil {
87                 return arvados.Collection{}, err
88         }
89         if len(opts.Select) > 0 {
90                 // We need to know IsTrashed and TrashAt to implement
91                 // signing properly, even if the caller doesn't want
92                 // them.
93                 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
94         }
95         if err := conn.applySplices(ctx, opts.UUID, opts.Attrs); err != nil {
96                 return arvados.Collection{}, err
97         }
98         resp, err := conn.railsProxy.CollectionUpdate(ctx, opts)
99         if err != nil {
100                 return resp, err
101         }
102         conn.signCollection(ctx, &resp)
103         return resp, nil
104 }
105
106 func (conn *Conn) signCollection(ctx context.Context, coll *arvados.Collection) {
107         if coll.IsTrashed || coll.ManifestText == "" || !conn.cluster.Collections.BlobSigning {
108                 return
109         }
110         var token string
111         if creds, ok := auth.FromContext(ctx); ok && len(creds.Tokens) > 0 {
112                 token = creds.Tokens[0]
113         }
114         if token == "" {
115                 return
116         }
117         ttl := conn.cluster.Collections.BlobSigningTTL.Duration()
118         exp := time.Now().Add(ttl)
119         if coll.TrashAt != nil && !coll.TrashAt.IsZero() && coll.TrashAt.Before(exp) {
120                 exp = *coll.TrashAt
121         }
122         coll.ManifestText = arvados.SignManifest(coll.ManifestText, token, exp, ttl, []byte(conn.cluster.Collections.BlobSigningKey))
123 }
124
125 // If attrs["splices"] is present, populate attrs["manifest_text"] by
126 // starting with the content of fromUUID (or an empty collection if
127 // fromUUID is empty) and applying the specified splice operations.
128 func (conn *Conn) applySplices(ctx context.Context, fromUUID string, attrs map[string]interface{}) error {
129         var splices map[string]string
130
131         // Validate the incoming attrs, and return early if the
132         // request doesn't ask for any splices.
133         if sp, ok := attrs["splices"]; !ok {
134                 return nil
135         } else {
136                 switch sp := sp.(type) {
137                 default:
138                         return httpserver.Errorf(http.StatusBadRequest, "invalid type %T for splices parameter", sp)
139                 case nil:
140                         return nil
141                 case map[string]string:
142                         splices = sp
143                 case map[string]interface{}:
144                         splices = make(map[string]string, len(sp))
145                         for dst, src := range sp {
146                                 if src, ok := src.(string); ok {
147                                         splices[dst] = src
148                                 } else {
149                                         return httpserver.Errorf(http.StatusBadRequest, "invalid source type for splice target %q: %v", dst, src)
150                                 }
151                         }
152                 }
153                 if len(splices) == 0 {
154                         return nil
155                 } else if mtxt, ok := attrs["manifest_text"].(string); ok && len(mtxt) > 0 {
156                         return httpserver.Errorf(http.StatusBadRequest, "ambiguous request: both 'splices' and 'manifest_text' values provided")
157                 }
158         }
159
160         // Load the current collection (if any) and set up an
161         // in-memory filesystem.
162         var dst arvados.Collection
163         if _, rootsplice := splices["/"]; !rootsplice && fromUUID != "" {
164                 src, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: fromUUID})
165                 if err != nil {
166                         return err
167                 }
168                 dst = src
169         }
170         dstfs, err := dst.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
171         if err != nil {
172                 return err
173         }
174
175         // Sort splices by source collection to avoid redundant
176         // reloads when a source collection is used more than
177         // once. Note empty sources (which mean "delete target path")
178         // sort first.
179         dstTodo := make([]string, 0, len(splices))
180         {
181                 srcid := make(map[string]string, len(splices))
182                 for dst, src := range splices {
183                         dstTodo = append(dstTodo, dst)
184                         if i := strings.IndexRune(src, '/'); i > 0 {
185                                 srcid[dst] = src[:i]
186                         }
187                 }
188                 sort.Slice(dstTodo, func(i, j int) bool {
189                         return srcid[dstTodo[i]] < srcid[dstTodo[j]]
190                 })
191         }
192
193         // Reject attempt to splice a node as well as its descendant
194         // (e.g., a/ and a/b/), which is unsupported, except where the
195         // source for a/ is empty (i.e., delete).
196         for _, dst := range dstTodo {
197                 if dst != "/" && (strings.HasSuffix(dst, "/") ||
198                         strings.HasSuffix(dst, "/.") ||
199                         strings.HasSuffix(dst, "/..") ||
200                         strings.Contains(dst, "//") ||
201                         strings.Contains(dst, "/./") ||
202                         strings.Contains(dst, "/../") ||
203                         !strings.HasPrefix(dst, "/")) {
204                         return httpserver.Errorf(http.StatusBadRequest, "invalid splice target: %q", dst)
205                 }
206                 for i := 0; i < len(dst)-1; i++ {
207                         if dst[i] != '/' {
208                                 continue
209                         }
210                         outerdst := dst[:i]
211                         if outerdst == "" {
212                                 outerdst = "/"
213                         }
214                         if outersrc := splices[outerdst]; outersrc != "" {
215                                 return httpserver.Errorf(http.StatusBadRequest, "cannot splice at target %q with non-empty splice at %q", dst, outerdst)
216                         }
217                 }
218         }
219
220         var srcidloaded string
221         var srcfs arvados.FileSystem
222         // Apply the requested splices.
223         for _, dst := range dstTodo {
224                 src := splices[dst]
225                 if src == "" {
226                         if dst == "/" {
227                                 // In this case we started with a
228                                 // blank manifest, so there can't be
229                                 // anything to delete.
230                                 continue
231                         }
232                         err := dstfs.RemoveAll(dst)
233                         if err != nil {
234                                 return fmt.Errorf("RemoveAll(%s): %w", dst, err)
235                         }
236                         continue
237                 }
238                 srcspec := strings.SplitN(src, "/", 2)
239                 srcid, srcpath := srcspec[0], "/"
240                 if !arvadosclient.PDHMatch(srcid) {
241                         return httpserver.Errorf(http.StatusBadRequest, "invalid source %q for splices[%q]: must be \"\" or \"PDH\" or \"PDH/path\"", src, dst)
242                 }
243                 if len(srcspec) == 2 && srcspec[1] != "" {
244                         srcpath = srcspec[1]
245                 }
246                 if srcidloaded != srcid {
247                         srcfs = nil
248                         srccoll, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: srcid})
249                         if err != nil {
250                                 return err
251                         }
252                         // We use StubClient here because we don't
253                         // want srcfs to read/write any file data or
254                         // sync collection state to/from the database.
255                         srcfs, err = srccoll.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
256                         if err != nil {
257                                 return err
258                         }
259                         srcidloaded = srcid
260                 }
261                 snap, err := arvados.Snapshot(srcfs, srcpath)
262                 if err != nil {
263                         return httpserver.Errorf(http.StatusBadRequest, "error getting snapshot of %q from %q: %w", srcpath, srcid, err)
264                 }
265                 // Create intermediate dirs, in case dst is
266                 // "newdir1/newdir2/dst".
267                 for i := 1; i < len(dst)-1; i++ {
268                         if dst[i] == '/' {
269                                 err = dstfs.Mkdir(dst[:i], 0777)
270                                 if err != nil && !os.IsExist(err) {
271                                         return httpserver.Errorf(http.StatusBadRequest, "error creating parent dirs for %q: %w", dst, err)
272                                 }
273                         }
274                 }
275                 err = arvados.Splice(dstfs, dst, snap)
276                 if err != nil {
277                         return fmt.Errorf("error splicing snapshot onto path %q: %w", dst, err)
278                 }
279         }
280         mtxt, err := dstfs.MarshalManifest(".")
281         if err != nil {
282                 return err
283         }
284         delete(attrs, "splices")
285         attrs["manifest_text"] = mtxt
286         return nil
287 }