github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/ocdav.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  	"net/http"
    24  	"path"
    25  	"strings"
    26  	"time"
    27  
    28  	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
    29  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    30  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    31  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    32  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/config"
    33  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net"
    34  	"github.com/cs3org/reva/v2/pkg/appctx"
    35  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    36  	"github.com/cs3org/reva/v2/pkg/errtypes"
    37  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    38  	"github.com/cs3org/reva/v2/pkg/rhttp"
    39  	"github.com/cs3org/reva/v2/pkg/rhttp/global"
    40  	"github.com/cs3org/reva/v2/pkg/rhttp/router"
    41  	"github.com/cs3org/reva/v2/pkg/storage/favorite"
    42  	"github.com/cs3org/reva/v2/pkg/storage/favorite/registry"
    43  	"github.com/cs3org/reva/v2/pkg/storage/utils/templates"
    44  	"github.com/cs3org/reva/v2/pkg/utils"
    45  	"github.com/jellydator/ttlcache/v2"
    46  	"github.com/mitchellh/mapstructure"
    47  	"github.com/rs/zerolog"
    48  	"google.golang.org/grpc/codes"
    49  	"google.golang.org/grpc/metadata"
    50  	"google.golang.org/grpc/status"
    51  )
    52  
    53  // name is the Tracer name used to identify this instrumentation library.
    54  const tracerName = "ocdav"
    55  
    56  func init() {
    57  	global.Register("ocdav", New)
    58  }
    59  
    60  type svc struct {
    61  	c                *config.Config
    62  	webDavHandler    *WebDavHandler
    63  	davHandler       *DavHandler
    64  	favoritesManager favorite.Manager
    65  	client           *http.Client
    66  	gatewaySelector  pool.Selectable[gateway.GatewayAPIClient]
    67  	// LockSystem is the lock management system.
    68  	LockSystem          LockSystem
    69  	userIdentifierCache *ttlcache.Cache
    70  	nameValidators      []Validator
    71  }
    72  
    73  func (s *svc) Config() *config.Config {
    74  	return s.c
    75  }
    76  
    77  func getFavoritesManager(c *config.Config) (favorite.Manager, error) {
    78  	if f, ok := registry.NewFuncs[c.FavoriteStorageDriver]; ok {
    79  		return f(c.FavoriteStorageDrivers[c.FavoriteStorageDriver])
    80  	}
    81  	return nil, errtypes.NotFound("driver not found: " + c.FavoriteStorageDriver)
    82  }
    83  func getLockSystem(c *config.Config) (LockSystem, error) {
    84  	// TODO in memory implementation
    85  	selector, err := pool.GatewaySelector(c.GatewaySvc)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	return NewCS3LS(selector), nil
    90  }
    91  
    92  // New returns a new ocdav service
    93  func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
    94  	conf := &config.Config{}
    95  	if err := mapstructure.Decode(m, conf); err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	conf.Init()
   100  
   101  	fm, err := getFavoritesManager(conf)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	ls, err := getLockSystem(conf)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	return NewWith(conf, fm, ls, log, nil)
   111  }
   112  
   113  // NewWith returns a new ocdav service
   114  func NewWith(conf *config.Config, fm favorite.Manager, ls LockSystem, _ *zerolog.Logger, selector pool.Selectable[gateway.GatewayAPIClient]) (global.Service, error) {
   115  	// be safe - init the conf again
   116  	conf.Init()
   117  
   118  	s := &svc{
   119  		c:             conf,
   120  		webDavHandler: new(WebDavHandler),
   121  		davHandler:    new(DavHandler),
   122  		client: rhttp.GetHTTPClient(
   123  			rhttp.Timeout(time.Duration(conf.Timeout*int64(time.Second))),
   124  			rhttp.Insecure(conf.Insecure),
   125  		),
   126  		gatewaySelector:     selector,
   127  		favoritesManager:    fm,
   128  		LockSystem:          ls,
   129  		userIdentifierCache: ttlcache.NewCache(),
   130  		nameValidators:      ValidatorsFromConfig(conf),
   131  	}
   132  	_ = s.userIdentifierCache.SetTTL(60 * time.Second)
   133  
   134  	// initialize handlers and set default configs
   135  	if err := s.webDavHandler.init(conf.WebdavNamespace, true); err != nil {
   136  		return nil, err
   137  	}
   138  	if err := s.davHandler.init(conf); err != nil {
   139  		return nil, err
   140  	}
   141  	if selector == nil {
   142  		var err error
   143  		s.gatewaySelector, err = pool.GatewaySelector(s.c.GatewaySvc)
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  	}
   148  	return s, nil
   149  }
   150  
   151  func (s *svc) Prefix() string {
   152  	return s.c.Prefix
   153  }
   154  
   155  func (s *svc) Close() error {
   156  	return nil
   157  }
   158  
   159  func (s *svc) Unprotected() []string {
   160  	return []string{"/status.php", "/status", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/remote.php/dav/ocm/", "/dav/ocm/"}
   161  }
   162  
   163  func (s *svc) Handler() http.Handler {
   164  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   165  		ctx := r.Context()
   166  		log := appctx.GetLogger(ctx)
   167  
   168  		// TODO(jfd): do we need this?
   169  		// fake litmus testing for empty namespace: see https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/litmus_test_server.go#L58-L89
   170  		if r.Header.Get(net.HeaderLitmus) == "props: 3 (propfind_invalid2)" {
   171  			http.Error(w, "400 Bad Request", http.StatusBadRequest)
   172  			return
   173  		}
   174  
   175  		// to build correct href prop urls we need to keep track of the base path
   176  		// always starts with /
   177  		base := path.Join("/", s.Prefix())
   178  
   179  		var head string
   180  		head, r.URL.Path = router.ShiftPath(r.URL.Path)
   181  		log.Debug().Str("method", r.Method).Str("head", head).Str("tail", r.URL.Path).Msg("http routing")
   182  		switch head {
   183  		case "status.php", "status":
   184  			s.doStatus(w, r)
   185  			return
   186  		case "remote.php":
   187  			// skip optional "remote.php"
   188  			head, r.URL.Path = router.ShiftPath(r.URL.Path)
   189  
   190  			// yet, add it to baseURI
   191  			base = path.Join(base, "remote.php")
   192  		case "apps":
   193  			head, r.URL.Path = router.ShiftPath(r.URL.Path)
   194  			if head == "files" {
   195  				s.handleLegacyPath(w, r)
   196  				return
   197  			}
   198  		case "index.php":
   199  			head, r.URL.Path = router.ShiftPath(r.URL.Path)
   200  			if head == "s" {
   201  				token := r.URL.Path
   202  				rURL := s.c.PublicURL + path.Join(head, token)
   203  
   204  				http.Redirect(w, r, rURL, http.StatusMovedPermanently)
   205  				return
   206  			}
   207  		}
   208  		switch head {
   209  		// the old `/webdav` endpoint uses remote.php/webdav/$path
   210  		case "webdav":
   211  			// for oc we need to prepend /home as the path that will be passed to the home storage provider
   212  			// will not contain the username
   213  			base = path.Join(base, "webdav")
   214  			ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
   215  			r = r.WithContext(ctx)
   216  			s.webDavHandler.Handler(s).ServeHTTP(w, r)
   217  			return
   218  		case "dav":
   219  			// cern uses /dav/files/$namespace -> /$namespace/...
   220  			// oc uses /dav/files/$user -> /$home/$user/...
   221  			// for oc we need to prepend the path to user homes
   222  			// or we take the path starting at /dav and allow rewriting it?
   223  			base = path.Join(base, "dav")
   224  			ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
   225  			r = r.WithContext(ctx)
   226  			s.davHandler.Handler(s).ServeHTTP(w, r)
   227  			return
   228  		}
   229  		log.Warn().Msg("resource not found")
   230  		w.WriteHeader(http.StatusNotFound)
   231  	})
   232  }
   233  
   234  func (s *svc) ApplyLayout(ctx context.Context, ns string, useLoggedInUserNS bool, requestPath string) (string, string, error) {
   235  	// If useLoggedInUserNS is false, that implies that the request is coming from
   236  	// the FilesHandler method invoked by a /dav/files/fileOwner where fileOwner
   237  	// is not the same as the logged in user. In that case, we'll treat fileOwner
   238  	// as the username whose files are to be accessed and use that in the
   239  	// namespace template.
   240  	u, ok := ctxpkg.ContextGetUser(ctx)
   241  	if !ok || !useLoggedInUserNS {
   242  		var requestUsernameOrID string
   243  		requestUsernameOrID, requestPath = router.ShiftPath(requestPath)
   244  
   245  		// Check if this is a Userid
   246  		client, err := s.gatewaySelector.Next()
   247  		if err != nil {
   248  			return "", "", err
   249  		}
   250  
   251  		userRes, err := client.GetUser(ctx, &userpb.GetUserRequest{
   252  			UserId: &userpb.UserId{OpaqueId: requestUsernameOrID},
   253  		})
   254  		if err != nil {
   255  			return "", "", err
   256  		}
   257  
   258  		// If it's not a userid try if it is a user name
   259  		if userRes.Status.Code != rpc.Code_CODE_OK {
   260  			res, err := client.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
   261  				Claim: "username",
   262  				Value: requestUsernameOrID,
   263  			})
   264  			if err != nil {
   265  				return "", "", err
   266  			}
   267  			userRes.Status = res.Status
   268  			userRes.User = res.User
   269  		}
   270  
   271  		// If still didn't find a user, fallback
   272  		if userRes.Status.Code != rpc.Code_CODE_OK {
   273  			userRes.User = &userpb.User{
   274  				Username: requestUsernameOrID,
   275  				Id:       &userpb.UserId{OpaqueId: requestUsernameOrID},
   276  			}
   277  		}
   278  
   279  		u = userRes.User
   280  	}
   281  
   282  	return templates.WithUser(u, ns), requestPath, nil
   283  }
   284  
   285  func authContextForUser(client gateway.GatewayAPIClient, userID *userpb.UserId, machineAuthAPIKey string) (context.Context, error) {
   286  	if machineAuthAPIKey == "" {
   287  		return nil, errtypes.NotSupported("machine auth not configured")
   288  	}
   289  	// Get auth
   290  	granteeCtx := ctxpkg.ContextSetUser(context.Background(), &userpb.User{Id: userID})
   291  
   292  	authRes, err := client.Authenticate(granteeCtx, &gateway.AuthenticateRequest{
   293  		Type:         "machine",
   294  		ClientId:     "userid:" + userID.OpaqueId,
   295  		ClientSecret: machineAuthAPIKey,
   296  	})
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  	if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
   301  		return nil, errtypes.NewErrtypeFromStatus(authRes.Status)
   302  	}
   303  	granteeCtx = metadata.AppendToOutgoingContext(granteeCtx, ctxpkg.TokenHeader, authRes.Token)
   304  	return granteeCtx, nil
   305  }
   306  
   307  func (s *svc) sspReferenceIsChildOf(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], child, parent *provider.Reference) (bool, error) {
   308  	client, err := selector.Next()
   309  	if err != nil {
   310  		return false, err
   311  	}
   312  	parentStatRes, err := client.Stat(ctx, &provider.StatRequest{Ref: parent})
   313  	if err != nil {
   314  		return false, err
   315  	}
   316  	if parentStatRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
   317  		return false, errtypes.NewErrtypeFromStatus(parentStatRes.GetStatus())
   318  	}
   319  	parentAuthCtx, err := authContextForUser(client, parentStatRes.GetInfo().GetOwner(), s.c.MachineAuthAPIKey)
   320  	if err != nil {
   321  		return false, err
   322  	}
   323  	parentPathRes, err := client.GetPath(parentAuthCtx, &provider.GetPathRequest{ResourceId: parentStatRes.GetInfo().GetId()})
   324  	if err != nil {
   325  		return false, err
   326  	}
   327  
   328  	childStatRes, err := client.Stat(ctx, &provider.StatRequest{Ref: child})
   329  	if err != nil {
   330  		return false, err
   331  	}
   332  	if childStatRes.GetStatus().GetCode() == rpc.Code_CODE_NOT_FOUND && utils.IsRelativeReference(child) && child.Path != "." {
   333  		childParentRef := &provider.Reference{
   334  			ResourceId: child.ResourceId,
   335  			Path:       utils.MakeRelativePath(path.Dir(child.Path)),
   336  		}
   337  		childStatRes, err = client.Stat(ctx, &provider.StatRequest{Ref: childParentRef})
   338  		if err != nil {
   339  			return false, err
   340  		}
   341  	}
   342  	if childStatRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
   343  		return false, errtypes.NewErrtypeFromStatus(parentStatRes.Status)
   344  	}
   345  	// TODO: this should use service accounts https://github.com/owncloud/ocis/issues/7597
   346  	childAuthCtx, err := authContextForUser(client, childStatRes.GetInfo().GetOwner(), s.c.MachineAuthAPIKey)
   347  	if err != nil {
   348  		return false, err
   349  	}
   350  	childPathRes, err := client.GetPath(childAuthCtx, &provider.GetPathRequest{ResourceId: childStatRes.GetInfo().GetId()})
   351  	if err != nil {
   352  		return false, err
   353  	}
   354  
   355  	cp := childPathRes.Path + "/"
   356  	pp := parentPathRes.Path + "/"
   357  	return strings.HasPrefix(cp, pp), nil
   358  }
   359  
   360  func (s *svc) referenceIsChildOf(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], child, parent *provider.Reference) (bool, error) {
   361  	if child.ResourceId.SpaceId != parent.ResourceId.SpaceId {
   362  		return false, nil // Not on the same storage -> not a child
   363  	}
   364  
   365  	if utils.ResourceIDEqual(child.ResourceId, parent.ResourceId) {
   366  		return strings.HasPrefix(child.Path, parent.Path+"/"), nil // Relative to the same resource -> compare paths
   367  	}
   368  
   369  	if child.ResourceId.SpaceId == utils.ShareStorageSpaceID || parent.ResourceId.SpaceId == utils.ShareStorageSpaceID {
   370  		// the sharesstorageprovider needs some special handling
   371  		return s.sspReferenceIsChildOf(ctx, selector, child, parent)
   372  	}
   373  
   374  	client, err := selector.Next()
   375  	if err != nil {
   376  		return false, err
   377  	}
   378  
   379  	// the references are on the same storage but relative to different resources
   380  	// -> we need to get the path for both resources
   381  	childPathRes, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: child.ResourceId})
   382  	if err != nil {
   383  		if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
   384  			return false, nil // the storage provider doesn't support GetPath() -> rely on it taking care of recursion issues
   385  		}
   386  		return false, err
   387  	}
   388  	parentPathRes, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: parent.ResourceId})
   389  	if err != nil {
   390  		return false, err
   391  	}
   392  
   393  	cp := path.Join(childPathRes.Path, child.Path) + "/"
   394  	pp := path.Join(parentPathRes.Path, parent.Path) + "/"
   395  	return strings.HasPrefix(cp, pp), nil
   396  }
   397  
   398  // filename returns the base filename from a path and replaces any slashes with an empty string
   399  func filename(p string) string {
   400  	return strings.Trim(path.Base(p), "/")
   401  }