github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/dav.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  	"context"
    23  	"fmt"
    24  	"net/http"
    25  	"path"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
    30  	userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    31  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    32  	link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
    33  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    34  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/config"
    35  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors"
    36  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net"
    37  	"github.com/cs3org/reva/v2/pkg/appctx"
    38  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    39  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    40  	"github.com/cs3org/reva/v2/pkg/rhttp/router"
    41  	"github.com/cs3org/reva/v2/pkg/storage/utils/grants"
    42  	"github.com/cs3org/reva/v2/pkg/storagespace"
    43  	"github.com/cs3org/reva/v2/pkg/utils"
    44  	"go.opentelemetry.io/otel/trace"
    45  	"google.golang.org/grpc/metadata"
    46  )
    47  
    48  const (
    49  	_trashbinPath = "trash-bin"
    50  
    51  	// WwwAuthenticate captures the Www-Authenticate header string.
    52  	WwwAuthenticate = "Www-Authenticate"
    53  )
    54  
    55  const (
    56  	ErrListingMembers     = "ERR_LISTING_MEMBERS_NOT_ALLOWED"
    57  	ErrInvalidCredentials = "ERR_INVALID_CREDENTIALS"
    58  	ErrMissingBasicAuth   = "ERR_MISSING_BASIC_AUTH"
    59  	ErrMissingBearerAuth  = "ERR_MISSING_BEARER_AUTH"
    60  	ErrFileNotFoundInRoot = "ERR_FILE_NOT_FOUND_IN_ROOT"
    61  )
    62  
    63  // DavHandler routes to the different sub handlers
    64  type DavHandler struct {
    65  	AvatarsHandler      *AvatarsHandler
    66  	FilesHandler        *WebDavHandler
    67  	FilesHomeHandler    *WebDavHandler
    68  	MetaHandler         *MetaHandler
    69  	TrashbinHandler     *TrashbinHandler
    70  	SpacesHandler       *SpacesHandler
    71  	PublicFolderHandler *WebDavHandler
    72  	PublicFileHandler   *PublicFileHandler
    73  	SharesHandler       *WebDavHandler
    74  	OCMSharesHandler    *WebDavHandler
    75  }
    76  
    77  func (h *DavHandler) init(c *config.Config) error {
    78  	h.AvatarsHandler = new(AvatarsHandler)
    79  	if err := h.AvatarsHandler.init(c); err != nil {
    80  		return err
    81  	}
    82  	h.FilesHandler = new(WebDavHandler)
    83  	if err := h.FilesHandler.init(c.FilesNamespace, false); err != nil {
    84  		return err
    85  	}
    86  	h.FilesHomeHandler = new(WebDavHandler)
    87  	if err := h.FilesHomeHandler.init(c.WebdavNamespace, true); err != nil {
    88  		return err
    89  	}
    90  	h.MetaHandler = new(MetaHandler)
    91  	if err := h.MetaHandler.init(c); err != nil {
    92  		return err
    93  	}
    94  	h.TrashbinHandler = new(TrashbinHandler)
    95  	if err := h.TrashbinHandler.init(c); err != nil {
    96  		return err
    97  	}
    98  
    99  	h.SpacesHandler = new(SpacesHandler)
   100  	if err := h.SpacesHandler.init(c); err != nil {
   101  		return err
   102  	}
   103  
   104  	h.PublicFolderHandler = new(WebDavHandler)
   105  	if err := h.PublicFolderHandler.init("public", true); err != nil { // jail public file requests to /public/ prefix
   106  		return err
   107  	}
   108  
   109  	h.PublicFileHandler = new(PublicFileHandler)
   110  	if err := h.PublicFileHandler.init("public"); err != nil { // jail public file requests to /public/ prefix
   111  		return err
   112  	}
   113  
   114  	h.OCMSharesHandler = new(WebDavHandler)
   115  	if err := h.OCMSharesHandler.init(c.OCMNamespace, true); err != nil {
   116  		return err
   117  	}
   118  
   119  	return nil
   120  }
   121  
   122  func isOwner(userIDorName string, user *userv1beta1.User) bool {
   123  	return userIDorName != "" && (userIDorName == user.Id.OpaqueId || strings.EqualFold(userIDorName, user.Username))
   124  }
   125  
   126  // Handler handles requests
   127  func (h *DavHandler) Handler(s *svc) http.Handler {
   128  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   129  		ctx := r.Context()
   130  		log := appctx.GetLogger(ctx)
   131  
   132  		// if there is no file in the request url we assume the request url is: "/remote.php/dav/files"
   133  		// https://github.com/owncloud/core/blob/18475dac812064b21dabcc50f25ef3ffe55691a5/tests/acceptance/features/apiWebdavOperations/propfind.feature
   134  		if r.URL.Path == "/files" {
   135  			log.Debug().Str("path", r.URL.Path).Msg("method not allowed")
   136  			contextUser, ok := ctxpkg.ContextGetUser(ctx)
   137  			if ok {
   138  				r.URL.Path = path.Join(r.URL.Path, contextUser.Username)
   139  			}
   140  
   141  			if r.Header.Get(net.HeaderDepth) == "" {
   142  				w.WriteHeader(http.StatusMethodNotAllowed)
   143  				b, err := errors.Marshal(http.StatusMethodNotAllowed, "Listing members of this collection is disabled", "", ErrListingMembers)
   144  				if err != nil {
   145  					log.Error().Msgf("error marshaling xml response: %s", b)
   146  					w.WriteHeader(http.StatusInternalServerError)
   147  					return
   148  				}
   149  				_, err = w.Write(b)
   150  				if err != nil {
   151  					log.Error().Msgf("error writing xml response: %s", b)
   152  					w.WriteHeader(http.StatusInternalServerError)
   153  					return
   154  				}
   155  				return
   156  			}
   157  		}
   158  
   159  		var head string
   160  		head, r.URL.Path = router.ShiftPath(r.URL.Path)
   161  
   162  		switch head {
   163  		case "avatars":
   164  			h.AvatarsHandler.Handler(s).ServeHTTP(w, r)
   165  		case "files":
   166  			var requestUserID string
   167  			var oldPath = r.URL.Path
   168  
   169  			// detect and check current user in URL
   170  			requestUserID, r.URL.Path = router.ShiftPath(r.URL.Path)
   171  
   172  			// note: some requests like OPTIONS don't forward the user
   173  			contextUser, ok := ctxpkg.ContextGetUser(ctx)
   174  			if ok && isOwner(requestUserID, contextUser) {
   175  				// use home storage handler when user was detected
   176  				base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "files", requestUserID)
   177  				ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
   178  				r = r.WithContext(ctx)
   179  
   180  				h.FilesHomeHandler.Handler(s).ServeHTTP(w, r)
   181  			} else {
   182  				r.URL.Path = oldPath
   183  				base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "files")
   184  				ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
   185  				r = r.WithContext(ctx)
   186  
   187  				h.FilesHandler.Handler(s).ServeHTTP(w, r)
   188  			}
   189  		case "meta":
   190  			base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "meta")
   191  			ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base)
   192  			r = r.WithContext(ctx)
   193  			h.MetaHandler.Handler(s).ServeHTTP(w, r)
   194  		case "ocm":
   195  			base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "ocm")
   196  			ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
   197  			c, err := s.gatewaySelector.Next()
   198  			if err != nil {
   199  				w.WriteHeader(http.StatusNotFound)
   200  				return
   201  			}
   202  
   203  			// OC10 and Nextcloud (OCM 1.0) are using basic auth for carrying the
   204  			// ocm share id.
   205  			var ocmshare, sharedSecret string
   206  			username, _, ok := r.BasicAuth()
   207  			if ok {
   208  				// OCM 1.0
   209  				ocmshare = username
   210  				sharedSecret = username
   211  				r.URL.Path = filepath.Join("/", ocmshare, r.URL.Path)
   212  			} else {
   213  				ocmshare, _ = router.ShiftPath(r.URL.Path)
   214  				sharedSecret = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
   215  			}
   216  
   217  			authRes, err := handleOCMAuth(ctx, c, ocmshare, sharedSecret)
   218  			switch {
   219  			case err != nil:
   220  				log.Error().Err(err).Msg("error during ocm authentication")
   221  				w.WriteHeader(http.StatusInternalServerError)
   222  				return
   223  			case authRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
   224  				log.Debug().Str("ocmshare", ocmshare).Msg("permission denied")
   225  				fallthrough
   226  			case authRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
   227  				log.Debug().Str("ocmshare", ocmshare).Msg("unauthorized")
   228  				w.WriteHeader(http.StatusUnauthorized)
   229  				return
   230  			case authRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
   231  				log.Debug().Str("ocmshare", ocmshare).Msg("not found")
   232  				w.WriteHeader(http.StatusNotFound)
   233  				return
   234  			case authRes.Status.Code != rpc.Code_CODE_OK:
   235  				log.Error().Str("ocmshare", ocmshare).Interface("status", authRes.Status).Msg("grpc auth request failed")
   236  				w.WriteHeader(http.StatusInternalServerError)
   237  				return
   238  			}
   239  
   240  			ctx = ctxpkg.ContextSetToken(ctx, authRes.Token)
   241  			ctx = ctxpkg.ContextSetUser(ctx, authRes.User)
   242  			ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, authRes.Token)
   243  
   244  			log.Debug().Str("ocmshare", ocmshare).Interface("user", authRes.User).Msg("OCM user authenticated")
   245  
   246  			r = r.WithContext(ctx)
   247  			h.OCMSharesHandler.Handler(s).ServeHTTP(w, r)
   248  		case "trash-bin":
   249  			base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "trash-bin")
   250  			ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
   251  			r = r.WithContext(ctx)
   252  			h.TrashbinHandler.Handler(s).ServeHTTP(w, r)
   253  		case "spaces":
   254  			base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "spaces")
   255  			ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
   256  			r = r.WithContext(ctx)
   257  			h.SpacesHandler.Handler(s, h.TrashbinHandler).ServeHTTP(w, r)
   258  		case "public-files":
   259  			base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "public-files")
   260  			ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base)
   261  
   262  			var res *gatewayv1beta1.AuthenticateResponse
   263  			token, _ := router.ShiftPath(r.URL.Path)
   264  			var hasValidBasicAuthHeader bool
   265  			var pass string
   266  			var err error
   267  			// If user is authenticated
   268  			_, userExists := ctxpkg.ContextGetUser(ctx)
   269  			if userExists {
   270  				client, err := s.gatewaySelector.Next()
   271  				if err != nil {
   272  					log.Error().Err(err).Msg("error sending grpc stat request")
   273  					w.WriteHeader(http.StatusInternalServerError)
   274  					return
   275  				}
   276  				psRes, err := client.GetPublicShare(ctx, &link.GetPublicShareRequest{
   277  					Ref: &link.PublicShareReference{
   278  						Spec: &link.PublicShareReference_Token{
   279  							Token: token,
   280  						},
   281  					}})
   282  				if err != nil && !strings.Contains(err.Error(), "core access token not found") {
   283  					log.Error().Err(err).Msg("error sending grpc stat request")
   284  					w.WriteHeader(http.StatusInternalServerError)
   285  					return
   286  				}
   287  				// If the link is internal then 307 redirect
   288  				if psRes.Status.Code == rpc.Code_CODE_OK && grants.PermissionsEqual(psRes.Share.Permissions.GetPermissions(), &provider.ResourcePermissions{}) {
   289  					if psRes.GetShare().GetResourceId() != nil {
   290  						rUrl := path.Join("/dav/spaces", storagespace.FormatResourceID(psRes.GetShare().GetResourceId()))
   291  						http.Redirect(w, r, rUrl, http.StatusTemporaryRedirect)
   292  						return
   293  					}
   294  					log.Debug().Str("token", token).Interface("status", psRes.Status).Msg("resource id not found")
   295  					w.WriteHeader(http.StatusNotFound)
   296  					return
   297  				}
   298  			}
   299  
   300  			if _, pass, hasValidBasicAuthHeader = r.BasicAuth(); hasValidBasicAuthHeader {
   301  				res, err = handleBasicAuth(r.Context(), s.gatewaySelector, token, pass)
   302  			} else {
   303  				q := r.URL.Query()
   304  				sig := q.Get("signature")
   305  				expiration := q.Get("expiration")
   306  				// We restrict the pre-signed urls to downloads.
   307  				if sig != "" && expiration != "" && !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
   308  					w.WriteHeader(http.StatusUnauthorized)
   309  					return
   310  				}
   311  				res, err = handleSignatureAuth(r.Context(), s.gatewaySelector, token, sig, expiration)
   312  			}
   313  
   314  			switch {
   315  			case err != nil:
   316  				w.WriteHeader(http.StatusInternalServerError)
   317  				return
   318  			case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
   319  				fallthrough
   320  			case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
   321  				w.WriteHeader(http.StatusUnauthorized)
   322  				if hasValidBasicAuthHeader {
   323  					b, err := errors.Marshal(http.StatusUnauthorized, "Username or password was incorrect", "", ErrInvalidCredentials)
   324  					errors.HandleWebdavError(log, w, b, err)
   325  					return
   326  				}
   327  				b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Basic' header found", "", ErrMissingBasicAuth)
   328  				errors.HandleWebdavError(log, w, b, err)
   329  				return
   330  			case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
   331  				w.WriteHeader(http.StatusNotFound)
   332  				return
   333  			case res.Status.Code != rpc.Code_CODE_OK:
   334  				w.WriteHeader(http.StatusInternalServerError)
   335  				return
   336  			}
   337  
   338  			if userExists {
   339  				// Build new context without an authenticated user.
   340  				// the public link should be resolved by the 'publicshares' authenticated user
   341  				baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
   342  				logger := appctx.GetLogger(ctx)
   343  				span := trace.SpanFromContext(ctx)
   344  				span.End()
   345  				ctx = trace.ContextWithSpan(context.Background(), span)
   346  				ctx = appctx.WithLogger(ctx, logger)
   347  				ctx = context.WithValue(ctx, net.CtxKeyBaseURI, baseURI)
   348  			}
   349  			ctx = ctxpkg.ContextSetToken(ctx, res.Token)
   350  			ctx = ctxpkg.ContextSetUser(ctx, res.User)
   351  			ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, res.Token)
   352  
   353  			r = r.WithContext(ctx)
   354  
   355  			// the public share manager knew the token, but does the referenced target still exist?
   356  			sRes, err := getTokenStatInfo(ctx, s.gatewaySelector, token)
   357  			switch {
   358  			case err != nil:
   359  				log.Error().Err(err).Msg("error sending grpc stat request")
   360  				w.WriteHeader(http.StatusInternalServerError)
   361  				return
   362  			case sRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
   363  				fallthrough
   364  			case sRes.Status.Code == rpc.Code_CODE_OK && grants.PermissionsEqual(sRes.GetInfo().GetPermissionSet(), &provider.ResourcePermissions{}):
   365  				// If the link is internal
   366  				if !userExists {
   367  					w.Header().Add(WwwAuthenticate, fmt.Sprintf("Bearer realm=\"%s\", charset=\"UTF-8\"", r.Host))
   368  					w.WriteHeader(http.StatusUnauthorized)
   369  					b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Bearer' header found", "", ErrMissingBearerAuth)
   370  					errors.HandleWebdavError(log, w, b, err)
   371  					return
   372  				}
   373  				fallthrough
   374  			case sRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
   375  				log.Debug().Str("token", token).Interface("status", res.Status).Msg("resource not found")
   376  				w.WriteHeader(http.StatusNotFound) // log the difference
   377  				return
   378  			case sRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
   379  				log.Debug().Str("token", token).Interface("status", res.Status).Msg("unauthorized")
   380  				w.WriteHeader(http.StatusUnauthorized)
   381  				return
   382  			case sRes.Status.Code != rpc.Code_CODE_OK:
   383  				log.Error().Str("token", token).Interface("status", res.Status).Msg("grpc stat request failed")
   384  				w.WriteHeader(http.StatusInternalServerError)
   385  				return
   386  			}
   387  			log.Debug().Interface("statInfo", sRes.Info).Msg("Stat info from public link token path")
   388  
   389  			ctx := ContextWithTokenStatInfo(ctx, sRes.Info)
   390  			r = r.WithContext(ctx)
   391  			if sRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
   392  				h.PublicFileHandler.Handler(s).ServeHTTP(w, r)
   393  			} else {
   394  				h.PublicFolderHandler.Handler(s).ServeHTTP(w, r)
   395  			}
   396  
   397  		default:
   398  			w.WriteHeader(http.StatusNotFound)
   399  			b, err := errors.Marshal(http.StatusNotFound, "File not found in root", "", ErrFileNotFoundInRoot)
   400  			errors.HandleWebdavError(log, w, b, err)
   401  		}
   402  	})
   403  }
   404  
   405  func getTokenStatInfo(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token string) (*provider.StatResponse, error) {
   406  	client, err := selector.Next()
   407  	if err != nil {
   408  		return nil, err
   409  	}
   410  
   411  	return client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{
   412  		ResourceId: &provider.ResourceId{
   413  			StorageId: utils.PublicStorageProviderID,
   414  			SpaceId:   utils.PublicStorageSpaceID,
   415  			OpaqueId:  token,
   416  		},
   417  	}})
   418  }
   419  
   420  func handleBasicAuth(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token, pw string) (*gatewayv1beta1.AuthenticateResponse, error) {
   421  	c, err := selector.Next()
   422  	if err != nil {
   423  		return nil, err
   424  	}
   425  	authenticateRequest := gatewayv1beta1.AuthenticateRequest{
   426  		Type:         "publicshares",
   427  		ClientId:     token,
   428  		ClientSecret: "password|" + pw,
   429  	}
   430  
   431  	return c.Authenticate(ctx, &authenticateRequest)
   432  }
   433  
   434  func handleSignatureAuth(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token, sig, expiration string) (*gatewayv1beta1.AuthenticateResponse, error) {
   435  	c, err := selector.Next()
   436  	if err != nil {
   437  		return nil, err
   438  	}
   439  	authenticateRequest := gatewayv1beta1.AuthenticateRequest{
   440  		Type:         "publicshares",
   441  		ClientId:     token,
   442  		ClientSecret: "signature|" + sig + "|" + expiration,
   443  	}
   444  
   445  	return c.Authenticate(ctx, &authenticateRequest)
   446  }
   447  
   448  func handleOCMAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, ocmshare, sharedSecret string) (*gatewayv1beta1.AuthenticateResponse, error) {
   449  	return c.Authenticate(ctx, &gatewayv1beta1.AuthenticateRequest{
   450  		Type:         "ocmshares",
   451  		ClientId:     ocmshare,
   452  		ClientSecret: sharedSecret,
   453  	})
   454  }