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  }