github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/proppatch.go (about) 1 // Copyright 2018-2021 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package ocdav 20 21 import ( 22 "bytes" 23 "context" 24 "encoding/xml" 25 "fmt" 26 "io" 27 "net/http" 28 "path" 29 "strings" 30 31 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 32 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 33 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors" 34 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" 35 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/prop" 36 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/propfind" 37 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/spacelookup" 38 "github.com/cs3org/reva/v2/pkg/appctx" 39 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 40 "github.com/cs3org/reva/v2/pkg/errtypes" 41 "github.com/cs3org/reva/v2/pkg/permission" 42 rstatus "github.com/cs3org/reva/v2/pkg/rgrpc/status" 43 "github.com/cs3org/reva/v2/pkg/utils" 44 "github.com/rs/zerolog" 45 ) 46 47 func (s *svc) handlePathProppatch(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) { 48 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "proppatch") 49 defer span.End() 50 51 fn := path.Join(ns, r.URL.Path) 52 53 sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() 54 55 pp, status, err := readProppatch(r.Body) 56 if err != nil { 57 return status, err 58 } 59 60 space, rpcStatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn) 61 switch { 62 case err != nil: 63 return http.StatusInternalServerError, err 64 case rpcStatus.Code == rpc.Code_CODE_ABORTED: 65 return http.StatusPreconditionFailed, errtypes.NewErrtypeFromStatus(rpcStatus) 66 case rpcStatus.Code != rpc.Code_CODE_OK: 67 return rstatus.HTTPStatusFromCode(rpcStatus.Code), errtypes.NewErrtypeFromStatus(rpcStatus) 68 } 69 70 client, err := s.gatewaySelector.Next() 71 if err != nil { 72 return http.StatusInternalServerError, errtypes.InternalError(err.Error()) 73 } 74 // check if resource exists 75 statReq := &provider.StatRequest{Ref: spacelookup.MakeRelativeReference(space, fn, false)} 76 statRes, err := client.Stat(ctx, statReq) 77 switch { 78 case err != nil: 79 return http.StatusInternalServerError, err 80 case statRes.Status.Code == rpc.Code_CODE_ABORTED: 81 return http.StatusPreconditionFailed, errtypes.NewErrtypeFromStatus(statRes.Status) 82 case statRes.Status.Code != rpc.Code_CODE_OK: 83 return rstatus.HTTPStatusFromCode(rpcStatus.Code), errtypes.NewErrtypeFromStatus(statRes.Status) 84 } 85 86 acceptedProps, removedProps, ok := s.handleProppatch(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false), pp, sublog) 87 if !ok { 88 // handleProppatch handles responses in error cases so return 0 89 return 0, nil 90 } 91 92 nRef := strings.TrimPrefix(fn, ns) 93 nRef = path.Join(ctx.Value(net.CtxKeyBaseURI).(string), nRef) 94 if statRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { 95 nRef += "/" 96 } 97 98 s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog) 99 return 0, nil 100 } 101 102 func (s *svc) handleSpacesProppatch(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) { 103 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_proppatch") 104 defer span.End() 105 106 sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Logger() 107 108 pp, status, err := readProppatch(r.Body) 109 if err != nil { 110 return status, err 111 } 112 113 ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path) 114 if err != nil { 115 return http.StatusBadRequest, err 116 } 117 118 acceptedProps, removedProps, ok := s.handleProppatch(ctx, w, r, &ref, pp, sublog) 119 if !ok { 120 // handleProppatch handles responses in error cases so return 0 121 return 0, nil 122 } 123 124 nRef := path.Join(spaceID, r.URL.Path) 125 nRef = path.Join(ctx.Value(net.CtxKeyBaseURI).(string), nRef) 126 127 s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog) 128 return 0, nil 129 } 130 131 func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, patches []Proppatch, log zerolog.Logger) ([]xml.Name, []xml.Name, bool) { 132 133 rreq := &provider.UnsetArbitraryMetadataRequest{ 134 Ref: ref, 135 ArbitraryMetadataKeys: []string{""}, 136 LockId: requestLockToken(r), 137 } 138 sreq := &provider.SetArbitraryMetadataRequest{ 139 Ref: ref, 140 ArbitraryMetadata: &provider.ArbitraryMetadata{ 141 Metadata: map[string]string{}, 142 }, 143 LockId: requestLockToken(r), 144 } 145 146 acceptedProps := []xml.Name{} 147 removedProps := []xml.Name{} 148 149 client, err := s.gatewaySelector.Next() 150 if err != nil { 151 log.Error().Err(err).Msg("error selecting next gateway client") 152 w.WriteHeader(http.StatusInternalServerError) 153 return nil, nil, false 154 } 155 for i := range patches { 156 if len(patches[i].Props) < 1 { 157 continue 158 } 159 for j := range patches[i].Props { 160 propNameXML := patches[i].Props[j].XMLName 161 // don't use path.Join. It removes the double slash! concatenate with a / 162 key := fmt.Sprintf("%s/%s", patches[i].Props[j].XMLName.Space, patches[i].Props[j].XMLName.Local) 163 value := string(patches[i].Props[j].InnerXML) 164 remove := patches[i].Remove 165 // boolean flags may be "set" to false as well 166 if s.isBooleanProperty(key) { 167 // Make boolean properties either "0" or "1" 168 value = s.as0or1(value) 169 if value == "0" { 170 remove = true 171 } 172 } 173 // Webdav spec requires the operations to be executed in the order 174 // specified in the PROPPATCH request 175 // http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2 176 // FIXME: batch this somehow 177 if remove { 178 rreq.ArbitraryMetadataKeys[0] = key 179 res, err := client.UnsetArbitraryMetadata(ctx, rreq) 180 if err != nil { 181 log.Error().Err(err).Msg("error sending a grpc UnsetArbitraryMetadata request") 182 w.WriteHeader(http.StatusInternalServerError) 183 return nil, nil, false 184 } 185 186 if res.Status.Code != rpc.Code_CODE_OK { 187 status := rstatus.HTTPStatusFromCode(res.Status.Code) 188 if res.Status.Code == rpc.Code_CODE_ABORTED { 189 // aborted is used for etag an lock mismatches, which translates to 412 190 // in case a real Conflict response is needed, the calling code needs to send the header 191 status = http.StatusPreconditionFailed 192 } 193 m := res.Status.Message 194 if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { 195 // check if user has access to resource 196 sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref}) 197 if err != nil { 198 log.Error().Err(err).Msg("error performing stat grpc request") 199 w.WriteHeader(http.StatusInternalServerError) 200 return nil, nil, false 201 } 202 if sRes.Status.Code != rpc.Code_CODE_OK { 203 // return not found error so we do not leak existence of a file 204 // TODO hide permission failed for users without access in every kind of request 205 // TODO should this be done in the driver? 206 status = http.StatusNotFound 207 } 208 } 209 if status == http.StatusNotFound { 210 m = "Resource not found" // mimic the oc10 error message 211 } 212 w.WriteHeader(status) 213 b, err := errors.Marshal(status, m, "", "") 214 errors.HandleWebdavError(&log, w, b, err) 215 return nil, nil, false 216 } 217 if key == "http://owncloud.org/ns/favorite" { 218 statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref}) 219 if err != nil { 220 w.WriteHeader(http.StatusInternalServerError) 221 return nil, nil, false 222 } 223 currentUser := ctxpkg.ContextMustGetUser(ctx) 224 ok, err := utils.CheckPermission(ctx, permission.WriteFavorites, client) 225 if err != nil { 226 log.Error().Err(err).Msg("error checking permission") 227 w.WriteHeader(http.StatusInternalServerError) 228 return nil, nil, false 229 } 230 if !ok { 231 log.Info().Interface("user", currentUser).Msg("user not allowed to unset favorite") 232 w.WriteHeader(http.StatusForbidden) 233 return nil, nil, false 234 } 235 err = s.favoritesManager.UnsetFavorite(ctx, currentUser.Id, statRes.Info) 236 if err != nil { 237 w.WriteHeader(http.StatusInternalServerError) 238 return nil, nil, false 239 } 240 } 241 removedProps = append(removedProps, propNameXML) 242 } else { 243 sreq.ArbitraryMetadata.Metadata[key] = value 244 res, err := client.SetArbitraryMetadata(ctx, sreq) 245 if err != nil { 246 log.Error().Err(err).Str("key", key).Str("value", value).Msg("error sending a grpc SetArbitraryMetadata request") 247 w.WriteHeader(http.StatusInternalServerError) 248 return nil, nil, false 249 } 250 251 if res.Status.Code != rpc.Code_CODE_OK { 252 status := rstatus.HTTPStatusFromCode(res.Status.Code) 253 if res.Status.Code == rpc.Code_CODE_ABORTED { 254 // aborted is used for etag an lock mismatches, which translates to 412 255 // in case a real Conflict response is needed, the calling code needs to send the header 256 status = http.StatusPreconditionFailed 257 } 258 m := res.Status.Message 259 if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { 260 // check if user has access to resource 261 sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref}) 262 if err != nil { 263 log.Error().Err(err).Msg("error performing stat grpc request") 264 w.WriteHeader(http.StatusInternalServerError) 265 return nil, nil, false 266 } 267 if sRes.Status.Code != rpc.Code_CODE_OK { 268 // return not found error so we don't leak existence of a file 269 // TODO hide permission failed for users without access in every kind of request 270 // TODO should this be done in the driver? 271 status = http.StatusNotFound 272 } 273 } 274 if status == http.StatusNotFound { 275 m = "Resource not found" // mimic the oc10 error message 276 } 277 w.WriteHeader(status) 278 b, err := errors.Marshal(status, m, "", "") 279 errors.HandleWebdavError(&log, w, b, err) 280 return nil, nil, false 281 } 282 283 acceptedProps = append(acceptedProps, propNameXML) 284 delete(sreq.ArbitraryMetadata.Metadata, key) 285 286 if key == "http://owncloud.org/ns/favorite" { 287 statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref}) 288 if err != nil || statRes.Info == nil { 289 w.WriteHeader(http.StatusInternalServerError) 290 return nil, nil, false 291 } 292 currentUser := ctxpkg.ContextMustGetUser(ctx) 293 ok, err := utils.CheckPermission(ctx, permission.WriteFavorites, client) 294 if err != nil { 295 log.Error().Err(err).Msg("error checking permission") 296 w.WriteHeader(http.StatusInternalServerError) 297 return nil, nil, false 298 } 299 if !ok { 300 log.Info().Interface("user", currentUser).Msg("user not allowed to set favorite") 301 w.WriteHeader(http.StatusForbidden) 302 return nil, nil, false 303 } 304 err = s.favoritesManager.SetFavorite(ctx, currentUser.Id, statRes.Info) 305 if err != nil { 306 w.WriteHeader(http.StatusInternalServerError) 307 return nil, nil, false 308 } 309 } 310 } 311 } 312 // FIXME: in case of error, need to set all properties back to the original state, 313 // and return the error in the matching propstat block, if applicable 314 // http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2 315 } 316 317 return acceptedProps, removedProps, true 318 } 319 320 func (s *svc) handleProppatchResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, acceptedProps, removedProps []xml.Name, path string, log zerolog.Logger) { 321 propRes, err := s.formatProppatchResponse(ctx, acceptedProps, removedProps, path) 322 if err != nil { 323 log.Error().Err(err).Msg("error formatting proppatch response") 324 w.WriteHeader(http.StatusInternalServerError) 325 return 326 } 327 w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol") 328 w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8") 329 w.WriteHeader(http.StatusMultiStatus) 330 if _, err := w.Write(propRes); err != nil { 331 log.Err(err).Msg("error writing response") 332 } 333 } 334 335 func (s *svc) formatProppatchResponse(ctx context.Context, acceptedProps []xml.Name, removedProps []xml.Name, ref string) ([]byte, error) { 336 responses := make([]propfind.ResponseXML, 0, 1) 337 response := propfind.ResponseXML{ 338 Href: net.EncodePath(ref), 339 Propstat: []propfind.PropstatXML{}, 340 } 341 342 if len(acceptedProps) > 0 { 343 propstatBody := []prop.PropertyXML{} 344 for i := range acceptedProps { 345 propstatBody = append(propstatBody, prop.EscapedNS(acceptedProps[i].Space, acceptedProps[i].Local, "")) 346 } 347 response.Propstat = append(response.Propstat, propfind.PropstatXML{ 348 Status: "HTTP/1.1 200 OK", 349 Prop: propstatBody, 350 }) 351 } 352 353 if len(removedProps) > 0 { 354 propstatBody := []prop.PropertyXML{} 355 for i := range removedProps { 356 propstatBody = append(propstatBody, prop.EscapedNS(removedProps[i].Space, removedProps[i].Local, "")) 357 } 358 response.Propstat = append(response.Propstat, propfind.PropstatXML{ 359 Status: "HTTP/1.1 204 No Content", 360 Prop: propstatBody, 361 }) 362 } 363 364 responses = append(responses, response) 365 responsesXML, err := xml.Marshal(&responses) 366 if err != nil { 367 return nil, err 368 } 369 370 var buf bytes.Buffer 371 buf.WriteString(`<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:" `) 372 buf.WriteString(`xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">`) 373 buf.Write(responsesXML) 374 buf.WriteString(`</d:multistatus>`) 375 return buf.Bytes(), nil 376 } 377 378 func (s *svc) isBooleanProperty(prop string) bool { 379 // TODO add other properties we know to be boolean? 380 return prop == net.PropOcFavorite 381 } 382 383 func (s *svc) as0or1(val string) string { 384 switch strings.TrimSpace(val) { 385 case "false": 386 return "0" 387 case "": 388 return "0" 389 case "0": 390 return "0" 391 case "no": 392 return "0" 393 case "off": 394 return "0" 395 } 396 return "1" 397 } 398 399 // Proppatch describes a property update instruction as defined in RFC 4918. 400 // See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH 401 type Proppatch struct { 402 // Remove specifies whether this patch removes properties. If it does not 403 // remove them, it sets them. 404 Remove bool 405 // Props contains the properties to be set or removed. 406 Props []prop.PropertyXML 407 } 408 409 // http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for proppatch) 410 type proppatchProps []prop.PropertyXML 411 412 // UnmarshalXML appends the property names and values enclosed within start 413 // to ps. 414 // 415 // An xml:lang attribute that is defined either on the DAV:prop or property 416 // name XML element is propagated to the property's Lang field. 417 // 418 // UnmarshalXML returns an error if start does not contain any properties or if 419 // property values contain syntactically incorrect XML. 420 func (ps *proppatchProps) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 421 lang := xmlLang(start, "") 422 for { 423 t, err := prop.Next(d) 424 if err != nil { 425 return err 426 } 427 switch elem := t.(type) { 428 case xml.EndElement: 429 if len(*ps) == 0 { 430 return fmt.Errorf("%s must not be empty", start.Name.Local) 431 } 432 return nil 433 case xml.StartElement: 434 p := prop.PropertyXML{} 435 err = d.DecodeElement(&p, &elem) 436 if err != nil { 437 return err 438 } 439 // special handling for the lang property 440 p.Lang = xmlLang(t.(xml.StartElement), lang) 441 *ps = append(*ps, p) 442 } 443 } 444 } 445 446 // http://www.webdav.org/specs/rfc4918.html#ELEMENT_set 447 // http://www.webdav.org/specs/rfc4918.html#ELEMENT_remove 448 type setRemove struct { 449 XMLName xml.Name 450 Lang string `xml:"xml:lang,attr,omitempty"` 451 Prop proppatchProps `xml:"DAV: prop"` 452 } 453 454 // http://www.webdav.org/specs/rfc4918.html#ELEMENT_propertyupdate 455 type propertyupdate struct { 456 XMLName xml.Name `xml:"DAV: propertyupdate"` 457 Lang string `xml:"xml:lang,attr,omitempty"` 458 SetRemove []setRemove `xml:",any"` 459 } 460 461 func readProppatch(r io.Reader) (patches []Proppatch, status int, err error) { 462 var pu propertyupdate 463 if err = xml.NewDecoder(r).Decode(&pu); err != nil { 464 return nil, http.StatusBadRequest, err 465 } 466 for _, op := range pu.SetRemove { 467 remove := false 468 switch op.XMLName { 469 case xml.Name{Space: net.NsDav, Local: "set"}: 470 // No-op. 471 case xml.Name{Space: net.NsDav, Local: "remove"}: 472 for _, p := range op.Prop { 473 if len(p.InnerXML) > 0 { 474 return nil, http.StatusBadRequest, errors.ErrInvalidProppatch 475 } 476 } 477 remove = true 478 default: 479 return nil, http.StatusBadRequest, errors.ErrInvalidProppatch 480 } 481 patches = append(patches, Proppatch{Remove: remove, Props: op.Prop}) 482 } 483 return patches, 0, nil 484 } 485 486 var xmlLangName = xml.Name{Space: "http://www.w3.org/XML/1998/namespace", Local: "lang"} 487 488 func xmlLang(s xml.StartElement, d string) string { 489 for _, attr := range s.Attr { 490 if attr.Name == xmlLangName { 491 return attr.Value 492 } 493 } 494 return d 495 }