github.com/cs3org/reva/v2@v2.27.7/internal/grpc/services/gateway/appprovider.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 gateway
    20  
    21  import (
    22  	"context"
    23  	"crypto/tls"
    24  	"fmt"
    25  	"net/url"
    26  	"strings"
    27  
    28  	providerpb "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
    29  	registry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
    30  	providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
    31  	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
    32  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    33  	ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
    34  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    35  	storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    36  	typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    37  	"github.com/cs3org/reva/v2/pkg/appctx"
    38  	"github.com/cs3org/reva/v2/pkg/auth/scope"
    39  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    40  	"github.com/cs3org/reva/v2/pkg/errtypes"
    41  	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
    42  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    43  	"github.com/cs3org/reva/v2/pkg/token"
    44  	"github.com/cs3org/reva/v2/pkg/utils"
    45  	"github.com/pkg/errors"
    46  	"google.golang.org/grpc"
    47  	"google.golang.org/grpc/credentials"
    48  	"google.golang.org/grpc/credentials/insecure"
    49  	"google.golang.org/grpc/metadata"
    50  )
    51  
    52  func (s *svc) OpenInApp(ctx context.Context, req *gateway.OpenInAppRequest) (*providerpb.OpenInAppResponse, error) {
    53  	statRes, err := s.Stat(ctx, &storageprovider.StatRequest{
    54  		Ref: req.Ref,
    55  	})
    56  	if err != nil {
    57  		return &providerpb.OpenInAppResponse{
    58  			Status: status.NewInternal(ctx, "gateway: error calling Stat on the resource path for the app provider: "+req.Ref.GetPath()),
    59  		}, nil
    60  	}
    61  	if statRes.Status.Code != rpc.Code_CODE_OK {
    62  		return &providerpb.OpenInAppResponse{
    63  			Status: statRes.Status,
    64  		}, nil
    65  	}
    66  
    67  	fileInfo := statRes.Info
    68  
    69  	// The file is a share
    70  	if fileInfo.Type == storageprovider.ResourceType_RESOURCE_TYPE_REFERENCE {
    71  		uri, err := url.Parse(fileInfo.Target)
    72  		if err != nil {
    73  			return &providerpb.OpenInAppResponse{
    74  				Status: status.NewInternal(ctx, "gateway: error parsing target uri: "+fileInfo.Target),
    75  			}, nil
    76  		}
    77  		if uri.Scheme == "webdav" {
    78  			insecure, skipVerify := getGRPCConfig(req.Opaque)
    79  			return s.openFederatedShares(ctx, fileInfo.Target, req.ViewMode, req.App, insecure, skipVerify, "")
    80  		}
    81  
    82  		res, err := s.Stat(ctx, &storageprovider.StatRequest{
    83  			Ref: req.Ref,
    84  		})
    85  		if err != nil {
    86  			return &providerpb.OpenInAppResponse{
    87  				Status: status.NewInternal(ctx, "gateway: error calling Stat on the resource path for the app provider: "+req.Ref.GetPath()),
    88  			}, nil
    89  		}
    90  		if res.Status.Code != rpc.Code_CODE_OK {
    91  			return &providerpb.OpenInAppResponse{
    92  				Status: status.NewInternal(ctx, "Stat failed on the resource path for the app provider: "+req.Ref.GetPath()),
    93  			}, nil
    94  		}
    95  		fileInfo = res.Info
    96  	}
    97  	return s.openLocalResources(ctx, fileInfo, req.ViewMode, req.App, req.Opaque)
    98  }
    99  
   100  func (s *svc) openFederatedShares(ctx context.Context, targetURL string, vm gateway.OpenInAppRequest_ViewMode, app string,
   101  	insecure, skipVerify bool, nameQueries ...string) (*providerpb.OpenInAppResponse, error) {
   102  	log := appctx.GetLogger(ctx)
   103  	targetURL, err := appendNameQuery(targetURL, nameQueries...)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	ep, err := s.extractEndpointInfo(ctx, targetURL)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	ref := &storageprovider.Reference{Path: ep.filePath}
   113  	appProviderReq := &gateway.OpenInAppRequest{
   114  		Ref:      ref,
   115  		ViewMode: vm,
   116  		App:      app,
   117  	}
   118  
   119  	meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
   120  		Domain: ep.endpoint,
   121  	})
   122  	if err != nil {
   123  		return nil, errors.Wrap(err, "gateway: error calling GetInfoByDomain")
   124  	}
   125  	var gatewayEP string
   126  	for _, s := range meshProvider.ProviderInfo.Services {
   127  		if strings.ToLower(s.Endpoint.Type.Name) == "gateway" {
   128  			gatewayEP = s.Endpoint.Path
   129  		}
   130  	}
   131  	log.Debug().Msgf("Forwarding OpenInApp request to: %s", gatewayEP)
   132  
   133  	conn, err := getConn(gatewayEP, insecure, skipVerify)
   134  	if err != nil {
   135  		log.Err(err).Msg("error connecting to remote reva")
   136  		return nil, errors.Wrap(err, "gateway: error connecting to remote reva")
   137  	}
   138  
   139  	gatewayClient := gateway.NewGatewayAPIClient(conn)
   140  	remoteCtx := ctxpkg.ContextSetToken(context.Background(), ep.token)
   141  	remoteCtx = metadata.AppendToOutgoingContext(remoteCtx, ctxpkg.TokenHeader, ep.token)
   142  
   143  	res, err := gatewayClient.OpenInApp(remoteCtx, appProviderReq)
   144  	if err != nil {
   145  		log.Err(err).Msg("error returned by remote OpenInApp call")
   146  		return nil, errors.Wrap(err, "gateway: error calling OpenInApp")
   147  	}
   148  	return res, nil
   149  }
   150  
   151  func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.ResourceInfo,
   152  	vm gateway.OpenInAppRequest_ViewMode, app string, opaque *typespb.Opaque) (*providerpb.OpenInAppResponse, error) {
   153  
   154  	accessToken, ok := ctxpkg.ContextGetToken(ctx)
   155  	if !ok || accessToken == "" {
   156  		return &providerpb.OpenInAppResponse{
   157  			Status: status.NewUnauthenticated(ctx, errtypes.InvalidCredentials("Access token is invalid or empty"), ""),
   158  		}, nil
   159  	}
   160  
   161  	provider, err := s.findAppProvider(ctx, ri, app)
   162  	if err != nil {
   163  		err = errors.Wrap(err, "gateway: error calling findAppProvider")
   164  		if _, ok := err.(errtypes.IsNotFound); ok {
   165  			return &providerpb.OpenInAppResponse{
   166  				Status: status.NewNotFound(ctx, "Could not find the requested app provider"),
   167  			}, nil
   168  		}
   169  		return nil, err
   170  	}
   171  
   172  	appProviderClient, err := pool.GetAppProviderClient(provider.Address)
   173  	if err != nil {
   174  		return nil, errors.Wrap(err, "gateway: error calling GetAppProviderClient")
   175  	}
   176  
   177  	appProviderReq, err := buildOpenInAppRequest(ctx, ri, vm, s.tokenmgr, accessToken, opaque)
   178  	if err != nil {
   179  		return nil, errors.Wrap(err, "gateway: error building OpenInApp request")
   180  	}
   181  
   182  	res, err := appProviderClient.OpenInApp(ctx, appProviderReq)
   183  	if err != nil {
   184  		return nil, errors.Wrap(err, "gateway: error calling OpenInApp")
   185  	}
   186  
   187  	return res, nil
   188  }
   189  
   190  func buildOpenInAppRequest(ctx context.Context, ri *storageprovider.ResourceInfo, vm gateway.OpenInAppRequest_ViewMode, tokenmgr token.Manager, accessToken string, opaque *typespb.Opaque) (*providerpb.OpenInAppRequest, error) {
   191  	// in case of a view only mode and a stat permission we need to create a view only token
   192  	if vm == gateway.OpenInAppRequest_VIEW_MODE_VIEW_ONLY && ri.GetPermissionSet().GetStat() && !ri.PermissionSet.GetInitiateFileDownload() {
   193  		// Limit scope to the resource
   194  		scope, err := scope.AddResourceInfoScope(ri, providerv1beta1.Role_ROLE_VIEWER, nil)
   195  		if err != nil {
   196  			return nil, err
   197  		}
   198  
   199  		// build a fake user object for the token
   200  		currentuser, ok := ctxpkg.ContextGetUser(ctx)
   201  		if !ok {
   202  			return nil, errors.New("user not found in context")
   203  		}
   204  
   205  		scopedUser := &userpb.User{
   206  			Id:          ri.GetOwner(), // the owner of the resource is always set, right?
   207  			DisplayName: "View Only user for " + currentuser.GetUsername(),
   208  		}
   209  
   210  		// mint a view only token
   211  		viewOnlyToken, err := tokenmgr.MintToken(ctx, scopedUser, scope)
   212  		if err != nil {
   213  			return nil, err
   214  		}
   215  
   216  		// TODO we should not append the token to the opaque, we should have a dedicated field in the request
   217  		opaque = utils.AppendPlainToOpaque(opaque, "viewOnlyToken", viewOnlyToken)
   218  	}
   219  
   220  	return &providerpb.OpenInAppRequest{
   221  		ResourceInfo: ri,
   222  		ViewMode:     providerpb.ViewMode(vm),
   223  		AccessToken:  accessToken,
   224  		// ViewOnlyToken: viewOnlyToken // scoped to the shared resource if the stat response hase a ViewOnly permission
   225  		Opaque: opaque,
   226  	}, nil
   227  }
   228  
   229  func (s *svc) findAppProvider(ctx context.Context, ri *storageprovider.ResourceInfo, app string) (*registry.ProviderInfo, error) {
   230  	c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
   231  	if err != nil {
   232  		err = errors.Wrap(err, "gateway: error getting appregistry client")
   233  		return nil, err
   234  	}
   235  
   236  	// when app is empty it means the user assumes a default behaviour.
   237  	// From a web perspective, means the user click on the file itself.
   238  	// Normally the file will get downloaded but if a suitable application exists
   239  	// the behaviour will change from download to open the file with the app.
   240  	if app == "" {
   241  		// If app is empty means that we need to rely on "default" behaviour.
   242  		// We currently do not have user preferences implemented so the only default
   243  		// we can currently enforce is one configured by the system admins, the
   244  		// "system default".
   245  		// If a default is not set we raise an error rather that giving the user the first provider in the list
   246  		// as the list is built on init time and is not deterministic, giving the user different results on service
   247  		// reload.
   248  		res, err := c.GetDefaultAppProviderForMimeType(ctx, &registry.GetDefaultAppProviderForMimeTypeRequest{
   249  			MimeType: ri.MimeType,
   250  		})
   251  		if err != nil {
   252  			err = errors.Wrap(err, "gateway: error calling GetDefaultAppProviderForMimeType")
   253  			return nil, err
   254  
   255  		}
   256  
   257  		// we've found a provider
   258  		if res.Status.Code == rpc.Code_CODE_OK && res.Provider != nil {
   259  			return res.Provider, nil
   260  		}
   261  
   262  		// we did not find a default provider
   263  		if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
   264  			err := errtypes.NotFound(fmt.Sprintf("gateway: default app provider for mime type:%s not found", ri.MimeType))
   265  			return nil, err
   266  		}
   267  
   268  		// response code is something else, bubble up error
   269  		// if a default is not set we abort as returning the first application available is not
   270  		// deterministic for the end-user as it depends on initialization order of the app approviders with the registry.
   271  		// It also provides a good hint to the system admin to configure the defaults accordingly.
   272  		err = errtypes.InternalError(fmt.Sprintf("gateway: unexpected grpc response status:%s when calling GetDefaultAppProviderForMimeType", res.Status))
   273  		return nil, err
   274  	}
   275  
   276  	// app has been forced and is set, we try to get an app provider that can satisfy it
   277  	// Note that we ask for the list of all available providers for a given resource
   278  	// even though we're only interested into the one set by the "app" parameter.
   279  	// A better call will be to issue a (to be added) GetAppProviderByName(app) method
   280  	// to just get what we ask for.
   281  	res, err := c.GetAppProviders(ctx, &registry.GetAppProvidersRequest{
   282  		ResourceInfo: ri,
   283  	})
   284  	if err != nil {
   285  		err = errors.Wrap(err, "gateway: error calling GetAppProviders")
   286  		return nil, err
   287  	}
   288  
   289  	// if the list of app providers is empty means we expect a CODE_NOT_FOUND in the response
   290  	if res.Status.Code != rpc.Code_CODE_OK {
   291  		if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
   292  			return nil, errtypes.NotFound("gateway: app provider not found for resource: " + ri.String())
   293  		}
   294  		return nil, errtypes.InternalError("gateway: error finding app providers")
   295  	}
   296  
   297  	// as long as the above mentioned GetAppProviderByName(app) method is not available
   298  	// we need to apply a manual filter
   299  	filteredProviders := []*registry.ProviderInfo{}
   300  	for _, p := range res.Providers {
   301  		if p.Name == app {
   302  			filteredProviders = append(filteredProviders, p)
   303  		}
   304  	}
   305  	res.Providers = filteredProviders
   306  
   307  	if len(res.Providers) == 0 {
   308  		return nil, errtypes.NotFound(fmt.Sprintf("app '%s' not found", app))
   309  	}
   310  
   311  	if len(res.Providers) == 1 {
   312  		return res.Providers[0], nil
   313  	}
   314  
   315  	// we should never arrive to the point of having more than one
   316  	// provider for the given "app" parameters sent by the user
   317  	return nil, errtypes.InternalError(fmt.Sprintf("gateway: user requested app %q and we provided %d applications", app, len(res.Providers)))
   318  
   319  }
   320  
   321  func getGRPCConfig(opaque *typespb.Opaque) (bool, bool) {
   322  	if opaque == nil {
   323  		return false, false
   324  	}
   325  	_, insecure := opaque.Map["insecure"]
   326  	_, skipVerify := opaque.Map["skip-verify"]
   327  	return insecure, skipVerify
   328  }
   329  
   330  func getConn(host string, ins, skipverify bool) (*grpc.ClientConn, error) {
   331  	if ins {
   332  		return grpc.NewClient(host, grpc.WithTransportCredentials(insecure.NewCredentials()))
   333  	}
   334  
   335  	// TODO(labkode): if in the future we want client-side certificate validation,
   336  	// we need to load the client cert here
   337  	tlsconf := &tls.Config{InsecureSkipVerify: skipverify}
   338  	creds := credentials.NewTLS(tlsconf)
   339  	return grpc.NewClient(host, grpc.WithTransportCredentials(creds))
   340  }