github.com/cs3org/reva/v2@v2.27.7/internal/http/services/appprovider/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 appprovider
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"net/http"
    25  	"net/url"
    26  	"path"
    27  	"strings"
    28  
    29  	appregistry "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  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    33  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    34  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    35  	"github.com/cs3org/reva/v2/pkg/rgrpc/status"
    36  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    37  	"github.com/cs3org/reva/v2/pkg/rhttp/global"
    38  	"github.com/cs3org/reva/v2/pkg/sharedconf"
    39  	"github.com/cs3org/reva/v2/pkg/storagespace"
    40  	"github.com/cs3org/reva/v2/pkg/utils"
    41  	iso6391 "github.com/emvi/iso-639-1"
    42  	"github.com/go-chi/chi/v5"
    43  	ua "github.com/mileusna/useragent"
    44  	"github.com/mitchellh/mapstructure"
    45  	"github.com/pkg/errors"
    46  	"github.com/rs/zerolog"
    47  	"google.golang.org/protobuf/proto"
    48  )
    49  
    50  func init() {
    51  	global.Register("appprovider", New)
    52  }
    53  
    54  // Config holds the config options for the HTTP appprovider service
    55  type Config struct {
    56  	Prefix            string `mapstructure:"prefix"`
    57  	GatewaySvc        string `mapstructure:"gatewaysvc"`
    58  	Insecure          bool   `mapstructure:"insecure"`
    59  	WebBaseURI        string `mapstructure:"webbaseuri"`
    60  	Web               Web    `mapstructure:"web"`
    61  	SecureViewAppAddr string `mapstructure:"secure_view_app_addr"`
    62  }
    63  
    64  // Web holds the config options for the URL parameters for Web
    65  type Web struct {
    66  	URLParamsMapping map[string]string `mapstructure:"urlparamsmapping"`
    67  	StaticURLParams  map[string]string `mapstructure:"staticurlparams"`
    68  }
    69  
    70  func (c *Config) init() {
    71  	if c.Prefix == "" {
    72  		c.Prefix = "app"
    73  	}
    74  	c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
    75  }
    76  
    77  type svc struct {
    78  	conf   *Config
    79  	router *chi.Mux
    80  }
    81  
    82  // New returns a new ocmd object
    83  func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
    84  
    85  	conf := &Config{}
    86  	if err := mapstructure.Decode(m, conf); err != nil {
    87  		return nil, err
    88  	}
    89  	conf.init()
    90  
    91  	r := chi.NewRouter()
    92  	s := &svc{
    93  		conf:   conf,
    94  		router: r,
    95  	}
    96  
    97  	if err := s.routerInit(); err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	_ = chi.Walk(s.router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
   102  		log.Debug().Str("service", "approvider").Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
   103  		return nil
   104  	})
   105  
   106  	return s, nil
   107  }
   108  
   109  const (
   110  	openModeNormal = iota
   111  	openModeWeb
   112  )
   113  
   114  func (s *svc) routerInit() error {
   115  	s.router.Get("/list", s.handleList)
   116  	s.router.Post("/new", s.handleNew)
   117  	s.router.Post("/open", s.handleOpen(openModeNormal))
   118  	s.router.Post("/open-with-web", s.handleOpen(openModeWeb))
   119  	return nil
   120  }
   121  
   122  // Close performs cleanup.
   123  func (s *svc) Close() error {
   124  	return nil
   125  }
   126  
   127  func (s *svc) Prefix() string {
   128  	return s.conf.Prefix
   129  }
   130  
   131  func (s *svc) Unprotected() []string {
   132  	return []string{"/list"}
   133  }
   134  
   135  func (s *svc) Handler() http.Handler {
   136  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   137  		s.router.ServeHTTP(w, r)
   138  	})
   139  }
   140  
   141  func (s *svc) handleNew(w http.ResponseWriter, r *http.Request) {
   142  	ctx := r.Context()
   143  
   144  	client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
   145  	if err != nil {
   146  		writeError(w, r, appErrorServerError, "error getting grpc gateway client", err)
   147  		return
   148  	}
   149  
   150  	err = r.ParseForm()
   151  	if err != nil {
   152  		writeError(w, r, appErrorInvalidParameter, "parameters could not be parsed", nil)
   153  	}
   154  
   155  	if r.Form.Get("template") != "" {
   156  		// TODO in the future we want to create a file out of the given template
   157  		writeError(w, r, appErrorUnimplemented, "template is not implemented", nil)
   158  		return
   159  	}
   160  
   161  	parentContainerIDStr := r.Form.Get("parent_container_id")
   162  	if parentContainerIDStr == "" {
   163  		writeError(w, r, appErrorInvalidParameter, "missing parent container ID", nil)
   164  		return
   165  	}
   166  
   167  	parentContainerID, err := storagespace.ParseID(parentContainerIDStr)
   168  	if err != nil {
   169  		writeError(w, r, appErrorInvalidParameter, "invalid parent container ID", nil)
   170  		return
   171  	}
   172  
   173  	filename := r.Form.Get("filename")
   174  	if filename == "" {
   175  		writeError(w, r, appErrorInvalidParameter, "missing filename", nil)
   176  		return
   177  	}
   178  
   179  	dirPart, filePart := path.Split(filename)
   180  	if dirPart != "" || filePart != filename {
   181  		writeError(w, r, appErrorInvalidParameter, "the filename must not contain a path segment", nil)
   182  		return
   183  	}
   184  
   185  	statParentContainerReq := &provider.StatRequest{
   186  		Ref: &provider.Reference{
   187  			ResourceId: &parentContainerID,
   188  		},
   189  	}
   190  	parentContainer, err := client.Stat(ctx, statParentContainerReq)
   191  	if err != nil {
   192  		writeError(w, r, appErrorServerError, "error sending a grpc stat request", err)
   193  		return
   194  	}
   195  
   196  	if parentContainer.Status.Code != rpc.Code_CODE_OK {
   197  		writeError(w, r, appErrorNotFound, "the parent container is not accessible or does not exist", err)
   198  		return
   199  	}
   200  
   201  	if parentContainer.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
   202  		writeError(w, r, appErrorInvalidParameter, "the parent container id does not point to a container", nil)
   203  		return
   204  	}
   205  
   206  	fileRef := &provider.Reference{
   207  		ResourceId: &parentContainerID,
   208  		Path:       utils.MakeRelativePath(filename),
   209  	}
   210  
   211  	statFileReq := &provider.StatRequest{
   212  		Ref: fileRef,
   213  	}
   214  	statFileRes, err := client.Stat(ctx, statFileReq)
   215  	if err != nil {
   216  		writeError(w, r, appErrorServerError, "failed to stat the file", err)
   217  		return
   218  	}
   219  
   220  	if statFileRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
   221  		if statFileRes.Status.Code == rpc.Code_CODE_OK {
   222  			writeError(w, r, appErrorAlreadyExists, "the file already exists", nil)
   223  			return
   224  		}
   225  		writeError(w, r, appErrorServerError, "statting the file returned unexpected status code", err)
   226  		return
   227  	}
   228  
   229  	touchFileReq := &provider.TouchFileRequest{
   230  		Ref: fileRef,
   231  	}
   232  
   233  	touchRes, err := client.TouchFile(ctx, touchFileReq)
   234  	if err != nil {
   235  		writeError(w, r, appErrorServerError, "error sending a grpc touchfile request", err)
   236  		return
   237  	}
   238  
   239  	if touchRes.Status.Code != rpc.Code_CODE_OK {
   240  		if touchRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
   241  			writeError(w, r, appErrorPermissionDenied, "permission denied to create the file", nil)
   242  			return
   243  		}
   244  		writeError(w, r, appErrorServerError, "touching the file failed", nil)
   245  		return
   246  	}
   247  
   248  	// Stat the newly created file
   249  	statRes, err := client.Stat(ctx, statFileReq)
   250  	if err != nil {
   251  		writeError(w, r, appErrorServerError, "statting the created file failed", err)
   252  		return
   253  	}
   254  
   255  	if statRes.Status.Code != rpc.Code_CODE_OK {
   256  		writeError(w, r, appErrorServerError, "statting the created file failed", nil)
   257  		return
   258  	}
   259  
   260  	if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
   261  		writeError(w, r, appErrorInvalidParameter, "the given file id does not point to a file", nil)
   262  		return
   263  	}
   264  	fileid := storagespace.FormatResourceID(statRes.Info.Id)
   265  
   266  	js, err := json.Marshal(
   267  		map[string]interface{}{
   268  			"file_id": fileid,
   269  		},
   270  	)
   271  	if err != nil {
   272  		writeError(w, r, appErrorServerError, "error marshalling JSON response", err)
   273  		return
   274  	}
   275  
   276  	w.Header().Set("Content-Type", "application/json")
   277  	if _, err = w.Write(js); err != nil {
   278  		writeError(w, r, appErrorServerError, "error writing JSON response", err)
   279  		return
   280  	}
   281  }
   282  
   283  func (s *svc) handleList(w http.ResponseWriter, r *http.Request) {
   284  	ctx := r.Context()
   285  	client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
   286  	if err != nil {
   287  		writeError(w, r, appErrorServerError, "error getting grpc gateway client", err)
   288  		return
   289  	}
   290  
   291  	listRes, err := client.ListSupportedMimeTypes(ctx, &appregistry.ListSupportedMimeTypesRequest{})
   292  	if err != nil {
   293  		writeError(w, r, appErrorServerError, "error listing supported mime types", err)
   294  		return
   295  	}
   296  	if listRes.Status.Code != rpc.Code_CODE_OK {
   297  		writeError(w, r, appErrorServerError, "error listing supported mime types", nil)
   298  		return
   299  	}
   300  
   301  	res := buildApps(listRes.MimeTypes, r.UserAgent(), s.conf.SecureViewAppAddr)
   302  
   303  	js, err := json.Marshal(map[string]interface{}{"mime-types": res})
   304  	if err != nil {
   305  		writeError(w, r, appErrorServerError, "error marshalling JSON response", err)
   306  		return
   307  	}
   308  
   309  	w.Header().Set("Content-Type", "application/json")
   310  	if _, err = w.Write(js); err != nil {
   311  		writeError(w, r, appErrorServerError, "error writing JSON response", err)
   312  		return
   313  	}
   314  }
   315  
   316  func (s *svc) handleOpen(openMode int) http.HandlerFunc {
   317  
   318  	return func(w http.ResponseWriter, r *http.Request) {
   319  		ctx := r.Context()
   320  
   321  		client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
   322  		if err != nil {
   323  			writeError(w, r, appErrorServerError, "Internal error with the gateway, please try again later", err)
   324  			return
   325  		}
   326  
   327  		err = r.ParseForm()
   328  		if err != nil {
   329  			writeError(w, r, appErrorInvalidParameter, "parameters could not be parsed", nil)
   330  		}
   331  
   332  		lang := r.Form.Get("lang")
   333  		parts := strings.Split(lang, "_")
   334  		if lang != "" && !iso6391.ValidCode(parts[0]) {
   335  			writeError(w, r, appErrorInvalidParameter, "lang parameter does not contain a valid ISO 639-1 language code in the language tag", nil)
   336  			return
   337  		}
   338  
   339  		fileID := r.Form.Get("file_id")
   340  
   341  		if fileID == "" {
   342  			writeError(w, r, appErrorInvalidParameter, "missing file ID", nil)
   343  			return
   344  		}
   345  
   346  		resourceID, err := storagespace.ParseID(fileID)
   347  		if err != nil {
   348  			writeError(w, r, appErrorInvalidParameter, "invalid file ID", nil)
   349  			return
   350  		}
   351  
   352  		fileRef := &provider.Reference{
   353  			ResourceId: &resourceID,
   354  			Path:       ".",
   355  		}
   356  
   357  		statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef})
   358  		if err != nil {
   359  			writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err)
   360  			return
   361  		}
   362  
   363  		if status := utils.ReadPlainFromOpaque(statRes.GetInfo().GetOpaque(), "status"); status == "processing" {
   364  			writeError(w, r, appErrorTooEarly, "The requested file is not yet available, please try again later", nil)
   365  			return
   366  		}
   367  
   368  		viewMode, err := getViewModeFromPublicScope(ctx)
   369  		if err != nil {
   370  			writeError(w, r, appErrorPermissionDenied, "permission denied to open the application", err)
   371  			return
   372  		}
   373  
   374  		if viewMode == gateway.OpenInAppRequest_VIEW_MODE_INVALID {
   375  			// we have no publicshare Role in the token scope
   376  			// do a stat request to assemble the permissions for this user
   377  			statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef})
   378  			if err != nil {
   379  				writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err)
   380  				return
   381  			}
   382  
   383  			if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
   384  				writeError(w, r, appErrorNotFound, "file does not exist", nil)
   385  				return
   386  			} else if statRes.Status.Code != rpc.Code_CODE_OK {
   387  				writeError(w, r, appErrorServerError, "failed to stat the file", nil)
   388  				return
   389  			}
   390  
   391  			if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
   392  				writeError(w, r, appErrorInvalidParameter, "the given file id does not point to a file", nil)
   393  				return
   394  			}
   395  
   396  			// Calculate the view mode from the resource permissions
   397  			viewMode = getViewMode(statRes.Info, r.Form.Get("view_mode"))
   398  			if viewMode == gateway.OpenInAppRequest_VIEW_MODE_INVALID {
   399  				writeError(w, r, appErrorInvalidParameter, "invalid view mode", err)
   400  				return
   401  			}
   402  		}
   403  
   404  		openReq := gateway.OpenInAppRequest{
   405  			Ref:      fileRef,
   406  			ViewMode: viewMode,
   407  			App:      r.Form.Get("app_name"),
   408  			Opaque:   utils.AppendPlainToOpaque(nil, "lang", lang),
   409  		}
   410  
   411  		templateID := r.Form.Get("template_id")
   412  		if templateID != "" {
   413  			openReq.Opaque = utils.AppendPlainToOpaque(openReq.Opaque, "template", templateID)
   414  		}
   415  		openRes, err := client.OpenInApp(ctx, &openReq)
   416  		if err != nil {
   417  			writeError(w, r, appErrorServerError,
   418  				"Error contacting the requested application, please use a different one or try again later", err)
   419  			return
   420  		}
   421  		if openRes.Status.Code != rpc.Code_CODE_OK {
   422  			if openRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
   423  				writeError(w, r, appErrorNotFound, openRes.Status.Message, nil)
   424  				return
   425  			}
   426  			writeError(w, r, appErrorServerError, openRes.Status.Message,
   427  				status.NewErrorFromCode(openRes.Status.Code, "error calling OpenInApp"))
   428  			return
   429  		}
   430  
   431  		var payload interface{}
   432  
   433  		switch openMode {
   434  		case openModeNormal:
   435  			payload = openRes.AppUrl
   436  
   437  		case openModeWeb:
   438  			payload, err = newOpenInWebResponse(s.conf.WebBaseURI, s.conf.Web.URLParamsMapping, s.conf.Web.StaticURLParams, fileID, r.Form.Get("app_name"), r.Form.Get("view_mode"))
   439  			if err != nil {
   440  				writeError(w, r, appErrorServerError, "Internal error",
   441  					errors.Wrap(err, "error building OpenInWeb response"))
   442  				return
   443  			}
   444  
   445  		default:
   446  			writeError(w, r, appErrorServerError, "Internal error with the open mode",
   447  				errors.New("unknown open mode"))
   448  			return
   449  
   450  		}
   451  
   452  		js, err := json.Marshal(payload)
   453  		if err != nil {
   454  			writeError(w, r, appErrorServerError, "Internal error with JSON payload",
   455  				errors.Wrap(err, "error marshalling JSON response"))
   456  			return
   457  		}
   458  
   459  		w.Header().Set("Content-Type", "application/json")
   460  		if _, err = w.Write(js); err != nil {
   461  			writeError(w, r, appErrorServerError, "Internal error with JSON payload",
   462  				errors.Wrap(err, "error writing JSON response"))
   463  			return
   464  		}
   465  	}
   466  }
   467  
   468  type openInWebResponse struct {
   469  	URI string `json:"uri"`
   470  }
   471  
   472  func newOpenInWebResponse(baseURI string, params, staticParams map[string]string, fileID, appName, viewMode string) (openInWebResponse, error) {
   473  
   474  	uri, err := url.Parse(baseURI)
   475  	if err != nil {
   476  		return openInWebResponse{}, err
   477  	}
   478  
   479  	query := uri.Query()
   480  
   481  	for key, val := range params {
   482  
   483  		switch val {
   484  		case "fileid":
   485  			if fileID != "" {
   486  				query.Add(key, fileID)
   487  			}
   488  		case "appname":
   489  			if appName != "" {
   490  				query.Add(key, appName)
   491  			}
   492  		case "viewmode":
   493  			if viewMode != "" {
   494  				query.Add(key, viewMode)
   495  			}
   496  		default:
   497  			return openInWebResponse{}, errors.New("unknown parameter mapper")
   498  		}
   499  
   500  	}
   501  
   502  	for key, val := range staticParams {
   503  		query.Add(key, val)
   504  	}
   505  
   506  	uri.RawQuery = query.Encode()
   507  
   508  	return openInWebResponse{URI: uri.String()}, nil
   509  }
   510  
   511  // MimeTypeInfo wraps the appregistry.MimeTypeInfo to change the app providers to ProviderInfos with a secure view flag
   512  type MimeTypeInfo struct {
   513  	appregistry.MimeTypeInfo
   514  	AppProviders []*ProviderInfo `json:"app_providers"`
   515  }
   516  
   517  // ProviderInfo wraps the appregistry.ProviderInfo to add a secure view flag
   518  type ProviderInfo struct {
   519  	appregistry.ProviderInfo
   520  	// TODO make this part of the CS3 provider info
   521  	SecureView bool   `json:"secure_view"`
   522  	TargetExt  string `json:"target_ext,omitempty"`
   523  }
   524  
   525  // buildApps rewrites the mime type info to only include apps that
   526  // * have a name
   527  // * can be called by the user agent, eg Desktop-only
   528  //
   529  // it also
   530  // * wraps the provider info to be able to add a secure view flag
   531  // * adds a secure view flag if the address matches the secure view app address and
   532  // * removes the address from the provider info to not expose internal addresses
   533  func buildApps(mimeTypes []*appregistry.MimeTypeInfo, userAgent, secureViewAppAddr string) []*MimeTypeInfo {
   534  	ua := ua.Parse(userAgent)
   535  	res := []*MimeTypeInfo{}
   536  	for _, m := range mimeTypes {
   537  		apps := []*ProviderInfo{}
   538  		for _, p := range m.AppProviders {
   539  			ep := &ProviderInfo{}
   540  			proto.Merge(&ep.ProviderInfo, p)
   541  			if p.Address == secureViewAppAddr {
   542  				ep.SecureView = true
   543  			}
   544  			p.Address = "" // address is internal only and not needed in the client
   545  			// apps are called by name, so if it has no name it cannot be called and should not be advertised
   546  			// also filter Desktop-only apps if ua is not Desktop
   547  			if p.Name != "" && (ua.Desktop || !p.DesktopOnly) {
   548  				apps = append(apps, ep)
   549  			}
   550  		}
   551  		if len(apps) > 0 {
   552  			mt := &MimeTypeInfo{}
   553  			addTemplateInfo(m, apps)
   554  			proto.Merge(&mt.MimeTypeInfo, m)
   555  			mt.AppProviders = apps
   556  			res = append(res, mt)
   557  		}
   558  	}
   559  	return res
   560  }
   561  
   562  func getViewMode(res *provider.ResourceInfo, vm string) gateway.OpenInAppRequest_ViewMode {
   563  	if vm != "" {
   564  		return utils.GetViewMode(vm)
   565  	}
   566  
   567  	var viewMode gateway.OpenInAppRequest_ViewMode
   568  	canEdit := res.PermissionSet.InitiateFileUpload
   569  	canView := res.PermissionSet.InitiateFileDownload
   570  
   571  	switch {
   572  	case canEdit && canView:
   573  		viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE
   574  	case canView:
   575  		viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_ONLY
   576  	default:
   577  		viewMode = gateway.OpenInAppRequest_VIEW_MODE_INVALID
   578  	}
   579  	return viewMode
   580  }
   581  
   582  // try to get the view mode from a publicshare scope
   583  func getViewModeFromPublicScope(ctx context.Context) (gateway.OpenInAppRequest_ViewMode, error) {
   584  	scopes, ok := ctxpkg.ContextGetScopes(ctx)
   585  	if ok {
   586  		for key, scope := range scopes {
   587  			if strings.HasPrefix(key, "publicshare:") {
   588  				switch scope.GetRole() {
   589  				case providerv1beta1.Role_ROLE_VIEWER:
   590  					return gateway.OpenInAppRequest_VIEW_MODE_VIEW_ONLY, nil
   591  				case providerv1beta1.Role_ROLE_EDITOR:
   592  					return gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE, nil
   593  				default:
   594  					return gateway.OpenInAppRequest_VIEW_MODE_INVALID, errors.New("invalid view mode in publicshare scope")
   595  				}
   596  			}
   597  		}
   598  	}
   599  	return gateway.OpenInAppRequest_VIEW_MODE_INVALID, nil
   600  }